mirror of
https://github.com/neondatabase/neon.git
synced 2026-07-12 08:30:37 +00:00
Compare commits
61 Commits
release-pr
...
cloneable/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c90b082222 | ||
|
|
0957c8ea69 | ||
|
|
6cea6560e9 | ||
|
|
e38193c530 | ||
|
|
21949137ed | ||
|
|
02f94edb60 | ||
|
|
58327ef74d | ||
|
|
73be6bb736 | ||
|
|
40d7583906 | ||
|
|
7a68699abb | ||
|
|
f42d44342d | ||
|
|
d759fcb8bd | ||
|
|
76f95f06d8 | ||
|
|
7efd4554ab | ||
|
|
3c7235669a | ||
|
|
6dd84041a1 | ||
|
|
df7e301a54 | ||
|
|
470c7d5e0e | ||
|
|
4d99b6ff4d | ||
|
|
590301df08 | ||
|
|
c511786548 | ||
|
|
fe31baf985 | ||
|
|
b23e75ebfe | ||
|
|
24d7c37e6e | ||
|
|
f64eb0cbaf | ||
|
|
6ae4b89000 | ||
|
|
f7ec7668a2 | ||
|
|
038e967daf | ||
|
|
6a43f23eca | ||
|
|
868f194a3b | ||
|
|
9c6c780201 | ||
|
|
6123fe2d5e | ||
|
|
1577665c20 | ||
|
|
d8ebd1d771 | ||
|
|
c8a96cf722 | ||
|
|
56d505bce6 | ||
|
|
dae203ef69 | ||
|
|
1fb1315aed | ||
|
|
838622c594 | ||
|
|
3fd5a94a85 | ||
|
|
e7d6f525b3 | ||
|
|
e4ca3ac745 | ||
|
|
b69d103b90 | ||
|
|
208cbd52d4 | ||
|
|
c567ed0de0 | ||
|
|
c698cee19a | ||
|
|
4a3f32bf4a | ||
|
|
a963aab14b | ||
|
|
5bdba70f7d | ||
|
|
25fffd3a55 | ||
|
|
e00fd45bba | ||
|
|
3b8be98b67 | ||
|
|
3e72edede5 | ||
|
|
a650f7f5af | ||
|
|
fc3994eb71 | ||
|
|
781bf4945d | ||
|
|
a21c1174ed | ||
|
|
8d7ed2a4ee | ||
|
|
5b62749c42 | ||
|
|
af5bb67f08 | ||
|
|
589bfdfd02 |
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -1445,6 +1445,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"safekeeper_api",
|
"safekeeper_api",
|
||||||
|
"safekeeper_client",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -2054,6 +2055,7 @@ dependencies = [
|
|||||||
"axum-extra",
|
"axum-extra",
|
||||||
"camino",
|
"camino",
|
||||||
"camino-tempfile",
|
"camino-tempfile",
|
||||||
|
"clap",
|
||||||
"futures",
|
"futures",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
@@ -4236,6 +4238,8 @@ name = "pagebench"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
"camino",
|
"camino",
|
||||||
"clap",
|
"clap",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -4244,12 +4248,15 @@ dependencies = [
|
|||||||
"humantime-serde",
|
"humantime-serde",
|
||||||
"pageserver_api",
|
"pageserver_api",
|
||||||
"pageserver_client",
|
"pageserver_client",
|
||||||
|
"pageserver_page_api",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
"tonic 0.13.1",
|
||||||
"tracing",
|
"tracing",
|
||||||
"utils",
|
"utils",
|
||||||
"workspace_hack",
|
"workspace_hack",
|
||||||
@@ -4305,6 +4312,7 @@ dependencies = [
|
|||||||
"hashlink",
|
"hashlink",
|
||||||
"hex",
|
"hex",
|
||||||
"hex-literal",
|
"hex-literal",
|
||||||
|
"http 1.1.0",
|
||||||
"http-utils",
|
"http-utils",
|
||||||
"humantime",
|
"humantime",
|
||||||
"humantime-serde",
|
"humantime-serde",
|
||||||
@@ -4367,6 +4375,7 @@ dependencies = [
|
|||||||
"toml_edit",
|
"toml_edit",
|
||||||
"tonic 0.13.1",
|
"tonic 0.13.1",
|
||||||
"tonic-reflection",
|
"tonic-reflection",
|
||||||
|
"tower 0.5.2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-utils",
|
"tracing-utils",
|
||||||
"twox-hash",
|
"twox-hash",
|
||||||
@@ -4463,7 +4472,6 @@ dependencies = [
|
|||||||
"pageserver_api",
|
"pageserver_api",
|
||||||
"postgres_ffi",
|
"postgres_ffi",
|
||||||
"prost 0.13.5",
|
"prost 0.13.5",
|
||||||
"smallvec",
|
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tonic 0.13.1",
|
"tonic 0.13.1",
|
||||||
"tonic-build",
|
"tonic-build",
|
||||||
|
|||||||
13
Dockerfile
13
Dockerfile
@@ -110,6 +110,19 @@ RUN set -e \
|
|||||||
# System postgres for use with client libraries (e.g. in storage controller)
|
# System postgres for use with client libraries (e.g. in storage controller)
|
||||||
postgresql-15 \
|
postgresql-15 \
|
||||||
openssl \
|
openssl \
|
||||||
|
unzip \
|
||||||
|
curl \
|
||||||
|
&& ARCH=$(uname -m) \
|
||||||
|
&& if [ "$ARCH" = "x86_64" ]; then \
|
||||||
|
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"; \
|
||||||
|
elif [ "$ARCH" = "aarch64" ]; then \
|
||||||
|
curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip"; \
|
||||||
|
else \
|
||||||
|
echo "Unsupported architecture: $ARCH" && exit 1; \
|
||||||
|
fi \
|
||||||
|
&& unzip awscliv2.zip \
|
||||||
|
&& ./aws/install \
|
||||||
|
&& rm -rf aws awscliv2.zip \
|
||||||
&& rm -f /etc/apt/apt.conf.d/80-retries \
|
&& rm -f /etc/apt/apt.conf.d/80-retries \
|
||||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
|
||||||
&& useradd -d /data neon \
|
&& useradd -d /data neon \
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ RUN ./autogen.sh && \
|
|||||||
./configure --with-sfcgal=/usr/local/bin/sfcgal-config && \
|
./configure --with-sfcgal=/usr/local/bin/sfcgal-config && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
|
make staged-install && \
|
||||||
cd extensions/postgis && \
|
cd extensions/postgis && \
|
||||||
make clean && \
|
make clean && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
@@ -602,7 +603,7 @@ RUN case "${PG_VERSION:?}" in \
|
|||||||
;; \
|
;; \
|
||||||
esac && \
|
esac && \
|
||||||
wget https://github.com/knizhnik/online_advisor/archive/refs/tags/1.0.tar.gz -O online_advisor.tar.gz && \
|
wget https://github.com/knizhnik/online_advisor/archive/refs/tags/1.0.tar.gz -O online_advisor.tar.gz && \
|
||||||
echo "059b7d9e5a90013a58bdd22e9505b88406ce05790675eb2d8434e5b215652d54 online_advisor.tar.gz" | sha256sum --check && \
|
echo "37dcadf8f7cc8d6cc1f8831276ee245b44f1b0274f09e511e47a67738ba9ed0f online_advisor.tar.gz" | sha256sum --check && \
|
||||||
mkdir online_advisor-src && cd online_advisor-src && tar xzf ../online_advisor.tar.gz --strip-components=1 -C .
|
mkdir online_advisor-src && cd online_advisor-src && tar xzf ../online_advisor.tar.gz --strip-components=1 -C .
|
||||||
|
|
||||||
FROM pg-build AS online_advisor-build
|
FROM pg-build AS online_advisor-build
|
||||||
@@ -1180,14 +1181,14 @@ RUN cd exts/rag && \
|
|||||||
RUN cd exts/rag_bge_small_en_v15 && \
|
RUN cd exts/rag_bge_small_en_v15 && \
|
||||||
sed -i 's/pgrx = "0.14.1"/pgrx = { version = "0.14.1", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
|
sed -i 's/pgrx = "0.14.1"/pgrx = { version = "0.14.1", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
|
||||||
ORT_LIB_LOCATION=/ext-src/onnxruntime-src/build/Linux \
|
ORT_LIB_LOCATION=/ext-src/onnxruntime-src/build/Linux \
|
||||||
REMOTE_ONNX_URL=http://pg-ext-s3-gateway/pgrag-data/bge_small_en_v15.onnx \
|
REMOTE_ONNX_URL=http://pg-ext-s3-gateway.pg-ext-s3-gateway.svc.cluster.local/pgrag-data/bge_small_en_v15.onnx \
|
||||||
cargo pgrx install --release --features remote_onnx && \
|
cargo pgrx install --release --features remote_onnx && \
|
||||||
echo "trusted = true" >> /usr/local/pgsql/share/extension/rag_bge_small_en_v15.control
|
echo "trusted = true" >> /usr/local/pgsql/share/extension/rag_bge_small_en_v15.control
|
||||||
|
|
||||||
RUN cd exts/rag_jina_reranker_v1_tiny_en && \
|
RUN cd exts/rag_jina_reranker_v1_tiny_en && \
|
||||||
sed -i 's/pgrx = "0.14.1"/pgrx = { version = "0.14.1", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
|
sed -i 's/pgrx = "0.14.1"/pgrx = { version = "0.14.1", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
|
||||||
ORT_LIB_LOCATION=/ext-src/onnxruntime-src/build/Linux \
|
ORT_LIB_LOCATION=/ext-src/onnxruntime-src/build/Linux \
|
||||||
REMOTE_ONNX_URL=http://pg-ext-s3-gateway/pgrag-data/jina_reranker_v1_tiny_en.onnx \
|
REMOTE_ONNX_URL=http://pg-ext-s3-gateway.pg-ext-s3-gateway.svc.cluster.local/pgrag-data/jina_reranker_v1_tiny_en.onnx \
|
||||||
cargo pgrx install --release --features remote_onnx && \
|
cargo pgrx install --release --features remote_onnx && \
|
||||||
echo "trusted = true" >> /usr/local/pgsql/share/extension/rag_jina_reranker_v1_tiny_en.control
|
echo "trusted = true" >> /usr/local/pgsql/share/extension/rag_jina_reranker_v1_tiny_en.control
|
||||||
|
|
||||||
@@ -1842,10 +1843,25 @@ RUN make PG_VERSION="${PG_VERSION:?}" -C compute
|
|||||||
|
|
||||||
FROM pg-build AS extension-tests
|
FROM pg-build AS extension-tests
|
||||||
ARG PG_VERSION
|
ARG PG_VERSION
|
||||||
|
# This is required for the PostGIS test
|
||||||
|
RUN apt-get update && case $DEBIAN_VERSION in \
|
||||||
|
bullseye) \
|
||||||
|
apt-get install -y libproj19 libgdal28 time; \
|
||||||
|
;; \
|
||||||
|
bookworm) \
|
||||||
|
apt-get install -y libgdal32 libproj25 time; \
|
||||||
|
;; \
|
||||||
|
*) \
|
||||||
|
echo "Unknown Debian version ${DEBIAN_VERSION}" && exit 1 \
|
||||||
|
;; \
|
||||||
|
esac
|
||||||
|
|
||||||
COPY docker-compose/ext-src/ /ext-src/
|
COPY docker-compose/ext-src/ /ext-src/
|
||||||
|
|
||||||
COPY --from=pg-build /postgres /postgres
|
COPY --from=pg-build /postgres /postgres
|
||||||
#COPY --from=postgis-src /ext-src/ /ext-src/
|
COPY --from=postgis-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
COPY --from=postgis-build /ext-src/postgis-src /ext-src/postgis-src
|
||||||
|
COPY --from=postgis-build /sfcgal/* /usr
|
||||||
COPY --from=plv8-src /ext-src/ /ext-src/
|
COPY --from=plv8-src /ext-src/ /ext-src/
|
||||||
COPY --from=h3-pg-src /ext-src/h3-pg-src /ext-src/h3-pg-src
|
COPY --from=h3-pg-src /ext-src/h3-pg-src /ext-src/h3-pg-src
|
||||||
COPY --from=postgresql-unit-src /ext-src/ /ext-src/
|
COPY --from=postgresql-unit-src /ext-src/ /ext-src/
|
||||||
@@ -1886,6 +1902,7 @@ COPY compute/patches/pg_repack.patch /ext-src
|
|||||||
RUN cd /ext-src/pg_repack-src && patch -p1 </ext-src/pg_repack.patch && rm -f /ext-src/pg_repack.patch
|
RUN cd /ext-src/pg_repack-src && patch -p1 </ext-src/pg_repack.patch && rm -f /ext-src/pg_repack.patch
|
||||||
|
|
||||||
COPY --chmod=755 docker-compose/run-tests.sh /run-tests.sh
|
COPY --chmod=755 docker-compose/run-tests.sh /run-tests.sh
|
||||||
|
RUN echo /usr/local/pgsql/lib > /etc/ld.so.conf.d/00-neon.conf && /sbin/ldconfig
|
||||||
RUN apt-get update && apt-get install -y libtap-parser-sourcehandler-pgtap-perl jq \
|
RUN apt-get update && apt-get install -y libtap-parser-sourcehandler-pgtap-perl jq \
|
||||||
&& apt clean && rm -rf /ext-src/*.tar.gz /ext-src/*.patch /var/lib/apt/lists/*
|
&& apt clean && rm -rf /ext-src/*.tar.gz /ext-src/*.patch /var/lib/apt/lists/*
|
||||||
ENV PATH=/usr/local/pgsql/bin:$PATH
|
ENV PATH=/usr/local/pgsql/bin:$PATH
|
||||||
|
|||||||
121
compute/manifest.yaml
Normal file
121
compute/manifest.yaml
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
pg_settings:
|
||||||
|
# Common settings for primaries and replicas of all versions.
|
||||||
|
common:
|
||||||
|
# Check for client disconnection every 1 minute. By default, Postgres will detect the
|
||||||
|
# loss of the connection only at the next interaction with the socket, when it waits
|
||||||
|
# for, receives or sends data, so it will likely waste resources till the end of the
|
||||||
|
# query execution. There should be no drawbacks in setting this for everyone, so enable
|
||||||
|
# it by default. If anyone will complain, we can allow editing it.
|
||||||
|
# https://www.postgresql.org/docs/16/runtime-config-connection.html#GUC-CLIENT-CONNECTION-CHECK-INTERVAL
|
||||||
|
client_connection_check_interval: "60000" # 1 minute
|
||||||
|
# ---- IO ----
|
||||||
|
effective_io_concurrency: "20"
|
||||||
|
maintenance_io_concurrency: "100"
|
||||||
|
fsync: "off"
|
||||||
|
hot_standby: "off"
|
||||||
|
# We allow users to change this if needed, but by default we
|
||||||
|
# just don't want to see long-lasting idle transactions, as they
|
||||||
|
# prevent activity monitor from suspending projects.
|
||||||
|
idle_in_transaction_session_timeout: "300000" # 5 minutes
|
||||||
|
listen_addresses: "*"
|
||||||
|
# --- LOGGING ---- helps investigations
|
||||||
|
log_connections: "on"
|
||||||
|
log_disconnections: "on"
|
||||||
|
# 1GB, unit is KB
|
||||||
|
log_temp_files: "1048576"
|
||||||
|
# Disable dumping customer data to logs, both to increase data privacy
|
||||||
|
# and to reduce the amount the logs.
|
||||||
|
log_error_verbosity: "terse"
|
||||||
|
log_min_error_statement: "panic"
|
||||||
|
max_connections: "100"
|
||||||
|
# --- WAL ---
|
||||||
|
# - flush lag is the max amount of WAL that has been generated but not yet stored
|
||||||
|
# to disk in the page server. A smaller value means less delay after a pageserver
|
||||||
|
# restart, but if you set it too small you might again need to slow down writes if the
|
||||||
|
# pageserver cannot flush incoming WAL to disk fast enough. This must be larger
|
||||||
|
# than the pageserver's checkpoint interval, currently 1 GB! Otherwise you get a
|
||||||
|
# a deadlock where the compute node refuses to generate more WAL before the
|
||||||
|
# old WAL has been uploaded to S3, but the pageserver is waiting for more WAL
|
||||||
|
# to be generated before it is uploaded to S3.
|
||||||
|
max_replication_flush_lag: "10GB"
|
||||||
|
max_replication_slots: "10"
|
||||||
|
# Backpressure configuration:
|
||||||
|
# - write lag is the max amount of WAL that has been generated by Postgres but not yet
|
||||||
|
# processed by the page server. Making this smaller reduces the worst case latency
|
||||||
|
# of a GetPage request, if you request a page that was recently modified. On the other
|
||||||
|
# hand, if this is too small, the compute node might need to wait on a write if there is a
|
||||||
|
# hiccup in the network or page server so that the page server has temporarily fallen
|
||||||
|
# behind.
|
||||||
|
#
|
||||||
|
# Previously it was set to 500 MB, but it caused compute being unresponsive under load
|
||||||
|
# https://github.com/neondatabase/neon/issues/2028
|
||||||
|
max_replication_write_lag: "500MB"
|
||||||
|
max_wal_senders: "10"
|
||||||
|
# A Postgres checkpoint is cheap in storage, as doesn't involve any significant amount
|
||||||
|
# of real I/O. Only the SLRU buffers and some other small files are flushed to disk.
|
||||||
|
# However, as long as we have full_page_writes=on, page updates after a checkpoint
|
||||||
|
# include full-page images which bloats the WAL. So may want to bump max_wal_size to
|
||||||
|
# reduce the WAL bloating, but at the same it will increase pg_wal directory size on
|
||||||
|
# compute and can lead to out of disk error on k8s nodes.
|
||||||
|
max_wal_size: "1024"
|
||||||
|
wal_keep_size: "0"
|
||||||
|
wal_level: "replica"
|
||||||
|
# Reduce amount of WAL generated by default.
|
||||||
|
wal_log_hints: "off"
|
||||||
|
# - without wal_sender_timeout set we don't get feedback messages,
|
||||||
|
# required for backpressure.
|
||||||
|
wal_sender_timeout: "10000"
|
||||||
|
# We have some experimental extensions, which we don't want users to install unconsciously.
|
||||||
|
# To install them, users would need to set the `neon.allow_unstable_extensions` setting.
|
||||||
|
# There are two of them currently:
|
||||||
|
# - `pgrag` - https://github.com/neondatabase-labs/pgrag - extension is actually called just `rag`,
|
||||||
|
# and two dependencies:
|
||||||
|
# - `rag_bge_small_en_v15`
|
||||||
|
# - `rag_jina_reranker_v1_tiny_en`
|
||||||
|
# - `pg_mooncake` - https://github.com/Mooncake-Labs/pg_mooncake/
|
||||||
|
neon.unstable_extensions: "rag,rag_bge_small_en_v15,rag_jina_reranker_v1_tiny_en,pg_mooncake,anon"
|
||||||
|
neon.protocol_version: "3"
|
||||||
|
password_encryption: "scram-sha-256"
|
||||||
|
# This is important to prevent Postgres from trying to perform
|
||||||
|
# a local WAL redo after backend crash. It should exit and let
|
||||||
|
# the systemd or k8s to do a fresh startup with compute_ctl.
|
||||||
|
restart_after_crash: "off"
|
||||||
|
# By default 3. We have the following persistent connections in the VM:
|
||||||
|
# * compute_activity_monitor (from compute_ctl)
|
||||||
|
# * postgres-exporter (metrics collector; it has 2 connections)
|
||||||
|
# * sql_exporter (metrics collector; we have 2 instances [1 for us & users; 1 for autoscaling])
|
||||||
|
# * vm-monitor (to query & change file cache size)
|
||||||
|
# i.e. total of 6. Let's reserve 7, so there's still at least one left over.
|
||||||
|
superuser_reserved_connections: "7"
|
||||||
|
synchronous_standby_names: "walproposer"
|
||||||
|
|
||||||
|
replica:
|
||||||
|
hot_standby: "on"
|
||||||
|
|
||||||
|
per_version:
|
||||||
|
17:
|
||||||
|
common:
|
||||||
|
# PostgreSQL 17 has a new IO system called "read stream", which can combine IOs up to some
|
||||||
|
# size. It still has some issues with readahead, though, so we default to disabled/
|
||||||
|
# "no combining of IOs" to make sure we get the maximum prefetch depth.
|
||||||
|
# See also: https://github.com/neondatabase/neon/pull/9860
|
||||||
|
io_combine_limit: "1"
|
||||||
|
replica:
|
||||||
|
# prefetching of blocks referenced in WAL doesn't make sense for us
|
||||||
|
# Neon hot standby ignores pages that are not in the shared_buffers
|
||||||
|
recovery_prefetch: "off"
|
||||||
|
16:
|
||||||
|
common:
|
||||||
|
replica:
|
||||||
|
# prefetching of blocks referenced in WAL doesn't make sense for us
|
||||||
|
# Neon hot standby ignores pages that are not in the shared_buffers
|
||||||
|
recovery_prefetch: "off"
|
||||||
|
15:
|
||||||
|
common:
|
||||||
|
replica:
|
||||||
|
# prefetching of blocks referenced in WAL doesn't make sense for us
|
||||||
|
# Neon hot standby ignores pages that are not in the shared_buffers
|
||||||
|
recovery_prefetch: "off"
|
||||||
|
14:
|
||||||
|
common:
|
||||||
|
replica:
|
||||||
@@ -40,7 +40,7 @@ use std::sync::mpsc;
|
|||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use compute_api::responses::ComputeConfig;
|
use compute_api::responses::ComputeConfig;
|
||||||
use compute_tools::compute::{
|
use compute_tools::compute::{
|
||||||
@@ -57,14 +57,14 @@ use tracing::{error, info};
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
use utils::failpoint_support;
|
use utils::failpoint_support;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(rename_all = "kebab-case")]
|
#[command(rename_all = "kebab-case")]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[arg(short = 'b', long, default_value = "postgres", env = "POSTGRES_PATH")]
|
#[arg(short = 'b', long, default_value = "postgres", env = "POSTGRES_PATH")]
|
||||||
pub pgbin: String,
|
pub pgbin: String,
|
||||||
|
|
||||||
/// The base URL for the remote extension storage proxy gateway.
|
/// The base URL for the remote extension storage proxy gateway.
|
||||||
#[arg(short = 'r', long)]
|
#[arg(short = 'r', long, value_parser = Self::parse_remote_ext_base_url)]
|
||||||
pub remote_ext_base_url: Option<Url>,
|
pub remote_ext_base_url: Option<Url>,
|
||||||
|
|
||||||
/// The port to bind the external listening HTTP server to. Clients running
|
/// The port to bind the external listening HTTP server to. Clients running
|
||||||
@@ -126,6 +126,25 @@ struct Cli {
|
|||||||
pub installed_extensions_collection_interval: u64,
|
pub installed_extensions_collection_interval: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Cli {
|
||||||
|
/// Parse a URL from an argument. By default, this isn't necessary, but we
|
||||||
|
/// want to do some sanity checking.
|
||||||
|
fn parse_remote_ext_base_url(value: &str) -> Result<Url> {
|
||||||
|
// Remove extra trailing slashes, and add one. We use Url::join() later
|
||||||
|
// when downloading remote extensions. If the base URL is something like
|
||||||
|
// http://example.com/pg-ext-s3-gateway, and join() is called with
|
||||||
|
// something like "xyz", the resulting URL is http://example.com/xyz.
|
||||||
|
let value = value.trim_end_matches('/').to_owned() + "/";
|
||||||
|
let url = Url::parse(&value)?;
|
||||||
|
|
||||||
|
if url.query_pairs().count() != 0 {
|
||||||
|
bail!("parameters detected in remote extensions base URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
@@ -252,7 +271,8 @@ fn handle_exit_signal(sig: i32) {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use clap::CommandFactory;
|
use clap::{CommandFactory, Parser};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use super::Cli;
|
use super::Cli;
|
||||||
|
|
||||||
@@ -260,4 +280,43 @@ mod test {
|
|||||||
fn verify_cli() {
|
fn verify_cli() {
|
||||||
Cli::command().debug_assert()
|
Cli::command().debug_assert()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_remote_ext_base_url() {
|
||||||
|
let cli = Cli::parse_from([
|
||||||
|
"compute_ctl",
|
||||||
|
"--pgdata=test",
|
||||||
|
"--connstr=test",
|
||||||
|
"--compute-id=test",
|
||||||
|
"--remote-ext-base-url",
|
||||||
|
"https://example.com/subpath",
|
||||||
|
]);
|
||||||
|
assert_eq!(
|
||||||
|
cli.remote_ext_base_url.unwrap(),
|
||||||
|
Url::parse("https://example.com/subpath/").unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let cli = Cli::parse_from([
|
||||||
|
"compute_ctl",
|
||||||
|
"--pgdata=test",
|
||||||
|
"--connstr=test",
|
||||||
|
"--compute-id=test",
|
||||||
|
"--remote-ext-base-url",
|
||||||
|
"https://example.com//",
|
||||||
|
]);
|
||||||
|
assert_eq!(
|
||||||
|
cli.remote_ext_base_url.unwrap(),
|
||||||
|
Url::parse("https://example.com").unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
Cli::try_parse_from([
|
||||||
|
"compute_ctl",
|
||||||
|
"--pgdata=test",
|
||||||
|
"--connstr=test",
|
||||||
|
"--compute-id=test",
|
||||||
|
"--remote-ext-base-url",
|
||||||
|
"https://example.com?hello=world",
|
||||||
|
])
|
||||||
|
.expect_err("URL parameters are not allowed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use compute_api::privilege::Privilege;
|
use compute_api::privilege::Privilege;
|
||||||
use compute_api::responses::{
|
use compute_api::responses::{
|
||||||
ComputeConfig, ComputeCtlConfig, ComputeMetrics, ComputeStatus, LfcOffloadState,
|
ComputeConfig, ComputeCtlConfig, ComputeMetrics, ComputeStatus, LfcOffloadState,
|
||||||
LfcPrewarmState,
|
LfcPrewarmState, TlsConfig,
|
||||||
};
|
};
|
||||||
use compute_api::spec::{
|
use compute_api::spec::{
|
||||||
ComputeAudit, ComputeFeature, ComputeMode, ComputeSpec, ExtVersion, PgIdent,
|
ComputeAudit, ComputeFeature, ComputeMode, ComputeSpec, ExtVersion, PgIdent,
|
||||||
@@ -396,7 +396,7 @@ impl ComputeNode {
|
|||||||
// because QEMU will already have its memory allocated from the host, and
|
// because QEMU will already have its memory allocated from the host, and
|
||||||
// the necessary binaries will already be cached.
|
// the necessary binaries will already be cached.
|
||||||
if cli_spec.is_none() {
|
if cli_spec.is_none() {
|
||||||
this.prewarm_postgres()?;
|
this.prewarm_postgres_vm_memory()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the up metric with Empty status before starting the HTTP server.
|
// Set the up metric with Empty status before starting the HTTP server.
|
||||||
@@ -603,6 +603,8 @@ impl ComputeNode {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let tls_config = self.tls_config(&pspec.spec);
|
||||||
|
|
||||||
// If there are any remote extensions in shared_preload_libraries, start downloading them
|
// If there are any remote extensions in shared_preload_libraries, start downloading them
|
||||||
if pspec.spec.remote_extensions.is_some() {
|
if pspec.spec.remote_extensions.is_some() {
|
||||||
let (this, spec) = (self.clone(), pspec.spec.clone());
|
let (this, spec) = (self.clone(), pspec.spec.clone());
|
||||||
@@ -659,7 +661,7 @@ impl ComputeNode {
|
|||||||
info!("tuning pgbouncer");
|
info!("tuning pgbouncer");
|
||||||
|
|
||||||
let pgbouncer_settings = pgbouncer_settings.clone();
|
let pgbouncer_settings = pgbouncer_settings.clone();
|
||||||
let tls_config = self.compute_ctl_config.tls.clone();
|
let tls_config = tls_config.clone();
|
||||||
|
|
||||||
// Spawn a background task to do the tuning,
|
// Spawn a background task to do the tuning,
|
||||||
// so that we don't block the main thread that starts Postgres.
|
// so that we don't block the main thread that starts Postgres.
|
||||||
@@ -678,7 +680,10 @@ impl ComputeNode {
|
|||||||
|
|
||||||
// Spawn a background task to do the configuration,
|
// Spawn a background task to do the configuration,
|
||||||
// so that we don't block the main thread that starts Postgres.
|
// so that we don't block the main thread that starts Postgres.
|
||||||
let local_proxy = local_proxy.clone();
|
|
||||||
|
let mut local_proxy = local_proxy.clone();
|
||||||
|
local_proxy.tls = tls_config.clone();
|
||||||
|
|
||||||
let _handle = tokio::spawn(async move {
|
let _handle = tokio::spawn(async move {
|
||||||
if let Err(err) = local_proxy::configure(&local_proxy) {
|
if let Err(err) = local_proxy::configure(&local_proxy) {
|
||||||
error!("error while configuring local_proxy: {err:?}");
|
error!("error while configuring local_proxy: {err:?}");
|
||||||
@@ -779,7 +784,7 @@ impl ComputeNode {
|
|||||||
// Spawn the extension stats background task
|
// Spawn the extension stats background task
|
||||||
self.spawn_extension_stats_task();
|
self.spawn_extension_stats_task();
|
||||||
|
|
||||||
if pspec.spec.prewarm_lfc_on_startup {
|
if pspec.spec.autoprewarm {
|
||||||
self.prewarm_lfc();
|
self.prewarm_lfc();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1205,13 +1210,15 @@ impl ComputeNode {
|
|||||||
let spec = &pspec.spec;
|
let spec = &pspec.spec;
|
||||||
let pgdata_path = Path::new(&self.params.pgdata);
|
let pgdata_path = Path::new(&self.params.pgdata);
|
||||||
|
|
||||||
|
let tls_config = self.tls_config(&pspec.spec);
|
||||||
|
|
||||||
// Remove/create an empty pgdata directory and put configuration there.
|
// Remove/create an empty pgdata directory and put configuration there.
|
||||||
self.create_pgdata()?;
|
self.create_pgdata()?;
|
||||||
config::write_postgres_conf(
|
config::write_postgres_conf(
|
||||||
pgdata_path,
|
pgdata_path,
|
||||||
&pspec.spec,
|
&pspec.spec,
|
||||||
self.params.internal_http_port,
|
self.params.internal_http_port,
|
||||||
&self.compute_ctl_config.tls,
|
tls_config,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Syncing safekeepers is only safe with primary nodes: if a primary
|
// Syncing safekeepers is only safe with primary nodes: if a primary
|
||||||
@@ -1307,8 +1314,8 @@ impl ComputeNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Start and stop a postgres process to warm up the VM for startup.
|
/// Start and stop a postgres process to warm up the VM for startup.
|
||||||
pub fn prewarm_postgres(&self) -> Result<()> {
|
pub fn prewarm_postgres_vm_memory(&self) -> Result<()> {
|
||||||
info!("prewarming");
|
info!("prewarming VM memory");
|
||||||
|
|
||||||
// Create pgdata
|
// Create pgdata
|
||||||
let pgdata = &format!("{}.warmup", self.params.pgdata);
|
let pgdata = &format!("{}.warmup", self.params.pgdata);
|
||||||
@@ -1350,7 +1357,7 @@ impl ComputeNode {
|
|||||||
kill(pm_pid, Signal::SIGQUIT)?;
|
kill(pm_pid, Signal::SIGQUIT)?;
|
||||||
info!("sent SIGQUIT signal");
|
info!("sent SIGQUIT signal");
|
||||||
pg.wait()?;
|
pg.wait()?;
|
||||||
info!("done prewarming");
|
info!("done prewarming vm memory");
|
||||||
|
|
||||||
// clean up
|
// clean up
|
||||||
let _ok = fs::remove_dir_all(pgdata);
|
let _ok = fs::remove_dir_all(pgdata);
|
||||||
@@ -1536,14 +1543,22 @@ impl ComputeNode {
|
|||||||
.clone(),
|
.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut tls_config = None::<TlsConfig>;
|
||||||
|
if spec.features.contains(&ComputeFeature::TlsExperimental) {
|
||||||
|
tls_config = self.compute_ctl_config.tls.clone();
|
||||||
|
}
|
||||||
|
|
||||||
let max_concurrent_connections = self.max_service_connections(compute_state, &spec);
|
let max_concurrent_connections = self.max_service_connections(compute_state, &spec);
|
||||||
|
|
||||||
// Merge-apply spec & changes to PostgreSQL state.
|
// Merge-apply spec & changes to PostgreSQL state.
|
||||||
self.apply_spec_sql(spec.clone(), conf.clone(), max_concurrent_connections)?;
|
self.apply_spec_sql(spec.clone(), conf.clone(), max_concurrent_connections)?;
|
||||||
|
|
||||||
if let Some(local_proxy) = &spec.clone().local_proxy_config {
|
if let Some(local_proxy) = &spec.clone().local_proxy_config {
|
||||||
|
let mut local_proxy = local_proxy.clone();
|
||||||
|
local_proxy.tls = tls_config.clone();
|
||||||
|
|
||||||
info!("configuring local_proxy");
|
info!("configuring local_proxy");
|
||||||
local_proxy::configure(local_proxy).context("apply_config local_proxy")?;
|
local_proxy::configure(&local_proxy).context("apply_config local_proxy")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run migrations separately to not hold up cold starts
|
// Run migrations separately to not hold up cold starts
|
||||||
@@ -1595,11 +1610,13 @@ impl ComputeNode {
|
|||||||
pub fn reconfigure(&self) -> Result<()> {
|
pub fn reconfigure(&self) -> Result<()> {
|
||||||
let spec = self.state.lock().unwrap().pspec.clone().unwrap().spec;
|
let spec = self.state.lock().unwrap().pspec.clone().unwrap().spec;
|
||||||
|
|
||||||
|
let tls_config = self.tls_config(&spec);
|
||||||
|
|
||||||
if let Some(ref pgbouncer_settings) = spec.pgbouncer_settings {
|
if let Some(ref pgbouncer_settings) = spec.pgbouncer_settings {
|
||||||
info!("tuning pgbouncer");
|
info!("tuning pgbouncer");
|
||||||
|
|
||||||
let pgbouncer_settings = pgbouncer_settings.clone();
|
let pgbouncer_settings = pgbouncer_settings.clone();
|
||||||
let tls_config = self.compute_ctl_config.tls.clone();
|
let tls_config = tls_config.clone();
|
||||||
|
|
||||||
// Spawn a background task to do the tuning,
|
// Spawn a background task to do the tuning,
|
||||||
// so that we don't block the main thread that starts Postgres.
|
// so that we don't block the main thread that starts Postgres.
|
||||||
@@ -1617,7 +1634,7 @@ impl ComputeNode {
|
|||||||
// Spawn a background task to do the configuration,
|
// Spawn a background task to do the configuration,
|
||||||
// so that we don't block the main thread that starts Postgres.
|
// so that we don't block the main thread that starts Postgres.
|
||||||
let mut local_proxy = local_proxy.clone();
|
let mut local_proxy = local_proxy.clone();
|
||||||
local_proxy.tls = self.compute_ctl_config.tls.clone();
|
local_proxy.tls = tls_config.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(err) = local_proxy::configure(&local_proxy) {
|
if let Err(err) = local_proxy::configure(&local_proxy) {
|
||||||
error!("error while configuring local_proxy: {err:?}");
|
error!("error while configuring local_proxy: {err:?}");
|
||||||
@@ -1635,7 +1652,7 @@ impl ComputeNode {
|
|||||||
pgdata_path,
|
pgdata_path,
|
||||||
&spec,
|
&spec,
|
||||||
self.params.internal_http_port,
|
self.params.internal_http_port,
|
||||||
&self.compute_ctl_config.tls,
|
tls_config,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if !spec.skip_pg_catalog_updates {
|
if !spec.skip_pg_catalog_updates {
|
||||||
@@ -1755,6 +1772,14 @@ impl ComputeNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tls_config(&self, spec: &ComputeSpec) -> &Option<TlsConfig> {
|
||||||
|
if spec.features.contains(&ComputeFeature::TlsExperimental) {
|
||||||
|
&self.compute_ctl_config.tls
|
||||||
|
} else {
|
||||||
|
&None::<TlsConfig>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Update the `last_active` in the shared state, but ensure that it's a more recent one.
|
/// Update the `last_active` in the shared state, but ensure that it's a more recent one.
|
||||||
pub fn update_last_active(&self, last_active: Option<DateTime<Utc>>) {
|
pub fn update_last_active(&self, last_active: Option<DateTime<Utc>>) {
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ pub async fn download_extension(
|
|||||||
|
|
||||||
// TODO add retry logic
|
// TODO add retry logic
|
||||||
let download_buffer =
|
let download_buffer =
|
||||||
match download_extension_tar(remote_ext_base_url.as_str(), &ext_path.to_string()).await {
|
match download_extension_tar(remote_ext_base_url, &ext_path.to_string()).await {
|
||||||
Ok(buffer) => buffer,
|
Ok(buffer) => buffer,
|
||||||
Err(error_message) => {
|
Err(error_message) => {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
@@ -271,10 +271,14 @@ pub fn create_control_files(remote_extensions: &RemoteExtSpec, pgbin: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do request to extension storage proxy, e.g.,
|
// Do request to extension storage proxy, e.g.,
|
||||||
// curl http://pg-ext-s3-gateway/latest/v15/extensions/anon.tar.zst
|
// curl http://pg-ext-s3-gateway.pg-ext-s3-gateway.svc.cluster.local/latest/v15/extensions/anon.tar.zst
|
||||||
// using HTTP GET and return the response body as bytes.
|
// using HTTP GET and return the response body as bytes.
|
||||||
async fn download_extension_tar(remote_ext_base_url: &str, ext_path: &str) -> Result<Bytes> {
|
async fn download_extension_tar(remote_ext_base_url: &Url, ext_path: &str) -> Result<Bytes> {
|
||||||
let uri = format!("{}/{}", remote_ext_base_url, ext_path);
|
let uri = remote_ext_base_url.join(ext_path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to create the remote extension URI for {ext_path} using {remote_ext_base_url}"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let filename = Path::new(ext_path)
|
let filename = Path::new(ext_path)
|
||||||
.file_name()
|
.file_name()
|
||||||
.unwrap_or_else(|| std::ffi::OsStr::new("unknown"))
|
.unwrap_or_else(|| std::ffi::OsStr::new("unknown"))
|
||||||
@@ -284,7 +288,7 @@ async fn download_extension_tar(remote_ext_base_url: &str, ext_path: &str) -> Re
|
|||||||
|
|
||||||
info!("Downloading extension file '{}' from uri {}", filename, uri);
|
info!("Downloading extension file '{}' from uri {}", filename, uri);
|
||||||
|
|
||||||
match do_extension_server_request(&uri).await {
|
match do_extension_server_request(uri).await {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
info!("Successfully downloaded remote extension data {}", ext_path);
|
info!("Successfully downloaded remote extension data {}", ext_path);
|
||||||
REMOTE_EXT_REQUESTS_TOTAL
|
REMOTE_EXT_REQUESTS_TOTAL
|
||||||
@@ -303,7 +307,7 @@ async fn download_extension_tar(remote_ext_base_url: &str, ext_path: &str) -> Re
|
|||||||
|
|
||||||
// Do a single remote extensions server request.
|
// Do a single remote extensions server request.
|
||||||
// Return result or (error message + stringified status code) in case of any failures.
|
// Return result or (error message + stringified status code) in case of any failures.
|
||||||
async fn do_extension_server_request(uri: &str) -> Result<Bytes, (String, String)> {
|
async fn do_extension_server_request(uri: Url) -> Result<Bytes, (String, String)> {
|
||||||
let resp = reqwest::get(uri).await.map_err(|e| {
|
let resp = reqwest::get(uri).await.map_err(|e| {
|
||||||
(
|
(
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@@ -48,11 +48,9 @@ impl JsonResponse {
|
|||||||
|
|
||||||
/// Create an error response related to the compute being in an invalid state
|
/// Create an error response related to the compute being in an invalid state
|
||||||
pub(self) fn invalid_status(status: ComputeStatus) -> Response {
|
pub(self) fn invalid_status(status: ComputeStatus) -> Response {
|
||||||
Self::create_response(
|
Self::error(
|
||||||
StatusCode::PRECONDITION_FAILED,
|
StatusCode::PRECONDITION_FAILED,
|
||||||
&GenericAPIError {
|
format!("invalid compute status: {status}"),
|
||||||
error: format!("invalid compute status: {status}"),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ pub(in crate::http) async fn configure(
|
|||||||
State(compute): State<Arc<ComputeNode>>,
|
State(compute): State<Arc<ComputeNode>>,
|
||||||
request: Json<ConfigurationRequest>,
|
request: Json<ConfigurationRequest>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let pspec = match ParsedSpec::try_from(request.spec.clone()) {
|
let pspec = match ParsedSpec::try_from(request.0.spec) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => return JsonResponse::error(StatusCode::BAD_REQUEST, e),
|
Err(e) => return JsonResponse::error(StatusCode::BAD_REQUEST, e),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ use crate::metrics::{PG_CURR_DOWNTIME_MS, PG_TOTAL_DOWNTIME_MS};
|
|||||||
|
|
||||||
const MONITOR_CHECK_INTERVAL: Duration = Duration::from_millis(500);
|
const MONITOR_CHECK_INTERVAL: Duration = Duration::from_millis(500);
|
||||||
|
|
||||||
|
/// Struct to store runtime state of the compute monitor thread.
|
||||||
|
/// In theory, this could be a part of `Compute`, but i)
|
||||||
|
/// this state is expected to be accessed only by single thread,
|
||||||
|
/// so we don't need to care about locking; ii) `Compute` is
|
||||||
|
/// already quite big. Thus, it seems to be a good idea to keep
|
||||||
|
/// all the activity/health monitoring parts here.
|
||||||
struct ComputeMonitor {
|
struct ComputeMonitor {
|
||||||
compute: Arc<ComputeNode>,
|
compute: Arc<ComputeNode>,
|
||||||
|
|
||||||
@@ -70,12 +76,36 @@ impl ComputeMonitor {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if compute is in some terminal or soon-to-be-terminal
|
||||||
|
/// state, then return `true`, signalling the caller that it
|
||||||
|
/// should exit gracefully. Otherwise, return `false`.
|
||||||
|
fn check_interrupts(&mut self) -> bool {
|
||||||
|
let compute_status = self.compute.get_status();
|
||||||
|
if matches!(
|
||||||
|
compute_status,
|
||||||
|
ComputeStatus::Terminated | ComputeStatus::TerminationPending | ComputeStatus::Failed
|
||||||
|
) {
|
||||||
|
info!(
|
||||||
|
"compute is in {} status, stopping compute monitor",
|
||||||
|
compute_status
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Spin in a loop and figure out the last activity time in the Postgres.
|
/// Spin in a loop and figure out the last activity time in the Postgres.
|
||||||
/// Then update it in the shared state. This function never errors out.
|
/// Then update it in the shared state. This function currently never
|
||||||
|
/// errors out explicitly, but there is a graceful termination path.
|
||||||
|
/// Every time we receive an error trying to check Postgres, we use
|
||||||
|
/// [`ComputeMonitor::check_interrupts()`] because it could be that
|
||||||
|
/// compute is being terminated already, then we can exit gracefully
|
||||||
|
/// to not produce errors' noise in the log.
|
||||||
/// NB: the only expected panic is at `Mutex` unwrap(), all other errors
|
/// NB: the only expected panic is at `Mutex` unwrap(), all other errors
|
||||||
/// should be handled gracefully.
|
/// should be handled gracefully.
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub fn run(&mut self) {
|
pub fn run(&mut self) -> anyhow::Result<()> {
|
||||||
// Suppose that `connstr` doesn't change
|
// Suppose that `connstr` doesn't change
|
||||||
let connstr = self.compute.params.connstr.clone();
|
let connstr = self.compute.params.connstr.clone();
|
||||||
let conf = self
|
let conf = self
|
||||||
@@ -93,6 +123,10 @@ impl ComputeMonitor {
|
|||||||
info!("starting compute monitor for {}", connstr);
|
info!("starting compute monitor for {}", connstr);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
if self.check_interrupts() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
match &mut client {
|
match &mut client {
|
||||||
Ok(cli) => {
|
Ok(cli) => {
|
||||||
if cli.is_closed() {
|
if cli.is_closed() {
|
||||||
@@ -100,6 +134,10 @@ impl ComputeMonitor {
|
|||||||
downtime_info = self.downtime_info(),
|
downtime_info = self.downtime_info(),
|
||||||
"connection to Postgres is closed, trying to reconnect"
|
"connection to Postgres is closed, trying to reconnect"
|
||||||
);
|
);
|
||||||
|
if self.check_interrupts() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
self.report_down();
|
self.report_down();
|
||||||
|
|
||||||
// Connection is closed, reconnect and try again.
|
// Connection is closed, reconnect and try again.
|
||||||
@@ -111,15 +149,19 @@ impl ComputeMonitor {
|
|||||||
self.compute.update_last_active(self.last_active);
|
self.compute.update_last_active(self.last_active);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
downtime_info = self.downtime_info(),
|
||||||
|
"could not check Postgres: {}", e
|
||||||
|
);
|
||||||
|
if self.check_interrupts() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Although we have many places where we can return errors in `check()`,
|
// Although we have many places where we can return errors in `check()`,
|
||||||
// normally it shouldn't happen. I.e., we will likely return error if
|
// normally it shouldn't happen. I.e., we will likely return error if
|
||||||
// connection got broken, query timed out, Postgres returned invalid data, etc.
|
// connection got broken, query timed out, Postgres returned invalid data, etc.
|
||||||
// In all such cases it's suspicious, so let's report this as downtime.
|
// In all such cases it's suspicious, so let's report this as downtime.
|
||||||
self.report_down();
|
self.report_down();
|
||||||
error!(
|
|
||||||
downtime_info = self.downtime_info(),
|
|
||||||
"could not check Postgres: {}", e
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reconnect to Postgres just in case. During tests, I noticed
|
// Reconnect to Postgres just in case. During tests, I noticed
|
||||||
// that queries in `check()` can fail with `connection closed`,
|
// that queries in `check()` can fail with `connection closed`,
|
||||||
@@ -136,6 +178,10 @@ impl ComputeMonitor {
|
|||||||
downtime_info = self.downtime_info(),
|
downtime_info = self.downtime_info(),
|
||||||
"could not connect to Postgres: {}, retrying", e
|
"could not connect to Postgres: {}, retrying", e
|
||||||
);
|
);
|
||||||
|
if self.check_interrupts() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
self.report_down();
|
self.report_down();
|
||||||
|
|
||||||
// Establish a new connection and try again.
|
// Establish a new connection and try again.
|
||||||
@@ -147,6 +193,9 @@ impl ComputeMonitor {
|
|||||||
self.last_checked = Utc::now();
|
self.last_checked = Utc::now();
|
||||||
thread::sleep(MONITOR_CHECK_INTERVAL);
|
thread::sleep(MONITOR_CHECK_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Graceful termination path
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
@@ -429,7 +478,10 @@ pub fn launch_monitor(compute: &Arc<ComputeNode>) -> thread::JoinHandle<()> {
|
|||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
let span = span!(Level::INFO, "compute_monitor");
|
let span = span!(Level::INFO, "compute_monitor");
|
||||||
let _enter = span.enter();
|
let _enter = span.enter();
|
||||||
monitor.run();
|
match monitor.run() {
|
||||||
|
Ok(_) => info!("compute monitor thread terminated gracefully"),
|
||||||
|
Err(err) => error!("compute monitor thread terminated abnormally {:?}", err),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.expect("cannot launch compute monitor thread")
|
.expect("cannot launch compute monitor thread")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ mod pg_helpers_tests {
|
|||||||
r#"fsync = off
|
r#"fsync = off
|
||||||
wal_level = logical
|
wal_level = logical
|
||||||
hot_standby = on
|
hot_standby = on
|
||||||
prewarm_lfc_on_startup = off
|
autoprewarm = off
|
||||||
neon.safekeepers = '127.0.0.1:6502,127.0.0.1:6503,127.0.0.1:6501'
|
neon.safekeepers = '127.0.0.1:6502,127.0.0.1:6503,127.0.0.1:6501'
|
||||||
wal_log_hints = on
|
wal_log_hints = on
|
||||||
log_connections = on
|
log_connections = on
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ pageserver_api.workspace = true
|
|||||||
pageserver_client.workspace = true
|
pageserver_client.workspace = true
|
||||||
postgres_backend.workspace = true
|
postgres_backend.workspace = true
|
||||||
safekeeper_api.workspace = true
|
safekeeper_api.workspace = true
|
||||||
|
safekeeper_client.workspace = true
|
||||||
postgres_connection.workspace = true
|
postgres_connection.workspace = true
|
||||||
storage_broker.workspace = true
|
storage_broker.workspace = true
|
||||||
http-utils.workspace = true
|
http-utils.workspace = true
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ use pageserver_api::models::{
|
|||||||
use pageserver_api::shard::{DEFAULT_STRIPE_SIZE, ShardCount, ShardStripeSize, TenantShardId};
|
use pageserver_api::shard::{DEFAULT_STRIPE_SIZE, ShardCount, ShardStripeSize, TenantShardId};
|
||||||
use postgres_backend::AuthType;
|
use postgres_backend::AuthType;
|
||||||
use postgres_connection::parse_host_port;
|
use postgres_connection::parse_host_port;
|
||||||
use safekeeper_api::membership::SafekeeperGeneration;
|
use safekeeper_api::membership::{SafekeeperGeneration, SafekeeperId};
|
||||||
use safekeeper_api::{
|
use safekeeper_api::{
|
||||||
DEFAULT_HTTP_LISTEN_PORT as DEFAULT_SAFEKEEPER_HTTP_PORT,
|
DEFAULT_HTTP_LISTEN_PORT as DEFAULT_SAFEKEEPER_HTTP_PORT,
|
||||||
DEFAULT_PG_LISTEN_PORT as DEFAULT_SAFEKEEPER_PG_PORT,
|
DEFAULT_PG_LISTEN_PORT as DEFAULT_SAFEKEEPER_PG_PORT,
|
||||||
@@ -1255,6 +1255,45 @@ async fn handle_timeline(cmd: &TimelineCmd, env: &mut local_env::LocalEnv) -> Re
|
|||||||
pageserver
|
pageserver
|
||||||
.timeline_import(tenant_id, timeline_id, base, pg_wal, args.pg_version)
|
.timeline_import(tenant_id, timeline_id, base, pg_wal, args.pg_version)
|
||||||
.await?;
|
.await?;
|
||||||
|
if env.storage_controller.timelines_onto_safekeepers {
|
||||||
|
println!("Creating timeline on safekeeper ...");
|
||||||
|
let timeline_info = pageserver
|
||||||
|
.timeline_info(
|
||||||
|
TenantShardId::unsharded(tenant_id),
|
||||||
|
timeline_id,
|
||||||
|
pageserver_client::mgmt_api::ForceAwaitLogicalSize::No,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let default_sk = SafekeeperNode::from_env(env, env.safekeepers.first().unwrap());
|
||||||
|
let default_host = default_sk
|
||||||
|
.conf
|
||||||
|
.listen_addr
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "localhost".to_string());
|
||||||
|
let mconf = safekeeper_api::membership::Configuration {
|
||||||
|
generation: SafekeeperGeneration::new(1),
|
||||||
|
members: safekeeper_api::membership::MemberSet {
|
||||||
|
m: vec![SafekeeperId {
|
||||||
|
host: default_host,
|
||||||
|
id: default_sk.conf.id,
|
||||||
|
pg_port: default_sk.conf.pg_port,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
new_members: None,
|
||||||
|
};
|
||||||
|
let pg_version = args.pg_version * 10000;
|
||||||
|
let req = safekeeper_api::models::TimelineCreateRequest {
|
||||||
|
tenant_id,
|
||||||
|
timeline_id,
|
||||||
|
mconf,
|
||||||
|
pg_version,
|
||||||
|
system_id: None,
|
||||||
|
wal_seg_size: None,
|
||||||
|
start_lsn: timeline_info.last_record_lsn,
|
||||||
|
commit_lsn: None,
|
||||||
|
};
|
||||||
|
default_sk.create_timeline(&req).await?;
|
||||||
|
}
|
||||||
env.register_branch_mapping(branch_name.to_string(), tenant_id, timeline_id)?;
|
env.register_branch_mapping(branch_name.to_string(), tenant_id, timeline_id)?;
|
||||||
println!("Done");
|
println!("Done");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -747,7 +747,7 @@ impl Endpoint {
|
|||||||
logs_export_host: None::<String>,
|
logs_export_host: None::<String>,
|
||||||
endpoint_storage_addr: Some(endpoint_storage_addr),
|
endpoint_storage_addr: Some(endpoint_storage_addr),
|
||||||
endpoint_storage_token: Some(endpoint_storage_token),
|
endpoint_storage_token: Some(endpoint_storage_token),
|
||||||
prewarm_lfc_on_startup: false,
|
autoprewarm: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// this strange code is needed to support respec() in tests
|
// this strange code is needed to support respec() in tests
|
||||||
|
|||||||
@@ -513,11 +513,6 @@ impl PageServerNode {
|
|||||||
.map(|x| x.parse::<bool>())
|
.map(|x| x.parse::<bool>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("Failed to parse 'timeline_offloading' as bool")?,
|
.context("Failed to parse 'timeline_offloading' as bool")?,
|
||||||
wal_receiver_protocol_override: settings
|
|
||||||
.remove("wal_receiver_protocol_override")
|
|
||||||
.map(serde_json::from_str)
|
|
||||||
.transpose()
|
|
||||||
.context("parse `wal_receiver_protocol_override` from json")?,
|
|
||||||
rel_size_v2_enabled: settings
|
rel_size_v2_enabled: settings
|
||||||
.remove("rel_size_v2_enabled")
|
.remove("rel_size_v2_enabled")
|
||||||
.map(|x| x.parse::<bool>())
|
.map(|x| x.parse::<bool>())
|
||||||
@@ -640,4 +635,16 @@ impl PageServerNode {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
pub async fn timeline_info(
|
||||||
|
&self,
|
||||||
|
tenant_shard_id: TenantShardId,
|
||||||
|
timeline_id: TimelineId,
|
||||||
|
force_await_logical_size: mgmt_api::ForceAwaitLogicalSize,
|
||||||
|
) -> anyhow::Result<TimelineInfo> {
|
||||||
|
let timeline_info = self
|
||||||
|
.http_client
|
||||||
|
.timeline_info(tenant_shard_id, timeline_id, force_await_logical_size)
|
||||||
|
.await?;
|
||||||
|
Ok(timeline_info)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
//! .neon/safekeepers/<safekeeper id>
|
//! .neon/safekeepers/<safekeeper id>
|
||||||
//! ```
|
//! ```
|
||||||
use std::error::Error as _;
|
use std::error::Error as _;
|
||||||
use std::future::Future;
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -14,9 +13,9 @@ use std::{io, result};
|
|||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use camino::Utf8PathBuf;
|
use camino::Utf8PathBuf;
|
||||||
use http_utils::error::HttpErrorBody;
|
|
||||||
use postgres_connection::PgConnectionConfig;
|
use postgres_connection::PgConnectionConfig;
|
||||||
use reqwest::{IntoUrl, Method};
|
use safekeeper_api::models::TimelineCreateRequest;
|
||||||
|
use safekeeper_client::mgmt_api;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use utils::auth::{Claims, Scope};
|
use utils::auth::{Claims, Scope};
|
||||||
use utils::id::NodeId;
|
use utils::id::NodeId;
|
||||||
@@ -35,25 +34,14 @@ pub enum SafekeeperHttpError {
|
|||||||
|
|
||||||
type Result<T> = result::Result<T, SafekeeperHttpError>;
|
type Result<T> = result::Result<T, SafekeeperHttpError>;
|
||||||
|
|
||||||
pub(crate) trait ResponseErrorMessageExt: Sized {
|
fn err_from_client_err(err: mgmt_api::Error) -> SafekeeperHttpError {
|
||||||
fn error_from_body(self) -> impl Future<Output = Result<Self>> + Send;
|
use mgmt_api::Error::*;
|
||||||
}
|
match err {
|
||||||
|
ApiError(_, str) => SafekeeperHttpError::Response(str),
|
||||||
impl ResponseErrorMessageExt for reqwest::Response {
|
Cancelled => SafekeeperHttpError::Response("Cancelled".to_owned()),
|
||||||
async fn error_from_body(self) -> Result<Self> {
|
ReceiveBody(err) => SafekeeperHttpError::Transport(err),
|
||||||
let status = self.status();
|
ReceiveErrorBody(err) => SafekeeperHttpError::Response(err),
|
||||||
if !(status.is_client_error() || status.is_server_error()) {
|
Timeout(str) => SafekeeperHttpError::Response(format!("timeout: {str}")),
|
||||||
return Ok(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// reqwest does not export its error construction utility functions, so let's craft the message ourselves
|
|
||||||
let url = self.url().to_owned();
|
|
||||||
Err(SafekeeperHttpError::Response(
|
|
||||||
match self.json::<HttpErrorBody>().await {
|
|
||||||
Ok(err_body) => format!("Error: {}", err_body.msg),
|
|
||||||
Err(_) => format!("Http error ({}) at {}.", status.as_u16(), url),
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,9 +58,8 @@ pub struct SafekeeperNode {
|
|||||||
|
|
||||||
pub pg_connection_config: PgConnectionConfig,
|
pub pg_connection_config: PgConnectionConfig,
|
||||||
pub env: LocalEnv,
|
pub env: LocalEnv,
|
||||||
pub http_client: reqwest::Client,
|
pub http_client: mgmt_api::Client,
|
||||||
pub listen_addr: String,
|
pub listen_addr: String,
|
||||||
pub http_base_url: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SafekeeperNode {
|
impl SafekeeperNode {
|
||||||
@@ -82,13 +69,14 @@ impl SafekeeperNode {
|
|||||||
} else {
|
} else {
|
||||||
"127.0.0.1".to_string()
|
"127.0.0.1".to_string()
|
||||||
};
|
};
|
||||||
|
let jwt = None;
|
||||||
|
let http_base_url = format!("http://{}:{}", listen_addr, conf.http_port);
|
||||||
SafekeeperNode {
|
SafekeeperNode {
|
||||||
id: conf.id,
|
id: conf.id,
|
||||||
conf: conf.clone(),
|
conf: conf.clone(),
|
||||||
pg_connection_config: Self::safekeeper_connection_config(&listen_addr, conf.pg_port),
|
pg_connection_config: Self::safekeeper_connection_config(&listen_addr, conf.pg_port),
|
||||||
env: env.clone(),
|
env: env.clone(),
|
||||||
http_client: env.create_http_client(),
|
http_client: mgmt_api::Client::new(env.create_http_client(), http_base_url, jwt),
|
||||||
http_base_url: format!("http://{}:{}/v1", listen_addr, conf.http_port),
|
|
||||||
listen_addr,
|
listen_addr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,20 +266,19 @@ impl SafekeeperNode {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn http_request<U: IntoUrl>(&self, method: Method, url: U) -> reqwest::RequestBuilder {
|
pub async fn check_status(&self) -> Result<()> {
|
||||||
// TODO: authentication
|
self.http_client
|
||||||
//if self.env.auth_type == AuthType::NeonJWT {
|
.status()
|
||||||
// builder = builder.bearer_auth(&self.env.safekeeper_auth_token)
|
.await
|
||||||
//}
|
.map_err(err_from_client_err)?;
|
||||||
self.http_client.request(method, url)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_status(&self) -> Result<()> {
|
pub async fn create_timeline(&self, req: &TimelineCreateRequest) -> Result<()> {
|
||||||
self.http_request(Method::GET, format!("{}/{}", self.http_base_url, "status"))
|
self.http_client
|
||||||
.send()
|
.create_timeline(req)
|
||||||
.await?
|
.await
|
||||||
.error_from_body()
|
.map_err(err_from_client_err)?;
|
||||||
.await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,10 +61,16 @@ enum Command {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
scheduling: Option<NodeSchedulingPolicy>,
|
scheduling: Option<NodeSchedulingPolicy>,
|
||||||
},
|
},
|
||||||
|
// Set a node status as deleted.
|
||||||
NodeDelete {
|
NodeDelete {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
},
|
},
|
||||||
|
/// Delete a tombstone of node from the storage controller.
|
||||||
|
NodeDeleteTombstone {
|
||||||
|
#[arg(long)]
|
||||||
|
node_id: NodeId,
|
||||||
|
},
|
||||||
/// Modify a tenant's policies in the storage controller
|
/// Modify a tenant's policies in the storage controller
|
||||||
TenantPolicy {
|
TenantPolicy {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -82,6 +88,8 @@ enum Command {
|
|||||||
},
|
},
|
||||||
/// List nodes known to the storage controller
|
/// List nodes known to the storage controller
|
||||||
Nodes {},
|
Nodes {},
|
||||||
|
/// List soft deleted nodes known to the storage controller
|
||||||
|
NodeTombstones {},
|
||||||
/// List tenants known to the storage controller
|
/// List tenants known to the storage controller
|
||||||
Tenants {
|
Tenants {
|
||||||
/// If this field is set, it will list the tenants on a specific node
|
/// If this field is set, it will list the tenants on a specific node
|
||||||
@@ -900,6 +908,39 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.dispatch::<(), ()>(Method::DELETE, format!("control/v1/node/{node_id}"), None)
|
.dispatch::<(), ()>(Method::DELETE, format!("control/v1/node/{node_id}"), None)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
Command::NodeDeleteTombstone { node_id } => {
|
||||||
|
storcon_client
|
||||||
|
.dispatch::<(), ()>(
|
||||||
|
Method::DELETE,
|
||||||
|
format!("debug/v1/tombstone/{node_id}"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Command::NodeTombstones {} => {
|
||||||
|
let mut resp = storcon_client
|
||||||
|
.dispatch::<(), Vec<NodeDescribeResponse>>(
|
||||||
|
Method::GET,
|
||||||
|
"debug/v1/tombstone".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
resp.sort_by(|a, b| a.listen_http_addr.cmp(&b.listen_http_addr));
|
||||||
|
|
||||||
|
let mut table = comfy_table::Table::new();
|
||||||
|
table.set_header(["Id", "Hostname", "AZ", "Scheduling", "Availability"]);
|
||||||
|
for node in resp {
|
||||||
|
table.add_row([
|
||||||
|
format!("{}", node.id),
|
||||||
|
node.listen_http_addr,
|
||||||
|
node.availability_zone_id,
|
||||||
|
format!("{:?}", node.scheduling),
|
||||||
|
format!("{:?}", node.availability),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
println!("{table}");
|
||||||
|
}
|
||||||
Command::TenantSetTimeBasedEviction {
|
Command::TenantSetTimeBasedEviction {
|
||||||
tenant_id,
|
tenant_id,
|
||||||
period,
|
period,
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ RUN echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries && \
|
|||||||
jq \
|
jq \
|
||||||
netcat-openbsd
|
netcat-openbsd
|
||||||
#This is required for the pg_hintplan test
|
#This is required for the pg_hintplan test
|
||||||
RUN mkdir -p /ext-src/pg_hint_plan-src /postgres/contrib/file_fdw && chown postgres /ext-src/pg_hint_plan-src /postgres/contrib/file_fdw
|
RUN mkdir -p /ext-src/pg_hint_plan-src /postgres/contrib/file_fdw /ext-src/postgis-src/ && chown postgres /ext-src/pg_hint_plan-src /postgres/contrib/file_fdw /ext-src/postgis-src
|
||||||
|
|
||||||
USER postgres
|
USER postgres
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
set -eux
|
set -eux
|
||||||
|
|
||||||
# Generate a random tenant or timeline ID
|
# Generate a random tenant or timeline ID
|
||||||
#
|
#
|
||||||
# Takes a variable name as argument. The result is stored in that variable.
|
# Takes a variable name as argument. The result is stored in that variable.
|
||||||
generate_id() {
|
generate_id() {
|
||||||
local -n resvar=$1
|
local -n resvar=${1}
|
||||||
printf -v resvar '%08x%08x%08x%08x' $SRANDOM $SRANDOM $SRANDOM $SRANDOM
|
printf -v resvar '%08x%08x%08x%08x' ${SRANDOM} ${SRANDOM} ${SRANDOM} ${SRANDOM}
|
||||||
}
|
}
|
||||||
|
|
||||||
PG_VERSION=${PG_VERSION:-14}
|
PG_VERSION=${PG_VERSION:-14}
|
||||||
|
|
||||||
CONFIG_FILE_ORG=/var/db/postgres/configs/config.json
|
readonly CONFIG_FILE_ORG=/var/db/postgres/configs/config.json
|
||||||
CONFIG_FILE=/tmp/config.json
|
readonly CONFIG_FILE=/tmp/config.json
|
||||||
|
|
||||||
# Test that the first library path that the dynamic loader looks in is the path
|
# Test that the first library path that the dynamic loader looks in is the path
|
||||||
# that we use for custom compiled software
|
# that we use for custom compiled software
|
||||||
@@ -20,17 +20,17 @@ first_path="$(ldconfig --verbose 2>/dev/null \
|
|||||||
| grep --invert-match ^$'\t' \
|
| grep --invert-match ^$'\t' \
|
||||||
| cut --delimiter=: --fields=1 \
|
| cut --delimiter=: --fields=1 \
|
||||||
| head --lines=1)"
|
| head --lines=1)"
|
||||||
test "$first_path" == '/usr/local/lib'
|
test "${first_path}" = '/usr/local/lib'
|
||||||
|
|
||||||
echo "Waiting pageserver become ready."
|
echo "Waiting pageserver become ready."
|
||||||
while ! nc -z pageserver 6400; do
|
while ! nc -z pageserver 6400; do
|
||||||
sleep 1;
|
sleep 1
|
||||||
done
|
done
|
||||||
echo "Page server is ready."
|
echo "Page server is ready."
|
||||||
|
|
||||||
cp ${CONFIG_FILE_ORG} ${CONFIG_FILE}
|
cp "${CONFIG_FILE_ORG}" "${CONFIG_FILE}"
|
||||||
|
|
||||||
if [ -n "${TENANT_ID:-}" ] && [ -n "${TIMELINE_ID:-}" ]; then
|
if [[ -n "${TENANT_ID:-}" && -n "${TIMELINE_ID:-}" ]]; then
|
||||||
tenant_id=${TENANT_ID}
|
tenant_id=${TENANT_ID}
|
||||||
timeline_id=${TIMELINE_ID}
|
timeline_id=${TIMELINE_ID}
|
||||||
else
|
else
|
||||||
@@ -41,7 +41,7 @@ else
|
|||||||
"http://pageserver:9898/v1/tenant"
|
"http://pageserver:9898/v1/tenant"
|
||||||
)
|
)
|
||||||
tenant_id=$(curl "${PARAMS[@]}" | jq -r .[0].id)
|
tenant_id=$(curl "${PARAMS[@]}" | jq -r .[0].id)
|
||||||
if [ -z "${tenant_id}" ] || [ "${tenant_id}" = null ]; then
|
if [[ -z "${tenant_id}" || "${tenant_id}" = null ]]; then
|
||||||
echo "Create a tenant"
|
echo "Create a tenant"
|
||||||
generate_id tenant_id
|
generate_id tenant_id
|
||||||
PARAMS=(
|
PARAMS=(
|
||||||
@@ -51,7 +51,7 @@ else
|
|||||||
"http://pageserver:9898/v1/tenant/${tenant_id}/location_config"
|
"http://pageserver:9898/v1/tenant/${tenant_id}/location_config"
|
||||||
)
|
)
|
||||||
result=$(curl "${PARAMS[@]}")
|
result=$(curl "${PARAMS[@]}")
|
||||||
echo $result | jq .
|
printf '%s\n' "${result}" | jq .
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Check if a timeline present"
|
echo "Check if a timeline present"
|
||||||
@@ -61,7 +61,7 @@ else
|
|||||||
"http://pageserver:9898/v1/tenant/${tenant_id}/timeline"
|
"http://pageserver:9898/v1/tenant/${tenant_id}/timeline"
|
||||||
)
|
)
|
||||||
timeline_id=$(curl "${PARAMS[@]}" | jq -r .[0].timeline_id)
|
timeline_id=$(curl "${PARAMS[@]}" | jq -r .[0].timeline_id)
|
||||||
if [ -z "${timeline_id}" ] || [ "${timeline_id}" = null ]; then
|
if [[ -z "${timeline_id}" || "${timeline_id}" = null ]]; then
|
||||||
generate_id timeline_id
|
generate_id timeline_id
|
||||||
PARAMS=(
|
PARAMS=(
|
||||||
-sbf
|
-sbf
|
||||||
@@ -71,7 +71,7 @@ else
|
|||||||
"http://pageserver:9898/v1/tenant/${tenant_id}/timeline/"
|
"http://pageserver:9898/v1/tenant/${tenant_id}/timeline/"
|
||||||
)
|
)
|
||||||
result=$(curl "${PARAMS[@]}")
|
result=$(curl "${PARAMS[@]}")
|
||||||
echo $result | jq .
|
printf '%s\n' "${result}" | jq .
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -82,10 +82,10 @@ else
|
|||||||
fi
|
fi
|
||||||
echo "Adding pgx_ulid"
|
echo "Adding pgx_ulid"
|
||||||
shared_libraries=$(jq -r '.spec.cluster.settings[] | select(.name=="shared_preload_libraries").value' ${CONFIG_FILE})
|
shared_libraries=$(jq -r '.spec.cluster.settings[] | select(.name=="shared_preload_libraries").value' ${CONFIG_FILE})
|
||||||
sed -i "s/${shared_libraries}/${shared_libraries},${ulid_extension}/" ${CONFIG_FILE}
|
sed -i "s|${shared_libraries}|${shared_libraries},${ulid_extension}|" ${CONFIG_FILE}
|
||||||
echo "Overwrite tenant id and timeline id in spec file"
|
echo "Overwrite tenant id and timeline id in spec file"
|
||||||
sed -i "s/TENANT_ID/${tenant_id}/" ${CONFIG_FILE}
|
sed -i "s|TENANT_ID|${tenant_id}|" ${CONFIG_FILE}
|
||||||
sed -i "s/TIMELINE_ID/${timeline_id}/" ${CONFIG_FILE}
|
sed -i "s|TIMELINE_ID|${timeline_id}|" ${CONFIG_FILE}
|
||||||
|
|
||||||
cat ${CONFIG_FILE}
|
cat ${CONFIG_FILE}
|
||||||
|
|
||||||
@@ -93,5 +93,5 @@ echo "Start compute node"
|
|||||||
/usr/local/bin/compute_ctl --pgdata /var/db/postgres/compute \
|
/usr/local/bin/compute_ctl --pgdata /var/db/postgres/compute \
|
||||||
-C "postgresql://cloud_admin@localhost:55433/postgres" \
|
-C "postgresql://cloud_admin@localhost:55433/postgres" \
|
||||||
-b /usr/local/bin/postgres \
|
-b /usr/local/bin/postgres \
|
||||||
--compute-id "compute-$RANDOM" \
|
--compute-id "compute-${RANDOM}" \
|
||||||
--config "$CONFIG_FILE"
|
--config "${CONFIG_FILE}"
|
||||||
|
|||||||
@@ -186,13 +186,14 @@ services:
|
|||||||
|
|
||||||
neon-test-extensions:
|
neon-test-extensions:
|
||||||
profiles: ["test-extensions"]
|
profiles: ["test-extensions"]
|
||||||
image: ${REPOSITORY:-ghcr.io/neondatabase}/neon-test-extensions-v${PG_TEST_VERSION:-16}:${TEST_EXTENSIONS_TAG:-${TAG:-latest}}
|
image: ${REPOSITORY:-ghcr.io/neondatabase}/neon-test-extensions-v${PG_TEST_VERSION:-${PG_VERSION:-16}}:${TEST_EXTENSIONS_TAG:-${TAG:-latest}}
|
||||||
environment:
|
environment:
|
||||||
- PGPASSWORD=cloud_admin
|
- PGUSER=${PGUSER:-cloud_admin}
|
||||||
|
- PGPASSWORD=${PGPASSWORD:-cloud_admin}
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- "/bin/bash"
|
- "/bin/bash"
|
||||||
- "-c"
|
- "-c"
|
||||||
command:
|
command:
|
||||||
- sleep 1800
|
- sleep 3600
|
||||||
depends_on:
|
depends_on:
|
||||||
- compute
|
- compute
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ for pg_version in ${TEST_VERSION_ONLY-14 15 16 17}; do
|
|||||||
# It cannot be moved to Dockerfile now because the database directory is created after the start of the container
|
# It cannot be moved to Dockerfile now because the database directory is created after the start of the container
|
||||||
echo Adding dummy config
|
echo Adding dummy config
|
||||||
docker compose exec compute touch /var/db/postgres/compute/compute_ctl_temp_override.conf
|
docker compose exec compute touch /var/db/postgres/compute/compute_ctl_temp_override.conf
|
||||||
|
# Prepare for the PostGIS test
|
||||||
|
docker compose exec compute mkdir -p /tmp/pgis_reg/pgis_reg_tmp
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
docker compose cp neon-test-extensions:/ext-src/postgis-src/raster/test "${TMPDIR}"
|
||||||
|
docker compose cp neon-test-extensions:/ext-src/postgis-src/regress/00-regress-install "${TMPDIR}"
|
||||||
|
docker compose exec compute mkdir -p /ext-src/postgis-src/raster /ext-src/postgis-src/regress /ext-src/postgis-src/regress/00-regress-install
|
||||||
|
docker compose cp "${TMPDIR}/test" compute:/ext-src/postgis-src/raster/test
|
||||||
|
docker compose cp "${TMPDIR}/00-regress-install" compute:/ext-src/postgis-src/regress
|
||||||
|
rm -rf "${TMPDIR}"
|
||||||
# The following block copies the files for the pg_hintplan test to the compute node for the extension test in an isolated docker-compose environment
|
# The following block copies the files for the pg_hintplan test to the compute node for the extension test in an isolated docker-compose environment
|
||||||
TMPDIR=$(mktemp -d)
|
TMPDIR=$(mktemp -d)
|
||||||
docker compose cp neon-test-extensions:/ext-src/pg_hint_plan-src/data "${TMPDIR}/data"
|
docker compose cp neon-test-extensions:/ext-src/pg_hint_plan-src/data "${TMPDIR}/data"
|
||||||
@@ -68,7 +77,7 @@ for pg_version in ${TEST_VERSION_ONLY-14 15 16 17}; do
|
|||||||
docker compose exec -T neon-test-extensions bash -c "(cd /postgres && patch -p1)" <"../compute/patches/contrib_pg${pg_version}.patch"
|
docker compose exec -T neon-test-extensions bash -c "(cd /postgres && patch -p1)" <"../compute/patches/contrib_pg${pg_version}.patch"
|
||||||
# We are running tests now
|
# We are running tests now
|
||||||
rm -f testout.txt testout_contrib.txt
|
rm -f testout.txt testout_contrib.txt
|
||||||
docker compose exec -e USE_PGXS=1 -e SKIP=timescaledb-src,rdkit-src,postgis-src,pg_jsonschema-src,kq_imcx-src,wal2json_2_5-src,rag_jina_reranker_v1_tiny_en-src,rag_bge_small_en_v15-src \
|
docker compose exec -e USE_PGXS=1 -e SKIP=timescaledb-src,rdkit-src,pg_jsonschema-src,kq_imcx-src,wal2json_2_5-src,rag_jina_reranker_v1_tiny_en-src,rag_bge_small_en_v15-src \
|
||||||
neon-test-extensions /run-tests.sh /ext-src | tee testout.txt && EXT_SUCCESS=1 || EXT_SUCCESS=0
|
neon-test-extensions /run-tests.sh /ext-src | tee testout.txt && EXT_SUCCESS=1 || EXT_SUCCESS=0
|
||||||
docker compose exec -e SKIP=start-scripts,postgres_fdw,ltree_plpython,jsonb_plpython,jsonb_plperl,hstore_plpython,hstore_plperl,dblink,bool_plperl \
|
docker compose exec -e SKIP=start-scripts,postgres_fdw,ltree_plpython,jsonb_plpython,jsonb_plperl,hstore_plpython,hstore_plperl,dblink,bool_plperl \
|
||||||
neon-test-extensions /run-tests.sh /postgres/contrib | tee testout_contrib.txt && CONTRIB_SUCCESS=1 || CONTRIB_SUCCESS=0
|
neon-test-extensions /run-tests.sh /postgres/contrib | tee testout_contrib.txt && CONTRIB_SUCCESS=1 || CONTRIB_SUCCESS=0
|
||||||
|
|||||||
70
docker-compose/ext-src/postgis-src/README-Neon.md
Normal file
70
docker-compose/ext-src/postgis-src/README-Neon.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# PostGIS Testing in Neon
|
||||||
|
|
||||||
|
This directory contains configuration files and patches for running PostGIS tests in the Neon database environment.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
PostGIS is a spatial database extension for PostgreSQL that adds support for geographic objects. Testing PostGIS compatibility ensures that Neon's modifications to PostgreSQL don't break compatibility with this critical extension.
|
||||||
|
|
||||||
|
## PostGIS Versions
|
||||||
|
|
||||||
|
- PostgreSQL v17: PostGIS 3.5.0
|
||||||
|
- PostgreSQL v14/v15/v16: PostGIS 3.3.3
|
||||||
|
|
||||||
|
## Test Configuration
|
||||||
|
|
||||||
|
The test setup includes:
|
||||||
|
|
||||||
|
- `postgis-no-upgrade-test.patch`: Disables upgrade tests by removing the upgrade test section from regress/runtest.mk
|
||||||
|
- `postgis-regular-v16.patch`: Version-specific patch for PostgreSQL v16
|
||||||
|
- `postgis-regular-v17.patch`: Version-specific patch for PostgreSQL v17
|
||||||
|
- `regular-test.sh`: Script to run PostGIS tests as a regular user
|
||||||
|
- `neon-test.sh`: Script to handle version-specific test configurations
|
||||||
|
- `raster_outdb_template.sql`: Template for raster tests with explicit file paths
|
||||||
|
|
||||||
|
## Excluded Tests
|
||||||
|
|
||||||
|
**Important Note:** The test exclusions listed below are specifically for regular-user tests against staging instances. These exclusions are necessary because staging instances run with limited privileges and cannot perform operations requiring superuser access. Docker-compose based tests are not affected by these exclusions.
|
||||||
|
|
||||||
|
### Tests Requiring Superuser Permissions
|
||||||
|
|
||||||
|
These tests cannot be run as a regular user:
|
||||||
|
- `estimatedextent`
|
||||||
|
- `regress/core/legacy`
|
||||||
|
- `regress/core/typmod`
|
||||||
|
- `regress/loader/TestSkipANALYZE`
|
||||||
|
- `regress/loader/TestANALYZE`
|
||||||
|
|
||||||
|
### Tests Requiring Filesystem Access
|
||||||
|
|
||||||
|
These tests need direct filesystem access that is only possible for superusers:
|
||||||
|
- `loader/load_outdb`
|
||||||
|
|
||||||
|
### Tests with Flaky Results
|
||||||
|
|
||||||
|
These tests have assumptions that don't always hold true:
|
||||||
|
- `regress/core/computed_columns` - Assumes computed columns always outperform alternatives, which is not consistently true
|
||||||
|
|
||||||
|
### Tests Requiring Tunable Parameter Modifications
|
||||||
|
|
||||||
|
These tests attempt to modify the `postgis.gdal_enabled_drivers` parameter, which is only accessible to superusers:
|
||||||
|
- `raster/test/regress/rt_wkb`
|
||||||
|
- `raster/test/regress/rt_addband`
|
||||||
|
- `raster/test/regress/rt_setbandpath`
|
||||||
|
- `raster/test/regress/rt_fromgdalraster`
|
||||||
|
- `raster/test/regress/rt_asgdalraster`
|
||||||
|
- `raster/test/regress/rt_astiff`
|
||||||
|
- `raster/test/regress/rt_asjpeg`
|
||||||
|
- `raster/test/regress/rt_aspng`
|
||||||
|
- `raster/test/regress/permitted_gdal_drivers`
|
||||||
|
- Loader tests: `BasicOutDB`, `Tiled10x10`, `Tiled10x10Copy`, `Tiled8x8`, `TiledAuto`, `TiledAutoSkipNoData`, `TiledAutoCopyn`
|
||||||
|
|
||||||
|
### Topology Tests (v17 only)
|
||||||
|
- `populate_topology_layer`
|
||||||
|
- `renametopogeometrycolumn`
|
||||||
|
|
||||||
|
## Other Modifications
|
||||||
|
|
||||||
|
- Binary.sql tests are modified to use explicit file paths
|
||||||
|
- Server-side SQL COPY commands (which require superuser privileges) are converted to client-side `\copy` commands
|
||||||
|
- Upgrade tests are disabled
|
||||||
6
docker-compose/ext-src/postgis-src/neon-test.sh
Executable file
6
docker-compose/ext-src/postgis-src/neon-test.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -ex
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
patch -p1 <"postgis-common-${PG_VERSION}.patch"
|
||||||
|
trap 'echo Cleaning up; patch -R -p1 <postgis-common-${PG_VERSION}.patch' EXIT
|
||||||
|
make installcheck-base
|
||||||
37
docker-compose/ext-src/postgis-src/postgis-common-v16.patch
Normal file
37
docker-compose/ext-src/postgis-src/postgis-common-v16.patch
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
diff --git a/regress/core/tests.mk b/regress/core/tests.mk
|
||||||
|
index 3abd7bc..64a9254 100644
|
||||||
|
--- a/regress/core/tests.mk
|
||||||
|
+++ b/regress/core/tests.mk
|
||||||
|
@@ -144,11 +144,6 @@ TESTS_SLOW = \
|
||||||
|
$(top_srcdir)/regress/core/concave_hull_hard \
|
||||||
|
$(top_srcdir)/regress/core/knn_recheck
|
||||||
|
|
||||||
|
-ifeq ($(shell expr "$(POSTGIS_PGSQL_VERSION)" ">=" 120),1)
|
||||||
|
- TESTS += \
|
||||||
|
- $(top_srcdir)/regress/core/computed_columns
|
||||||
|
-endif
|
||||||
|
-
|
||||||
|
ifeq ($(shell expr "$(POSTGIS_GEOS_VERSION)" ">=" 30700),1)
|
||||||
|
# GEOS-3.7 adds:
|
||||||
|
# ST_FrechetDistance
|
||||||
|
diff --git a/regress/runtest.mk b/regress/runtest.mk
|
||||||
|
index c051f03..010e493 100644
|
||||||
|
--- a/regress/runtest.mk
|
||||||
|
+++ b/regress/runtest.mk
|
||||||
|
@@ -24,16 +24,6 @@ check-regress:
|
||||||
|
|
||||||
|
POSTGIS_TOP_BUILD_DIR=$(abs_top_builddir) $(PERL) $(top_srcdir)/regress/run_test.pl $(RUNTESTFLAGS) $(RUNTESTFLAGS_INTERNAL) $(TESTS)
|
||||||
|
|
||||||
|
- @if echo "$(RUNTESTFLAGS)" | grep -vq -- --upgrade; then \
|
||||||
|
- echo "Running upgrade test as RUNTESTFLAGS did not contain that"; \
|
||||||
|
- POSTGIS_TOP_BUILD_DIR=$(abs_top_builddir) $(PERL) $(top_srcdir)/regress/run_test.pl \
|
||||||
|
- --upgrade \
|
||||||
|
- $(RUNTESTFLAGS) \
|
||||||
|
- $(RUNTESTFLAGS_INTERNAL) \
|
||||||
|
- $(TESTS); \
|
||||||
|
- else \
|
||||||
|
- echo "Skipping upgrade test as RUNTESTFLAGS already requested upgrades"; \
|
||||||
|
- fi
|
||||||
|
|
||||||
|
check-long:
|
||||||
|
$(PERL) $(top_srcdir)/regress/run_test.pl $(RUNTESTFLAGS) $(TESTS) $(TESTS_SLOW)
|
||||||
35
docker-compose/ext-src/postgis-src/postgis-common-v17.patch
Normal file
35
docker-compose/ext-src/postgis-src/postgis-common-v17.patch
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
diff --git a/regress/core/tests.mk b/regress/core/tests.mk
|
||||||
|
index 9e05244..90987df 100644
|
||||||
|
--- a/regress/core/tests.mk
|
||||||
|
+++ b/regress/core/tests.mk
|
||||||
|
@@ -143,8 +143,7 @@ TESTS += \
|
||||||
|
$(top_srcdir)/regress/core/oriented_envelope \
|
||||||
|
$(top_srcdir)/regress/core/point_coordinates \
|
||||||
|
$(top_srcdir)/regress/core/out_geojson \
|
||||||
|
- $(top_srcdir)/regress/core/wrapx \
|
||||||
|
- $(top_srcdir)/regress/core/computed_columns
|
||||||
|
+ $(top_srcdir)/regress/core/wrapx
|
||||||
|
|
||||||
|
# Slow slow tests
|
||||||
|
TESTS_SLOW = \
|
||||||
|
diff --git a/regress/runtest.mk b/regress/runtest.mk
|
||||||
|
index 4b95b7e..449d5a2 100644
|
||||||
|
--- a/regress/runtest.mk
|
||||||
|
+++ b/regress/runtest.mk
|
||||||
|
@@ -24,16 +24,6 @@ check-regress:
|
||||||
|
|
||||||
|
@POSTGIS_TOP_BUILD_DIR=$(abs_top_builddir) $(PERL) $(top_srcdir)/regress/run_test.pl $(RUNTESTFLAGS) $(RUNTESTFLAGS_INTERNAL) $(TESTS)
|
||||||
|
|
||||||
|
- @if echo "$(RUNTESTFLAGS)" | grep -vq -- --upgrade; then \
|
||||||
|
- echo "Running upgrade test as RUNTESTFLAGS did not contain that"; \
|
||||||
|
- POSTGIS_TOP_BUILD_DIR=$(abs_top_builddir) $(PERL) $(top_srcdir)/regress/run_test.pl \
|
||||||
|
- --upgrade \
|
||||||
|
- $(RUNTESTFLAGS) \
|
||||||
|
- $(RUNTESTFLAGS_INTERNAL) \
|
||||||
|
- $(TESTS); \
|
||||||
|
- else \
|
||||||
|
- echo "Skipping upgrade test as RUNTESTFLAGS already requested upgrades"; \
|
||||||
|
- fi
|
||||||
|
|
||||||
|
check-long:
|
||||||
|
$(PERL) $(top_srcdir)/regress/run_test.pl $(RUNTESTFLAGS) $(TESTS) $(TESTS_SLOW)
|
||||||
186
docker-compose/ext-src/postgis-src/postgis-regular-v16.patch
Normal file
186
docker-compose/ext-src/postgis-src/postgis-regular-v16.patch
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
diff --git a/raster/test/regress/tests.mk b/raster/test/regress/tests.mk
|
||||||
|
index 00918e1..7e2b6cd 100644
|
||||||
|
--- a/raster/test/regress/tests.mk
|
||||||
|
+++ b/raster/test/regress/tests.mk
|
||||||
|
@@ -17,9 +17,7 @@ override RUNTESTFLAGS_INTERNAL := \
|
||||||
|
$(RUNTESTFLAGS_INTERNAL) \
|
||||||
|
--after-upgrade-script $(top_srcdir)/raster/test/regress/hooks/hook-after-upgrade-raster.sql
|
||||||
|
|
||||||
|
-RASTER_TEST_FIRST = \
|
||||||
|
- $(top_srcdir)/raster/test/regress/check_gdal \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/load_outdb
|
||||||
|
+RASTER_TEST_FIRST =
|
||||||
|
|
||||||
|
RASTER_TEST_LAST = \
|
||||||
|
$(top_srcdir)/raster/test/regress/clean
|
||||||
|
@@ -33,9 +31,7 @@ RASTER_TEST_IO = \
|
||||||
|
|
||||||
|
RASTER_TEST_BASIC_FUNC = \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_bytea \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_wkb \
|
||||||
|
$(top_srcdir)/raster/test/regress/box3d \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_addband \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_band \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_tile
|
||||||
|
|
||||||
|
@@ -73,16 +69,10 @@ RASTER_TEST_BANDPROPS = \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_neighborhood \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_nearestvalue \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_pixelofvalue \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_polygon \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_setbandpath
|
||||||
|
+ $(top_srcdir)/raster/test/regress/rt_polygon
|
||||||
|
|
||||||
|
RASTER_TEST_UTILITY = \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_utility \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_fromgdalraster \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_asgdalraster \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_astiff \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_asjpeg \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_aspng \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_reclass \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_gdalwarp \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_gdalcontour \
|
||||||
|
@@ -120,21 +110,13 @@ RASTER_TEST_SREL = \
|
||||||
|
|
||||||
|
RASTER_TEST_BUGS = \
|
||||||
|
$(top_srcdir)/raster/test/regress/bug_test_car5 \
|
||||||
|
- $(top_srcdir)/raster/test/regress/permitted_gdal_drivers \
|
||||||
|
$(top_srcdir)/raster/test/regress/tickets
|
||||||
|
|
||||||
|
RASTER_TEST_LOADER = \
|
||||||
|
$(top_srcdir)/raster/test/regress/loader/Basic \
|
||||||
|
$(top_srcdir)/raster/test/regress/loader/Projected \
|
||||||
|
$(top_srcdir)/raster/test/regress/loader/BasicCopy \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/BasicFilename \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/BasicOutDB \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/Tiled10x10 \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/Tiled10x10Copy \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/Tiled8x8 \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/TiledAuto \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/TiledAutoSkipNoData \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/TiledAutoCopyn
|
||||||
|
+ $(top_srcdir)/raster/test/regress/loader/BasicFilename
|
||||||
|
|
||||||
|
RASTER_TESTS := $(RASTER_TEST_FIRST) \
|
||||||
|
$(RASTER_TEST_METADATA) $(RASTER_TEST_IO) $(RASTER_TEST_BASIC_FUNC) \
|
||||||
|
diff --git a/regress/core/binary.sql b/regress/core/binary.sql
|
||||||
|
index 7a36b65..ad78fc7 100644
|
||||||
|
--- a/regress/core/binary.sql
|
||||||
|
+++ b/regress/core/binary.sql
|
||||||
|
@@ -1,4 +1,5 @@
|
||||||
|
SET client_min_messages TO warning;
|
||||||
|
+
|
||||||
|
CREATE SCHEMA tm;
|
||||||
|
|
||||||
|
CREATE TABLE tm.geoms (id serial, g geometry);
|
||||||
|
@@ -31,24 +32,39 @@ SELECT st_force4d(g) FROM tm.geoms WHERE id < 15 ORDER BY id;
|
||||||
|
INSERT INTO tm.geoms(g)
|
||||||
|
SELECT st_setsrid(g,4326) FROM tm.geoms ORDER BY id;
|
||||||
|
|
||||||
|
-COPY tm.geoms TO :tmpfile WITH BINARY;
|
||||||
|
+-- define temp file path
|
||||||
|
+\set tmpfile '/tmp/postgis_binary_test.dat'
|
||||||
|
+
|
||||||
|
+-- export
|
||||||
|
+\set command '\\copy tm.geoms TO ':tmpfile' WITH (FORMAT BINARY)'
|
||||||
|
+:command
|
||||||
|
+
|
||||||
|
+-- import
|
||||||
|
CREATE TABLE tm.geoms_in AS SELECT * FROM tm.geoms LIMIT 0;
|
||||||
|
-COPY tm.geoms_in FROM :tmpfile WITH BINARY;
|
||||||
|
-SELECT 'geometry', count(*) FROM tm.geoms_in i, tm.geoms o WHERE i.id = o.id
|
||||||
|
- AND ST_OrderingEquals(i.g, o.g);
|
||||||
|
+\set command '\\copy tm.geoms_in FROM ':tmpfile' WITH (FORMAT BINARY)'
|
||||||
|
+:command
|
||||||
|
+
|
||||||
|
+SELECT 'geometry', count(*) FROM tm.geoms_in i, tm.geoms o
|
||||||
|
+WHERE i.id = o.id AND ST_OrderingEquals(i.g, o.g);
|
||||||
|
|
||||||
|
CREATE TABLE tm.geogs AS SELECT id,g::geography FROM tm.geoms
|
||||||
|
WHERE geometrytype(g) NOT LIKE '%CURVE%'
|
||||||
|
AND geometrytype(g) NOT LIKE '%CIRCULAR%'
|
||||||
|
AND geometrytype(g) NOT LIKE '%SURFACE%'
|
||||||
|
AND geometrytype(g) NOT LIKE 'TRIANGLE%'
|
||||||
|
- AND geometrytype(g) NOT LIKE 'TIN%'
|
||||||
|
-;
|
||||||
|
+ AND geometrytype(g) NOT LIKE 'TIN%';
|
||||||
|
|
||||||
|
-COPY tm.geogs TO :tmpfile WITH BINARY;
|
||||||
|
+-- export
|
||||||
|
+\set command '\\copy tm.geogs TO ':tmpfile' WITH (FORMAT BINARY)'
|
||||||
|
+:command
|
||||||
|
+
|
||||||
|
+-- import
|
||||||
|
CREATE TABLE tm.geogs_in AS SELECT * FROM tm.geogs LIMIT 0;
|
||||||
|
-COPY tm.geogs_in FROM :tmpfile WITH BINARY;
|
||||||
|
-SELECT 'geometry', count(*) FROM tm.geogs_in i, tm.geogs o WHERE i.id = o.id
|
||||||
|
- AND ST_OrderingEquals(i.g::geometry, o.g::geometry);
|
||||||
|
+\set command '\\copy tm.geogs_in FROM ':tmpfile' WITH (FORMAT BINARY)'
|
||||||
|
+:command
|
||||||
|
+
|
||||||
|
+SELECT 'geometry', count(*) FROM tm.geogs_in i, tm.geogs o
|
||||||
|
+WHERE i.id = o.id AND ST_OrderingEquals(i.g::geometry, o.g::geometry);
|
||||||
|
|
||||||
|
DROP SCHEMA tm CASCADE;
|
||||||
|
+
|
||||||
|
diff --git a/regress/core/tests.mk b/regress/core/tests.mk
|
||||||
|
index 64a9254..94903c3 100644
|
||||||
|
--- a/regress/core/tests.mk
|
||||||
|
+++ b/regress/core/tests.mk
|
||||||
|
@@ -23,7 +23,6 @@ current_dir := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
RUNTESTFLAGS_INTERNAL += \
|
||||||
|
--before-upgrade-script $(top_srcdir)/regress/hooks/hook-before-upgrade.sql \
|
||||||
|
--after-upgrade-script $(top_srcdir)/regress/hooks/hook-after-upgrade.sql \
|
||||||
|
- --after-create-script $(top_srcdir)/regress/hooks/hook-after-create.sql \
|
||||||
|
--before-uninstall-script $(top_srcdir)/regress/hooks/hook-before-uninstall.sql
|
||||||
|
|
||||||
|
TESTS += \
|
||||||
|
@@ -40,7 +39,6 @@ TESTS += \
|
||||||
|
$(top_srcdir)/regress/core/dumppoints \
|
||||||
|
$(top_srcdir)/regress/core/dumpsegments \
|
||||||
|
$(top_srcdir)/regress/core/empty \
|
||||||
|
- $(top_srcdir)/regress/core/estimatedextent \
|
||||||
|
$(top_srcdir)/regress/core/forcecurve \
|
||||||
|
$(top_srcdir)/regress/core/flatgeobuf \
|
||||||
|
$(top_srcdir)/regress/core/geography \
|
||||||
|
@@ -55,7 +53,6 @@ TESTS += \
|
||||||
|
$(top_srcdir)/regress/core/out_marc21 \
|
||||||
|
$(top_srcdir)/regress/core/in_encodedpolyline \
|
||||||
|
$(top_srcdir)/regress/core/iscollection \
|
||||||
|
- $(top_srcdir)/regress/core/legacy \
|
||||||
|
$(top_srcdir)/regress/core/letters \
|
||||||
|
$(top_srcdir)/regress/core/long_xact \
|
||||||
|
$(top_srcdir)/regress/core/lwgeom_regress \
|
||||||
|
@@ -112,7 +109,6 @@ TESTS += \
|
||||||
|
$(top_srcdir)/regress/core/temporal_knn \
|
||||||
|
$(top_srcdir)/regress/core/tickets \
|
||||||
|
$(top_srcdir)/regress/core/twkb \
|
||||||
|
- $(top_srcdir)/regress/core/typmod \
|
||||||
|
$(top_srcdir)/regress/core/wkb \
|
||||||
|
$(top_srcdir)/regress/core/wkt \
|
||||||
|
$(top_srcdir)/regress/core/wmsservers \
|
||||||
|
diff --git a/regress/loader/tests.mk b/regress/loader/tests.mk
|
||||||
|
index 1fc77ac..c3cb9de 100644
|
||||||
|
--- a/regress/loader/tests.mk
|
||||||
|
+++ b/regress/loader/tests.mk
|
||||||
|
@@ -38,7 +38,5 @@ TESTS += \
|
||||||
|
$(top_srcdir)/regress/loader/Latin1 \
|
||||||
|
$(top_srcdir)/regress/loader/Latin1-implicit \
|
||||||
|
$(top_srcdir)/regress/loader/mfile \
|
||||||
|
- $(top_srcdir)/regress/loader/TestSkipANALYZE \
|
||||||
|
- $(top_srcdir)/regress/loader/TestANALYZE \
|
||||||
|
$(top_srcdir)/regress/loader/CharNoWidth
|
||||||
|
|
||||||
|
diff --git a/regress/run_test.pl b/regress/run_test.pl
|
||||||
|
index 0ec5b2d..1c331f4 100755
|
||||||
|
--- a/regress/run_test.pl
|
||||||
|
+++ b/regress/run_test.pl
|
||||||
|
@@ -147,7 +147,6 @@ $ENV{"LANG"} = "C";
|
||||||
|
# Add locale info to the psql options
|
||||||
|
# Add pg12 precision suppression
|
||||||
|
my $PGOPTIONS = $ENV{"PGOPTIONS"};
|
||||||
|
-$PGOPTIONS .= " -c lc_messages=C";
|
||||||
|
$PGOPTIONS .= " -c client_min_messages=NOTICE";
|
||||||
|
$PGOPTIONS .= " -c extra_float_digits=0";
|
||||||
|
$ENV{"PGOPTIONS"} = $PGOPTIONS;
|
||||||
208
docker-compose/ext-src/postgis-src/postgis-regular-v17.patch
Normal file
208
docker-compose/ext-src/postgis-src/postgis-regular-v17.patch
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
diff --git a/raster/test/regress/tests.mk b/raster/test/regress/tests.mk
|
||||||
|
index 00918e1..7e2b6cd 100644
|
||||||
|
--- a/raster/test/regress/tests.mk
|
||||||
|
+++ b/raster/test/regress/tests.mk
|
||||||
|
@@ -17,9 +17,7 @@ override RUNTESTFLAGS_INTERNAL := \
|
||||||
|
$(RUNTESTFLAGS_INTERNAL) \
|
||||||
|
--after-upgrade-script $(top_srcdir)/raster/test/regress/hooks/hook-after-upgrade-raster.sql
|
||||||
|
|
||||||
|
-RASTER_TEST_FIRST = \
|
||||||
|
- $(top_srcdir)/raster/test/regress/check_gdal \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/load_outdb
|
||||||
|
+RASTER_TEST_FIRST =
|
||||||
|
|
||||||
|
RASTER_TEST_LAST = \
|
||||||
|
$(top_srcdir)/raster/test/regress/clean
|
||||||
|
@@ -33,9 +31,7 @@ RASTER_TEST_IO = \
|
||||||
|
|
||||||
|
RASTER_TEST_BASIC_FUNC = \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_bytea \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_wkb \
|
||||||
|
$(top_srcdir)/raster/test/regress/box3d \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_addband \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_band \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_tile
|
||||||
|
|
||||||
|
@@ -73,16 +69,10 @@ RASTER_TEST_BANDPROPS = \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_neighborhood \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_nearestvalue \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_pixelofvalue \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_polygon \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_setbandpath
|
||||||
|
+ $(top_srcdir)/raster/test/regress/rt_polygon
|
||||||
|
|
||||||
|
RASTER_TEST_UTILITY = \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_utility \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_fromgdalraster \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_asgdalraster \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_astiff \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_asjpeg \
|
||||||
|
- $(top_srcdir)/raster/test/regress/rt_aspng \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_reclass \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_gdalwarp \
|
||||||
|
$(top_srcdir)/raster/test/regress/rt_gdalcontour \
|
||||||
|
@@ -120,21 +110,13 @@ RASTER_TEST_SREL = \
|
||||||
|
|
||||||
|
RASTER_TEST_BUGS = \
|
||||||
|
$(top_srcdir)/raster/test/regress/bug_test_car5 \
|
||||||
|
- $(top_srcdir)/raster/test/regress/permitted_gdal_drivers \
|
||||||
|
$(top_srcdir)/raster/test/regress/tickets
|
||||||
|
|
||||||
|
RASTER_TEST_LOADER = \
|
||||||
|
$(top_srcdir)/raster/test/regress/loader/Basic \
|
||||||
|
$(top_srcdir)/raster/test/regress/loader/Projected \
|
||||||
|
$(top_srcdir)/raster/test/regress/loader/BasicCopy \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/BasicFilename \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/BasicOutDB \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/Tiled10x10 \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/Tiled10x10Copy \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/Tiled8x8 \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/TiledAuto \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/TiledAutoSkipNoData \
|
||||||
|
- $(top_srcdir)/raster/test/regress/loader/TiledAutoCopyn
|
||||||
|
+ $(top_srcdir)/raster/test/regress/loader/BasicFilename
|
||||||
|
|
||||||
|
RASTER_TESTS := $(RASTER_TEST_FIRST) \
|
||||||
|
$(RASTER_TEST_METADATA) $(RASTER_TEST_IO) $(RASTER_TEST_BASIC_FUNC) \
|
||||||
|
diff --git a/regress/core/binary.sql b/regress/core/binary.sql
|
||||||
|
index 7a36b65..ad78fc7 100644
|
||||||
|
--- a/regress/core/binary.sql
|
||||||
|
+++ b/regress/core/binary.sql
|
||||||
|
@@ -1,4 +1,5 @@
|
||||||
|
SET client_min_messages TO warning;
|
||||||
|
+
|
||||||
|
CREATE SCHEMA tm;
|
||||||
|
|
||||||
|
CREATE TABLE tm.geoms (id serial, g geometry);
|
||||||
|
@@ -31,24 +32,39 @@ SELECT st_force4d(g) FROM tm.geoms WHERE id < 15 ORDER BY id;
|
||||||
|
INSERT INTO tm.geoms(g)
|
||||||
|
SELECT st_setsrid(g,4326) FROM tm.geoms ORDER BY id;
|
||||||
|
|
||||||
|
-COPY tm.geoms TO :tmpfile WITH BINARY;
|
||||||
|
+-- define temp file path
|
||||||
|
+\set tmpfile '/tmp/postgis_binary_test.dat'
|
||||||
|
+
|
||||||
|
+-- export
|
||||||
|
+\set command '\\copy tm.geoms TO ':tmpfile' WITH (FORMAT BINARY)'
|
||||||
|
+:command
|
||||||
|
+
|
||||||
|
+-- import
|
||||||
|
CREATE TABLE tm.geoms_in AS SELECT * FROM tm.geoms LIMIT 0;
|
||||||
|
-COPY tm.geoms_in FROM :tmpfile WITH BINARY;
|
||||||
|
-SELECT 'geometry', count(*) FROM tm.geoms_in i, tm.geoms o WHERE i.id = o.id
|
||||||
|
- AND ST_OrderingEquals(i.g, o.g);
|
||||||
|
+\set command '\\copy tm.geoms_in FROM ':tmpfile' WITH (FORMAT BINARY)'
|
||||||
|
+:command
|
||||||
|
+
|
||||||
|
+SELECT 'geometry', count(*) FROM tm.geoms_in i, tm.geoms o
|
||||||
|
+WHERE i.id = o.id AND ST_OrderingEquals(i.g, o.g);
|
||||||
|
|
||||||
|
CREATE TABLE tm.geogs AS SELECT id,g::geography FROM tm.geoms
|
||||||
|
WHERE geometrytype(g) NOT LIKE '%CURVE%'
|
||||||
|
AND geometrytype(g) NOT LIKE '%CIRCULAR%'
|
||||||
|
AND geometrytype(g) NOT LIKE '%SURFACE%'
|
||||||
|
AND geometrytype(g) NOT LIKE 'TRIANGLE%'
|
||||||
|
- AND geometrytype(g) NOT LIKE 'TIN%'
|
||||||
|
-;
|
||||||
|
+ AND geometrytype(g) NOT LIKE 'TIN%';
|
||||||
|
|
||||||
|
-COPY tm.geogs TO :tmpfile WITH BINARY;
|
||||||
|
+-- export
|
||||||
|
+\set command '\\copy tm.geogs TO ':tmpfile' WITH (FORMAT BINARY)'
|
||||||
|
+:command
|
||||||
|
+
|
||||||
|
+-- import
|
||||||
|
CREATE TABLE tm.geogs_in AS SELECT * FROM tm.geogs LIMIT 0;
|
||||||
|
-COPY tm.geogs_in FROM :tmpfile WITH BINARY;
|
||||||
|
-SELECT 'geometry', count(*) FROM tm.geogs_in i, tm.geogs o WHERE i.id = o.id
|
||||||
|
- AND ST_OrderingEquals(i.g::geometry, o.g::geometry);
|
||||||
|
+\set command '\\copy tm.geogs_in FROM ':tmpfile' WITH (FORMAT BINARY)'
|
||||||
|
+:command
|
||||||
|
+
|
||||||
|
+SELECT 'geometry', count(*) FROM tm.geogs_in i, tm.geogs o
|
||||||
|
+WHERE i.id = o.id AND ST_OrderingEquals(i.g::geometry, o.g::geometry);
|
||||||
|
|
||||||
|
DROP SCHEMA tm CASCADE;
|
||||||
|
+
|
||||||
|
diff --git a/regress/core/tests.mk b/regress/core/tests.mk
|
||||||
|
index 90987df..74fe3f1 100644
|
||||||
|
--- a/regress/core/tests.mk
|
||||||
|
+++ b/regress/core/tests.mk
|
||||||
|
@@ -16,14 +16,13 @@ POSTGIS_PGSQL_VERSION=170
|
||||||
|
POSTGIS_GEOS_VERSION=31101
|
||||||
|
HAVE_JSON=yes
|
||||||
|
HAVE_SPGIST=yes
|
||||||
|
-INTERRUPTTESTS=yes
|
||||||
|
+INTERRUPTTESTS=no
|
||||||
|
|
||||||
|
current_dir := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
|
||||||
|
RUNTESTFLAGS_INTERNAL += \
|
||||||
|
--before-upgrade-script $(top_srcdir)/regress/hooks/hook-before-upgrade.sql \
|
||||||
|
--after-upgrade-script $(top_srcdir)/regress/hooks/hook-after-upgrade.sql \
|
||||||
|
- --after-create-script $(top_srcdir)/regress/hooks/hook-after-create.sql \
|
||||||
|
--before-uninstall-script $(top_srcdir)/regress/hooks/hook-before-uninstall.sql
|
||||||
|
|
||||||
|
TESTS += \
|
||||||
|
@@ -40,7 +39,6 @@ TESTS += \
|
||||||
|
$(top_srcdir)/regress/core/dumppoints \
|
||||||
|
$(top_srcdir)/regress/core/dumpsegments \
|
||||||
|
$(top_srcdir)/regress/core/empty \
|
||||||
|
- $(top_srcdir)/regress/core/estimatedextent \
|
||||||
|
$(top_srcdir)/regress/core/forcecurve \
|
||||||
|
$(top_srcdir)/regress/core/flatgeobuf \
|
||||||
|
$(top_srcdir)/regress/core/frechet \
|
||||||
|
@@ -60,7 +58,6 @@ TESTS += \
|
||||||
|
$(top_srcdir)/regress/core/out_marc21 \
|
||||||
|
$(top_srcdir)/regress/core/in_encodedpolyline \
|
||||||
|
$(top_srcdir)/regress/core/iscollection \
|
||||||
|
- $(top_srcdir)/regress/core/legacy \
|
||||||
|
$(top_srcdir)/regress/core/letters \
|
||||||
|
$(top_srcdir)/regress/core/lwgeom_regress \
|
||||||
|
$(top_srcdir)/regress/core/measures \
|
||||||
|
@@ -119,7 +116,6 @@ TESTS += \
|
||||||
|
$(top_srcdir)/regress/core/temporal_knn \
|
||||||
|
$(top_srcdir)/regress/core/tickets \
|
||||||
|
$(top_srcdir)/regress/core/twkb \
|
||||||
|
- $(top_srcdir)/regress/core/typmod \
|
||||||
|
$(top_srcdir)/regress/core/wkb \
|
||||||
|
$(top_srcdir)/regress/core/wkt \
|
||||||
|
$(top_srcdir)/regress/core/wmsservers \
|
||||||
|
diff --git a/regress/loader/tests.mk b/regress/loader/tests.mk
|
||||||
|
index ac4f8ad..4bad4fc 100644
|
||||||
|
--- a/regress/loader/tests.mk
|
||||||
|
+++ b/regress/loader/tests.mk
|
||||||
|
@@ -38,7 +38,5 @@ TESTS += \
|
||||||
|
$(top_srcdir)/regress/loader/Latin1 \
|
||||||
|
$(top_srcdir)/regress/loader/Latin1-implicit \
|
||||||
|
$(top_srcdir)/regress/loader/mfile \
|
||||||
|
- $(top_srcdir)/regress/loader/TestSkipANALYZE \
|
||||||
|
- $(top_srcdir)/regress/loader/TestANALYZE \
|
||||||
|
$(top_srcdir)/regress/loader/CharNoWidth \
|
||||||
|
|
||||||
|
diff --git a/regress/run_test.pl b/regress/run_test.pl
|
||||||
|
index cac4b2e..4c7c82b 100755
|
||||||
|
--- a/regress/run_test.pl
|
||||||
|
+++ b/regress/run_test.pl
|
||||||
|
@@ -238,7 +238,6 @@ $ENV{"LANG"} = "C";
|
||||||
|
# Add locale info to the psql options
|
||||||
|
# Add pg12 precision suppression
|
||||||
|
my $PGOPTIONS = $ENV{"PGOPTIONS"};
|
||||||
|
-$PGOPTIONS .= " -c lc_messages=C";
|
||||||
|
$PGOPTIONS .= " -c client_min_messages=NOTICE";
|
||||||
|
$PGOPTIONS .= " -c extra_float_digits=0";
|
||||||
|
$ENV{"PGOPTIONS"} = $PGOPTIONS;
|
||||||
|
diff --git a/topology/test/tests.mk b/topology/test/tests.mk
|
||||||
|
index cbe2633..2c7c18f 100644
|
||||||
|
--- a/topology/test/tests.mk
|
||||||
|
+++ b/topology/test/tests.mk
|
||||||
|
@@ -46,9 +46,7 @@ TESTS += \
|
||||||
|
$(top_srcdir)/topology/test/regress/legacy_query.sql \
|
||||||
|
$(top_srcdir)/topology/test/regress/legacy_validate.sql \
|
||||||
|
$(top_srcdir)/topology/test/regress/polygonize.sql \
|
||||||
|
- $(top_srcdir)/topology/test/regress/populate_topology_layer.sql \
|
||||||
|
$(top_srcdir)/topology/test/regress/removeunusedprimitives.sql \
|
||||||
|
- $(top_srcdir)/topology/test/regress/renametopogeometrycolumn.sql \
|
||||||
|
$(top_srcdir)/topology/test/regress/renametopology.sql \
|
||||||
|
$(top_srcdir)/topology/test/regress/share_sequences.sql \
|
||||||
|
$(top_srcdir)/topology/test/regress/sqlmm.sql \
|
||||||
46
docker-compose/ext-src/postgis-src/raster_outdb_template.sql
Normal file
46
docker-compose/ext-src/postgis-src/raster_outdb_template.sql
Normal file
File diff suppressed because one or more lines are too long
17
docker-compose/ext-src/postgis-src/regular-test.sh
Executable file
17
docker-compose/ext-src/postgis-src/regular-test.sh
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -ex
|
||||||
|
cd "$(dirname "${0}")"
|
||||||
|
dropdb --if-exist contrib_regression
|
||||||
|
createdb contrib_regression
|
||||||
|
psql -d contrib_regression -c "ALTER DATABASE contrib_regression SET TimeZone='UTC'" \
|
||||||
|
-c "ALTER DATABASE contrib_regression SET DateStyle='ISO, MDY'" \
|
||||||
|
-c "CREATE EXTENSION postgis SCHEMA public" \
|
||||||
|
-c "CREATE EXTENSION postgis_topology" \
|
||||||
|
-c "CREATE EXTENSION postgis_tiger_geocoder CASCADE" \
|
||||||
|
-c "CREATE EXTENSION postgis_raster SCHEMA public" \
|
||||||
|
-c "CREATE EXTENSION postgis_sfcgal SCHEMA public"
|
||||||
|
patch -p1 <"postgis-common-${PG_VERSION}.patch"
|
||||||
|
patch -p1 <"postgis-regular-${PG_VERSION}.patch"
|
||||||
|
psql -d contrib_regression -f raster_outdb_template.sql
|
||||||
|
trap 'patch -R -p1 <postgis-regular-${PG_VERSION}.patch && patch -R -p1 <"postgis-common-${PG_VERSION}.patch"' EXIT
|
||||||
|
POSTGIS_REGRESS_DB=contrib_regression RUNTESTFLAGS=--nocreate make installcheck-base
|
||||||
@@ -63,5 +63,9 @@ done
|
|||||||
for d in ${FAILED}; do
|
for d in ${FAILED}; do
|
||||||
cat "$(find $d -name regression.diffs)"
|
cat "$(find $d -name regression.diffs)"
|
||||||
done
|
done
|
||||||
|
for postgis_diff in /tmp/pgis_reg/*_diff; do
|
||||||
|
echo "${postgis_diff}:"
|
||||||
|
cat "${postgis_diff}"
|
||||||
|
done
|
||||||
echo "${FAILED}"
|
echo "${FAILED}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ anyhow.workspace = true
|
|||||||
axum-extra.workspace = true
|
axum-extra.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
camino.workspace = true
|
camino.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
jsonwebtoken.workspace = true
|
jsonwebtoken.workspace = true
|
||||||
prometheus.workspace = true
|
prometheus.workspace = true
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
//! for large computes.
|
//! for large computes.
|
||||||
mod app;
|
mod app;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use clap::Parser;
|
||||||
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use utils::logging;
|
use utils::logging;
|
||||||
|
|
||||||
@@ -12,9 +14,26 @@ const fn max_upload_file_limit() -> usize {
|
|||||||
100 * 1024 * 1024
|
100 * 1024 * 1024
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fn listen() -> SocketAddr {
|
||||||
|
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 51243)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Args {
|
||||||
|
#[arg(exclusive = true)]
|
||||||
|
config_file: Option<String>,
|
||||||
|
#[arg(long, default_value = "false", requires = "config")]
|
||||||
|
/// to allow testing k8s helm chart where we don't have s3 credentials
|
||||||
|
no_s3_check_on_startup: bool,
|
||||||
|
#[arg(long, value_name = "FILE")]
|
||||||
|
/// inline config mode for k8s helm chart
|
||||||
|
config: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
struct Config {
|
struct Config {
|
||||||
|
#[serde(default = "listen")]
|
||||||
listen: std::net::SocketAddr,
|
listen: std::net::SocketAddr,
|
||||||
pemfile: camino::Utf8PathBuf,
|
pemfile: camino::Utf8PathBuf,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
@@ -31,13 +50,18 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
logging::Output::Stdout,
|
logging::Output::Stdout,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let config: String = std::env::args().skip(1).take(1).collect();
|
let args = Args::parse();
|
||||||
if config.is_empty() {
|
let config: Config = if let Some(config_path) = args.config_file {
|
||||||
anyhow::bail!("Usage: endpoint_storage config.json")
|
info!("Reading config from {config_path}");
|
||||||
}
|
let config = std::fs::read_to_string(config_path)?;
|
||||||
info!("Reading config from {config}");
|
serde_json::from_str(&config).context("parsing config")?
|
||||||
let config = std::fs::read_to_string(config.clone())?;
|
} else if let Some(config) = args.config {
|
||||||
let config: Config = serde_json::from_str(&config).context("parsing config")?;
|
info!("Reading inline config");
|
||||||
|
serde_json::from_str(&config).context("parsing config")?
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("Supply either config file path or --config=inline-config");
|
||||||
|
};
|
||||||
|
|
||||||
info!("Reading pemfile from {}", config.pemfile.clone());
|
info!("Reading pemfile from {}", config.pemfile.clone());
|
||||||
let pemfile = std::fs::read(config.pemfile.clone())?;
|
let pemfile = std::fs::read(config.pemfile.clone())?;
|
||||||
info!("Loading public key from {}", config.pemfile.clone());
|
info!("Loading public key from {}", config.pemfile.clone());
|
||||||
@@ -48,7 +72,9 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let storage = remote_storage::GenericRemoteStorage::from_config(&config.storage_config).await?;
|
let storage = remote_storage::GenericRemoteStorage::from_config(&config.storage_config).await?;
|
||||||
let cancel = tokio_util::sync::CancellationToken::new();
|
let cancel = tokio_util::sync::CancellationToken::new();
|
||||||
app::check_storage_permissions(&storage, cancel.clone()).await?;
|
if !args.no_s3_check_on_startup {
|
||||||
|
app::check_storage_permissions(&storage, cancel.clone()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
let proxy = std::sync::Arc::new(endpoint_storage::Storage {
|
let proxy = std::sync::Arc::new(endpoint_storage::Storage {
|
||||||
auth,
|
auth,
|
||||||
|
|||||||
@@ -178,9 +178,9 @@ pub struct ComputeSpec {
|
|||||||
/// JWT for authorizing requests to endpoint storage service
|
/// JWT for authorizing requests to endpoint storage service
|
||||||
pub endpoint_storage_token: Option<String>,
|
pub endpoint_storage_token: Option<String>,
|
||||||
|
|
||||||
/// If true, download LFC state from endpoint_storage and pass it to Postgres on startup
|
/// Download LFC state from endpoint_storage and pass it to Postgres on startup
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub prewarm_lfc_on_startup: bool,
|
pub autoprewarm: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Feature flag to signal `compute_ctl` to enable certain experimental functionality.
|
/// Feature flag to signal `compute_ctl` to enable certain experimental functionality.
|
||||||
@@ -192,6 +192,9 @@ pub enum ComputeFeature {
|
|||||||
/// track short-lived connections as user activity.
|
/// track short-lived connections as user activity.
|
||||||
ActivityMonitorExperimental,
|
ActivityMonitorExperimental,
|
||||||
|
|
||||||
|
/// Enable TLS functionality.
|
||||||
|
TlsExperimental,
|
||||||
|
|
||||||
/// This is a special feature flag that is used to represent unknown feature flags.
|
/// This is a special feature flag that is used to represent unknown feature flags.
|
||||||
/// Basically all unknown to enum flags are represented as this one. See unit test
|
/// Basically all unknown to enum flags are represented as this one. See unit test
|
||||||
/// `parse_unknown_features()` for more details.
|
/// `parse_unknown_features()` for more details.
|
||||||
@@ -250,34 +253,44 @@ impl RemoteExtSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match self.extension_data.get(real_ext_name) {
|
match self.extension_data.get(real_ext_name) {
|
||||||
Some(_ext_data) => {
|
Some(_ext_data) => Ok((
|
||||||
// We have decided to use the Go naming convention due to Kubernetes.
|
real_ext_name.to_string(),
|
||||||
|
Self::build_remote_path(build_tag, pg_major_version, real_ext_name)?,
|
||||||
let arch = match std::env::consts::ARCH {
|
)),
|
||||||
"x86_64" => "amd64",
|
|
||||||
"aarch64" => "arm64",
|
|
||||||
arch => arch,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Construct the path to the extension archive
|
|
||||||
// BUILD_TAG/PG_MAJOR_VERSION/extensions/EXTENSION_NAME.tar.zst
|
|
||||||
//
|
|
||||||
// Keep it in sync with path generation in
|
|
||||||
// https://github.com/neondatabase/build-custom-extensions/tree/main
|
|
||||||
let archive_path_str = format!(
|
|
||||||
"{build_tag}/{arch}/{pg_major_version}/extensions/{real_ext_name}.tar.zst"
|
|
||||||
);
|
|
||||||
Ok((
|
|
||||||
real_ext_name.to_string(),
|
|
||||||
RemotePath::from_string(&archive_path_str)?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
None => Err(anyhow::anyhow!(
|
None => Err(anyhow::anyhow!(
|
||||||
"real_ext_name {} is not found",
|
"real_ext_name {} is not found",
|
||||||
real_ext_name
|
real_ext_name
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the architecture-specific portion of the remote extension path. We
|
||||||
|
/// use the Go naming convention due to Kubernetes.
|
||||||
|
fn get_arch() -> &'static str {
|
||||||
|
match std::env::consts::ARCH {
|
||||||
|
"x86_64" => "amd64",
|
||||||
|
"aarch64" => "arm64",
|
||||||
|
arch => arch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a [`RemotePath`] for an extension.
|
||||||
|
fn build_remote_path(
|
||||||
|
build_tag: &str,
|
||||||
|
pg_major_version: &str,
|
||||||
|
ext_name: &str,
|
||||||
|
) -> anyhow::Result<RemotePath> {
|
||||||
|
let arch = Self::get_arch();
|
||||||
|
|
||||||
|
// Construct the path to the extension archive
|
||||||
|
// BUILD_TAG/PG_MAJOR_VERSION/extensions/EXTENSION_NAME.tar.zst
|
||||||
|
//
|
||||||
|
// Keep it in sync with path generation in
|
||||||
|
// https://github.com/neondatabase/build-custom-extensions/tree/main
|
||||||
|
RemotePath::from_string(&format!(
|
||||||
|
"{build_tag}/{arch}/{pg_major_version}/extensions/{ext_name}.tar.zst"
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
@@ -518,6 +531,37 @@ mod tests {
|
|||||||
.expect("Library should be found");
|
.expect("Library should be found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remote_extension_path() {
|
||||||
|
let rspec: RemoteExtSpec = serde_json::from_value(serde_json::json!({
|
||||||
|
"public_extensions": ["ext"],
|
||||||
|
"custom_extensions": [],
|
||||||
|
"library_index": {
|
||||||
|
"extlib": "ext",
|
||||||
|
},
|
||||||
|
"extension_data": {
|
||||||
|
"ext": {
|
||||||
|
"control_data": {
|
||||||
|
"ext.control": ""
|
||||||
|
},
|
||||||
|
"archive_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (_ext_name, ext_path) = rspec
|
||||||
|
.get_ext("ext", false, "latest", "v17")
|
||||||
|
.expect("Extension should be found");
|
||||||
|
// Starting with a forward slash would have consequences for the
|
||||||
|
// Url::join() that occurs when downloading a remote extension.
|
||||||
|
assert!(!ext_path.to_string().starts_with("/"));
|
||||||
|
assert_eq!(
|
||||||
|
ext_path,
|
||||||
|
RemoteExtSpec::build_remote_path("latest", "v17", "ext").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_spec_file() {
|
fn parse_spec_file() {
|
||||||
let file = File::open("tests/cluster_spec.json").unwrap();
|
let file = File::open("tests/cluster_spec.json").unwrap();
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
"vartype": "bool"
|
"vartype": "bool"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "prewarm_lfc_on_startup",
|
"name": "autoprewarm",
|
||||||
"value": "off",
|
"value": "off",
|
||||||
"vartype": "bool"
|
"vartype": "bool"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ impl<const N: usize> MetricType for HyperLogLogState<N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> HyperLogLogState<N> {
|
impl<const N: usize> HyperLogLogState<N> {
|
||||||
pub fn measure(&self, item: &impl Hash) {
|
pub fn measure(&self, item: &(impl Hash + ?Sized)) {
|
||||||
// changing the hasher will break compatibility with previous measurements.
|
// changing the hasher will break compatibility with previous measurements.
|
||||||
self.record(BuildHasherDefault::<xxh3::Hash64>::default().hash_one(item));
|
self.record(BuildHasherDefault::<xxh3::Hash64>::default().hash_one(item));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ use postgres_backend::AuthType;
|
|||||||
use remote_storage::RemoteStorageConfig;
|
use remote_storage::RemoteStorageConfig;
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use utils::logging::LogFormat;
|
use utils::logging::LogFormat;
|
||||||
use utils::postgres_client::PostgresClientProtocol;
|
|
||||||
|
|
||||||
use crate::models::{ImageCompressionAlgorithm, LsnLease};
|
use crate::models::{ImageCompressionAlgorithm, LsnLease};
|
||||||
|
|
||||||
@@ -181,6 +180,7 @@ pub struct ConfigToml {
|
|||||||
pub virtual_file_io_engine: Option<crate::models::virtual_file::IoEngineKind>,
|
pub virtual_file_io_engine: Option<crate::models::virtual_file::IoEngineKind>,
|
||||||
pub ingest_batch_size: u64,
|
pub ingest_batch_size: u64,
|
||||||
pub max_vectored_read_bytes: MaxVectoredReadBytes,
|
pub max_vectored_read_bytes: MaxVectoredReadBytes,
|
||||||
|
pub max_get_vectored_keys: MaxGetVectoredKeys,
|
||||||
pub image_compression: ImageCompressionAlgorithm,
|
pub image_compression: ImageCompressionAlgorithm,
|
||||||
pub timeline_offloading: bool,
|
pub timeline_offloading: bool,
|
||||||
pub ephemeral_bytes_per_memory_kb: usize,
|
pub ephemeral_bytes_per_memory_kb: usize,
|
||||||
@@ -188,7 +188,6 @@ pub struct ConfigToml {
|
|||||||
pub virtual_file_io_mode: Option<crate::models::virtual_file::IoMode>,
|
pub virtual_file_io_mode: Option<crate::models::virtual_file::IoMode>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub no_sync: Option<bool>,
|
pub no_sync: Option<bool>,
|
||||||
pub wal_receiver_protocol: PostgresClientProtocol,
|
|
||||||
pub page_service_pipelining: PageServicePipeliningConfig,
|
pub page_service_pipelining: PageServicePipeliningConfig,
|
||||||
pub get_vectored_concurrent_io: GetVectoredConcurrentIo,
|
pub get_vectored_concurrent_io: GetVectoredConcurrentIo,
|
||||||
pub enable_read_path_debugging: Option<bool>,
|
pub enable_read_path_debugging: Option<bool>,
|
||||||
@@ -229,7 +228,7 @@ pub enum PageServicePipeliningConfig {
|
|||||||
}
|
}
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct PageServicePipeliningConfigPipelined {
|
pub struct PageServicePipeliningConfigPipelined {
|
||||||
/// Causes runtime errors if larger than max get_vectored batch size.
|
/// Failed config parsing and validation if larger than `max_get_vectored_keys`.
|
||||||
pub max_batch_size: NonZeroUsize,
|
pub max_batch_size: NonZeroUsize,
|
||||||
pub execution: PageServiceProtocolPipelinedExecutionStrategy,
|
pub execution: PageServiceProtocolPipelinedExecutionStrategy,
|
||||||
// The default below is such that new versions of the software can start
|
// The default below is such that new versions of the software can start
|
||||||
@@ -329,6 +328,8 @@ pub struct TimelineImportConfig {
|
|||||||
pub import_job_concurrency: NonZeroUsize,
|
pub import_job_concurrency: NonZeroUsize,
|
||||||
pub import_job_soft_size_limit: NonZeroUsize,
|
pub import_job_soft_size_limit: NonZeroUsize,
|
||||||
pub import_job_checkpoint_threshold: NonZeroUsize,
|
pub import_job_checkpoint_threshold: NonZeroUsize,
|
||||||
|
/// Max size of the remote storage partial read done by any job
|
||||||
|
pub import_job_max_byte_range_size: NonZeroUsize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -403,6 +404,16 @@ impl Default for EvictionOrder {
|
|||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct MaxVectoredReadBytes(pub NonZeroUsize);
|
pub struct MaxVectoredReadBytes(pub NonZeroUsize);
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct MaxGetVectoredKeys(NonZeroUsize);
|
||||||
|
|
||||||
|
impl MaxGetVectoredKeys {
|
||||||
|
pub fn get(&self) -> usize {
|
||||||
|
self.0.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Tenant-level configuration values, used for various purposes.
|
/// Tenant-level configuration values, used for various purposes.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -514,8 +525,6 @@ pub struct TenantConfigToml {
|
|||||||
/// (either this flag or the pageserver-global one need to be set)
|
/// (either this flag or the pageserver-global one need to be set)
|
||||||
pub timeline_offloading: bool,
|
pub timeline_offloading: bool,
|
||||||
|
|
||||||
pub wal_receiver_protocol_override: Option<PostgresClientProtocol>,
|
|
||||||
|
|
||||||
/// Enable rel_size_v2 for this tenant. Once enabled, the tenant will persist this information into
|
/// Enable rel_size_v2 for this tenant. Once enabled, the tenant will persist this information into
|
||||||
/// `index_part.json`, and it cannot be reversed.
|
/// `index_part.json`, and it cannot be reversed.
|
||||||
pub rel_size_v2_enabled: bool,
|
pub rel_size_v2_enabled: bool,
|
||||||
@@ -587,6 +596,8 @@ pub mod defaults {
|
|||||||
/// That is, slightly above 128 kB.
|
/// That is, slightly above 128 kB.
|
||||||
pub const DEFAULT_MAX_VECTORED_READ_BYTES: usize = 130 * 1024; // 130 KiB
|
pub const DEFAULT_MAX_VECTORED_READ_BYTES: usize = 130 * 1024; // 130 KiB
|
||||||
|
|
||||||
|
pub const DEFAULT_MAX_GET_VECTORED_KEYS: usize = 32;
|
||||||
|
|
||||||
pub const DEFAULT_IMAGE_COMPRESSION: ImageCompressionAlgorithm =
|
pub const DEFAULT_IMAGE_COMPRESSION: ImageCompressionAlgorithm =
|
||||||
ImageCompressionAlgorithm::Zstd { level: Some(1) };
|
ImageCompressionAlgorithm::Zstd { level: Some(1) };
|
||||||
|
|
||||||
@@ -594,9 +605,6 @@ pub mod defaults {
|
|||||||
|
|
||||||
pub const DEFAULT_IO_BUFFER_ALIGNMENT: usize = 512;
|
pub const DEFAULT_IO_BUFFER_ALIGNMENT: usize = 512;
|
||||||
|
|
||||||
pub const DEFAULT_WAL_RECEIVER_PROTOCOL: utils::postgres_client::PostgresClientProtocol =
|
|
||||||
utils::postgres_client::PostgresClientProtocol::Vanilla;
|
|
||||||
|
|
||||||
pub const DEFAULT_SSL_KEY_FILE: &str = "server.key";
|
pub const DEFAULT_SSL_KEY_FILE: &str = "server.key";
|
||||||
pub const DEFAULT_SSL_CERT_FILE: &str = "server.crt";
|
pub const DEFAULT_SSL_CERT_FILE: &str = "server.crt";
|
||||||
}
|
}
|
||||||
@@ -685,6 +693,9 @@ impl Default for ConfigToml {
|
|||||||
max_vectored_read_bytes: (MaxVectoredReadBytes(
|
max_vectored_read_bytes: (MaxVectoredReadBytes(
|
||||||
NonZeroUsize::new(DEFAULT_MAX_VECTORED_READ_BYTES).unwrap(),
|
NonZeroUsize::new(DEFAULT_MAX_VECTORED_READ_BYTES).unwrap(),
|
||||||
)),
|
)),
|
||||||
|
max_get_vectored_keys: (MaxGetVectoredKeys(
|
||||||
|
NonZeroUsize::new(DEFAULT_MAX_GET_VECTORED_KEYS).unwrap(),
|
||||||
|
)),
|
||||||
image_compression: (DEFAULT_IMAGE_COMPRESSION),
|
image_compression: (DEFAULT_IMAGE_COMPRESSION),
|
||||||
timeline_offloading: true,
|
timeline_offloading: true,
|
||||||
ephemeral_bytes_per_memory_kb: (DEFAULT_EPHEMERAL_BYTES_PER_MEMORY_KB),
|
ephemeral_bytes_per_memory_kb: (DEFAULT_EPHEMERAL_BYTES_PER_MEMORY_KB),
|
||||||
@@ -692,7 +703,6 @@ impl Default for ConfigToml {
|
|||||||
virtual_file_io_mode: None,
|
virtual_file_io_mode: None,
|
||||||
tenant_config: TenantConfigToml::default(),
|
tenant_config: TenantConfigToml::default(),
|
||||||
no_sync: None,
|
no_sync: None,
|
||||||
wal_receiver_protocol: DEFAULT_WAL_RECEIVER_PROTOCOL,
|
|
||||||
page_service_pipelining: PageServicePipeliningConfig::Pipelined(
|
page_service_pipelining: PageServicePipeliningConfig::Pipelined(
|
||||||
PageServicePipeliningConfigPipelined {
|
PageServicePipeliningConfigPipelined {
|
||||||
max_batch_size: NonZeroUsize::new(32).unwrap(),
|
max_batch_size: NonZeroUsize::new(32).unwrap(),
|
||||||
@@ -713,9 +723,10 @@ impl Default for ConfigToml {
|
|||||||
enable_tls_page_service_api: false,
|
enable_tls_page_service_api: false,
|
||||||
dev_mode: false,
|
dev_mode: false,
|
||||||
timeline_import_config: TimelineImportConfig {
|
timeline_import_config: TimelineImportConfig {
|
||||||
import_job_concurrency: NonZeroUsize::new(128).unwrap(),
|
import_job_concurrency: NonZeroUsize::new(32).unwrap(),
|
||||||
import_job_soft_size_limit: NonZeroUsize::new(1024 * 1024 * 1024).unwrap(),
|
import_job_soft_size_limit: NonZeroUsize::new(256 * 1024 * 1024).unwrap(),
|
||||||
import_job_checkpoint_threshold: NonZeroUsize::new(128).unwrap(),
|
import_job_checkpoint_threshold: NonZeroUsize::new(32).unwrap(),
|
||||||
|
import_job_max_byte_range_size: NonZeroUsize::new(4 * 1024 * 1024).unwrap(),
|
||||||
},
|
},
|
||||||
basebackup_cache_config: None,
|
basebackup_cache_config: None,
|
||||||
posthog_config: None,
|
posthog_config: None,
|
||||||
@@ -836,7 +847,6 @@ impl Default for TenantConfigToml {
|
|||||||
lsn_lease_length: LsnLease::DEFAULT_LENGTH,
|
lsn_lease_length: LsnLease::DEFAULT_LENGTH,
|
||||||
lsn_lease_length_for_ts: LsnLease::DEFAULT_LENGTH_FOR_TS,
|
lsn_lease_length_for_ts: LsnLease::DEFAULT_LENGTH_FOR_TS,
|
||||||
timeline_offloading: true,
|
timeline_offloading: true,
|
||||||
wal_receiver_protocol_override: None,
|
|
||||||
rel_size_v2_enabled: false,
|
rel_size_v2_enabled: false,
|
||||||
gc_compaction_enabled: DEFAULT_GC_COMPACTION_ENABLED,
|
gc_compaction_enabled: DEFAULT_GC_COMPACTION_ENABLED,
|
||||||
gc_compaction_verification: DEFAULT_GC_COMPACTION_VERIFICATION,
|
gc_compaction_verification: DEFAULT_GC_COMPACTION_VERIFICATION,
|
||||||
|
|||||||
@@ -344,6 +344,35 @@ impl Default for ShardSchedulingPolicy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Debug)]
|
||||||
|
pub enum NodeLifecycle {
|
||||||
|
Active,
|
||||||
|
Deleted,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for NodeLifecycle {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"active" => Ok(Self::Active),
|
||||||
|
"deleted" => Ok(Self::Deleted),
|
||||||
|
_ => Err(anyhow::anyhow!("Unknown node lifecycle '{s}'")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NodeLifecycle> for String {
|
||||||
|
fn from(value: NodeLifecycle) -> String {
|
||||||
|
use NodeLifecycle::*;
|
||||||
|
match value {
|
||||||
|
Active => "active",
|
||||||
|
Deleted => "deleted",
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Debug)]
|
||||||
pub enum NodeSchedulingPolicy {
|
pub enum NodeSchedulingPolicy {
|
||||||
Active,
|
Active,
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ use serde_with::serde_as;
|
|||||||
pub use utilization::PageserverUtilization;
|
pub use utilization::PageserverUtilization;
|
||||||
use utils::id::{NodeId, TenantId, TimelineId};
|
use utils::id::{NodeId, TenantId, TimelineId};
|
||||||
use utils::lsn::Lsn;
|
use utils::lsn::Lsn;
|
||||||
use utils::postgres_client::PostgresClientProtocol;
|
|
||||||
use utils::{completion, serde_system_time};
|
use utils::{completion, serde_system_time};
|
||||||
|
|
||||||
use crate::config::Ratio;
|
use crate::config::Ratio;
|
||||||
@@ -622,8 +621,6 @@ pub struct TenantConfigPatch {
|
|||||||
#[serde(skip_serializing_if = "FieldPatch::is_noop")]
|
#[serde(skip_serializing_if = "FieldPatch::is_noop")]
|
||||||
pub timeline_offloading: FieldPatch<bool>,
|
pub timeline_offloading: FieldPatch<bool>,
|
||||||
#[serde(skip_serializing_if = "FieldPatch::is_noop")]
|
#[serde(skip_serializing_if = "FieldPatch::is_noop")]
|
||||||
pub wal_receiver_protocol_override: FieldPatch<PostgresClientProtocol>,
|
|
||||||
#[serde(skip_serializing_if = "FieldPatch::is_noop")]
|
|
||||||
pub rel_size_v2_enabled: FieldPatch<bool>,
|
pub rel_size_v2_enabled: FieldPatch<bool>,
|
||||||
#[serde(skip_serializing_if = "FieldPatch::is_noop")]
|
#[serde(skip_serializing_if = "FieldPatch::is_noop")]
|
||||||
pub gc_compaction_enabled: FieldPatch<bool>,
|
pub gc_compaction_enabled: FieldPatch<bool>,
|
||||||
@@ -748,9 +745,6 @@ pub struct TenantConfig {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub timeline_offloading: Option<bool>,
|
pub timeline_offloading: Option<bool>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub wal_receiver_protocol_override: Option<PostgresClientProtocol>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub rel_size_v2_enabled: Option<bool>,
|
pub rel_size_v2_enabled: Option<bool>,
|
||||||
|
|
||||||
@@ -812,7 +806,6 @@ impl TenantConfig {
|
|||||||
mut lsn_lease_length,
|
mut lsn_lease_length,
|
||||||
mut lsn_lease_length_for_ts,
|
mut lsn_lease_length_for_ts,
|
||||||
mut timeline_offloading,
|
mut timeline_offloading,
|
||||||
mut wal_receiver_protocol_override,
|
|
||||||
mut rel_size_v2_enabled,
|
mut rel_size_v2_enabled,
|
||||||
mut gc_compaction_enabled,
|
mut gc_compaction_enabled,
|
||||||
mut gc_compaction_verification,
|
mut gc_compaction_verification,
|
||||||
@@ -905,9 +898,6 @@ impl TenantConfig {
|
|||||||
.map(|v| humantime::parse_duration(&v))?
|
.map(|v| humantime::parse_duration(&v))?
|
||||||
.apply(&mut lsn_lease_length_for_ts);
|
.apply(&mut lsn_lease_length_for_ts);
|
||||||
patch.timeline_offloading.apply(&mut timeline_offloading);
|
patch.timeline_offloading.apply(&mut timeline_offloading);
|
||||||
patch
|
|
||||||
.wal_receiver_protocol_override
|
|
||||||
.apply(&mut wal_receiver_protocol_override);
|
|
||||||
patch.rel_size_v2_enabled.apply(&mut rel_size_v2_enabled);
|
patch.rel_size_v2_enabled.apply(&mut rel_size_v2_enabled);
|
||||||
patch
|
patch
|
||||||
.gc_compaction_enabled
|
.gc_compaction_enabled
|
||||||
@@ -960,7 +950,6 @@ impl TenantConfig {
|
|||||||
lsn_lease_length,
|
lsn_lease_length,
|
||||||
lsn_lease_length_for_ts,
|
lsn_lease_length_for_ts,
|
||||||
timeline_offloading,
|
timeline_offloading,
|
||||||
wal_receiver_protocol_override,
|
|
||||||
rel_size_v2_enabled,
|
rel_size_v2_enabled,
|
||||||
gc_compaction_enabled,
|
gc_compaction_enabled,
|
||||||
gc_compaction_verification,
|
gc_compaction_verification,
|
||||||
@@ -1058,9 +1047,6 @@ impl TenantConfig {
|
|||||||
timeline_offloading: self
|
timeline_offloading: self
|
||||||
.timeline_offloading
|
.timeline_offloading
|
||||||
.unwrap_or(global_conf.timeline_offloading),
|
.unwrap_or(global_conf.timeline_offloading),
|
||||||
wal_receiver_protocol_override: self
|
|
||||||
.wal_receiver_protocol_override
|
|
||||||
.or(global_conf.wal_receiver_protocol_override),
|
|
||||||
rel_size_v2_enabled: self
|
rel_size_v2_enabled: self
|
||||||
.rel_size_v2_enabled
|
.rel_size_v2_enabled
|
||||||
.unwrap_or(global_conf.rel_size_v2_enabled),
|
.unwrap_or(global_conf.rel_size_v2_enabled),
|
||||||
@@ -1934,7 +1920,7 @@ pub enum PagestreamFeMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wrapped in libpq CopyData
|
// Wrapped in libpq CopyData
|
||||||
#[derive(strum_macros::EnumProperty)]
|
#[derive(Debug, strum_macros::EnumProperty)]
|
||||||
pub enum PagestreamBeMessage {
|
pub enum PagestreamBeMessage {
|
||||||
Exists(PagestreamExistsResponse),
|
Exists(PagestreamExistsResponse),
|
||||||
Nblocks(PagestreamNblocksResponse),
|
Nblocks(PagestreamNblocksResponse),
|
||||||
@@ -2045,7 +2031,7 @@ pub enum PagestreamProtocolVersion {
|
|||||||
|
|
||||||
pub type RequestId = u64;
|
pub type RequestId = u64;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
|
||||||
pub struct PagestreamRequest {
|
pub struct PagestreamRequest {
|
||||||
pub reqid: RequestId,
|
pub reqid: RequestId,
|
||||||
pub request_lsn: Lsn,
|
pub request_lsn: Lsn,
|
||||||
@@ -2064,7 +2050,7 @@ pub struct PagestreamNblocksRequest {
|
|||||||
pub rel: RelTag,
|
pub rel: RelTag,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
|
||||||
pub struct PagestreamGetPageRequest {
|
pub struct PagestreamGetPageRequest {
|
||||||
pub hdr: PagestreamRequest,
|
pub hdr: PagestreamRequest,
|
||||||
pub rel: RelTag,
|
pub rel: RelTag,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
// FIXME: should move 'forknum' as last field to keep this consistent with Postgres.
|
// FIXME: should move 'forknum' as last field to keep this consistent with Postgres.
|
||||||
// Then we could replace the custom Ord and PartialOrd implementations below with
|
// Then we could replace the custom Ord and PartialOrd implementations below with
|
||||||
// deriving them. This will require changes in walredoproc.c.
|
// deriving them. This will require changes in walredoproc.c.
|
||||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)]
|
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||||
pub struct RelTag {
|
pub struct RelTag {
|
||||||
pub forknum: u8,
|
pub forknum: u8,
|
||||||
pub spcnode: Oid,
|
pub spcnode: Oid,
|
||||||
@@ -184,12 +184,12 @@ pub enum SlruKind {
|
|||||||
MultiXactOffsets,
|
MultiXactOffsets,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SlruKind {
|
impl fmt::Display for SlruKind {
|
||||||
pub fn to_str(&self) -> &'static str {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Clog => "pg_xact",
|
Self::Clog => write!(f, "pg_xact"),
|
||||||
Self::MultiXactMembers => "pg_multixact/members",
|
Self::MultiXactMembers => write!(f, "pg_multixact/members"),
|
||||||
Self::MultiXactOffsets => "pg_multixact/offsets",
|
Self::MultiXactOffsets => write!(f, "pg_multixact/offsets"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use arc_swap::ArcSwap;
|
|||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::{Instrument, info_span};
|
use tracing::{Instrument, info_span};
|
||||||
|
|
||||||
use crate::{FeatureStore, PostHogClient, PostHogClientConfig};
|
use crate::{CaptureEvent, FeatureStore, PostHogClient, PostHogClientConfig};
|
||||||
|
|
||||||
/// A background loop that fetches feature flags from PostHog and updates the feature store.
|
/// A background loop that fetches feature flags from PostHog and updates the feature store.
|
||||||
pub struct FeatureResolverBackgroundLoop {
|
pub struct FeatureResolverBackgroundLoop {
|
||||||
@@ -24,9 +24,16 @@ impl FeatureResolverBackgroundLoop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn(self: Arc<Self>, handle: &tokio::runtime::Handle, refresh_period: Duration) {
|
pub fn spawn(
|
||||||
|
self: Arc<Self>,
|
||||||
|
handle: &tokio::runtime::Handle,
|
||||||
|
refresh_period: Duration,
|
||||||
|
fake_tenants: Vec<CaptureEvent>,
|
||||||
|
) {
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
let cancel = self.cancel.clone();
|
let cancel = self.cancel.clone();
|
||||||
|
|
||||||
|
// Main loop of updating the feature flags.
|
||||||
handle.spawn(
|
handle.spawn(
|
||||||
async move {
|
async move {
|
||||||
tracing::info!("Starting PostHog feature resolver");
|
tracing::info!("Starting PostHog feature resolver");
|
||||||
@@ -56,6 +63,22 @@ impl FeatureResolverBackgroundLoop {
|
|||||||
}
|
}
|
||||||
.instrument(info_span!("posthog_feature_resolver")),
|
.instrument(info_span!("posthog_feature_resolver")),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Report fake tenants to PostHog so that we have the combination of all the properties in the UI.
|
||||||
|
// Do one report per pageserver restart.
|
||||||
|
let this = self.clone();
|
||||||
|
handle.spawn(
|
||||||
|
async move {
|
||||||
|
tracing::info!("Starting PostHog feature reporter");
|
||||||
|
for tenant in &fake_tenants {
|
||||||
|
tracing::info!("Reporting fake tenant: {:?}", tenant);
|
||||||
|
}
|
||||||
|
if let Err(e) = this.posthog_client.capture_event_batch(&fake_tenants).await {
|
||||||
|
tracing::warn!("Cannot report fake tenants: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.instrument(info_span!("posthog_feature_reporter")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn feature_store(&self) -> Arc<FeatureStore> {
|
pub fn feature_store(&self) -> Arc<FeatureStore> {
|
||||||
|
|||||||
@@ -22,6 +22,16 @@ pub enum PostHogEvaluationError {
|
|||||||
Internal(String),
|
Internal(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PostHogEvaluationError {
|
||||||
|
pub fn as_variant_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
PostHogEvaluationError::NotAvailable(_) => "not_available",
|
||||||
|
PostHogEvaluationError::NoConditionGroupMatched => "no_condition_group_matched",
|
||||||
|
PostHogEvaluationError::Internal(_) => "internal",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct LocalEvaluationResponse {
|
pub struct LocalEvaluationResponse {
|
||||||
pub flags: Vec<LocalEvaluationFlag>,
|
pub flags: Vec<LocalEvaluationFlag>,
|
||||||
@@ -54,7 +64,7 @@ pub struct LocalEvaluationFlagFilterProperty {
|
|||||||
operator: String,
|
operator: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum PostHogFlagFilterPropertyValue {
|
pub enum PostHogFlagFilterPropertyValue {
|
||||||
String(String),
|
String(String),
|
||||||
@@ -497,6 +507,13 @@ pub struct PostHogClient {
|
|||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
pub struct CaptureEvent {
|
||||||
|
pub event: String,
|
||||||
|
pub distinct_id: String,
|
||||||
|
pub properties: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
impl PostHogClient {
|
impl PostHogClient {
|
||||||
pub fn new(config: PostHogClientConfig) -> Self {
|
pub fn new(config: PostHogClientConfig) -> Self {
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
@@ -560,12 +577,12 @@ impl PostHogClient {
|
|||||||
&self,
|
&self,
|
||||||
event: &str,
|
event: &str,
|
||||||
distinct_id: &str,
|
distinct_id: &str,
|
||||||
properties: &HashMap<String, PostHogFlagFilterPropertyValue>,
|
properties: &serde_json::Value,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// PUBLIC_URL/capture/
|
// PUBLIC_URL/capture/
|
||||||
// with bearer token of self.client_api_key
|
|
||||||
let url = format!("{}/capture/", self.config.public_api_url);
|
let url = format!("{}/capture/", self.config.public_api_url);
|
||||||
self.client
|
let response = self
|
||||||
|
.client
|
||||||
.post(url)
|
.post(url)
|
||||||
.body(serde_json::to_string(&json!({
|
.body(serde_json::to_string(&json!({
|
||||||
"api_key": self.config.client_api_key,
|
"api_key": self.config.client_api_key,
|
||||||
@@ -575,6 +592,39 @@ impl PostHogClient {
|
|||||||
}))?)
|
}))?)
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
let status = response.status();
|
||||||
|
let body = response.text().await?;
|
||||||
|
if !status.is_success() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Failed to capture events: {}, {}",
|
||||||
|
status,
|
||||||
|
body
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn capture_event_batch(&self, events: &[CaptureEvent]) -> anyhow::Result<()> {
|
||||||
|
// PUBLIC_URL/batch/
|
||||||
|
let url = format!("{}/batch/", self.config.public_api_url);
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.post(url)
|
||||||
|
.body(serde_json::to_string(&json!({
|
||||||
|
"api_key": self.config.client_api_key,
|
||||||
|
"batch": events,
|
||||||
|
}))?)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
let status = response.status();
|
||||||
|
let body = response.text().await?;
|
||||||
|
if !status.is_success() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Failed to capture events: {}, {}",
|
||||||
|
status,
|
||||||
|
body
|
||||||
|
));
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use crate::{Error, cancel_query_raw, connect_socket};
|
|||||||
pub(crate) async fn cancel_query<T>(
|
pub(crate) async fn cancel_query<T>(
|
||||||
config: Option<SocketConfig>,
|
config: Option<SocketConfig>,
|
||||||
ssl_mode: SslMode,
|
ssl_mode: SslMode,
|
||||||
mut tls: T,
|
tls: T,
|
||||||
process_id: i32,
|
process_id: i32,
|
||||||
secret_key: i32,
|
secret_key: i32,
|
||||||
) -> Result<(), Error>
|
) -> Result<(), Error>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ use crate::{Client, Connection, Error};
|
|||||||
|
|
||||||
/// TLS configuration.
|
/// TLS configuration.
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum SslMode {
|
pub enum SslMode {
|
||||||
/// Do not use TLS.
|
/// Do not use TLS.
|
||||||
Disable,
|
Disable,
|
||||||
@@ -231,7 +230,7 @@ impl Config {
|
|||||||
/// Requires the `runtime` Cargo feature (enabled by default).
|
/// Requires the `runtime` Cargo feature (enabled by default).
|
||||||
pub async fn connect<T>(
|
pub async fn connect<T>(
|
||||||
&self,
|
&self,
|
||||||
tls: T,
|
tls: &T,
|
||||||
) -> Result<(Client, Connection<TcpStream, T::Stream>), Error>
|
) -> Result<(Client, Connection<TcpStream, T::Stream>), Error>
|
||||||
where
|
where
|
||||||
T: MakeTlsConnect<TcpStream>,
|
T: MakeTlsConnect<TcpStream>,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use crate::tls::{MakeTlsConnect, TlsConnect};
|
|||||||
use crate::{Client, Config, Connection, Error, RawConnection};
|
use crate::{Client, Config, Connection, Error, RawConnection};
|
||||||
|
|
||||||
pub async fn connect<T>(
|
pub async fn connect<T>(
|
||||||
mut tls: T,
|
tls: &T,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
) -> Result<(Client, Connection<TcpStream, T::Stream>), Error>
|
) -> Result<(Client, Connection<TcpStream, T::Stream>), Error>
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ pub trait MakeTlsConnect<S> {
|
|||||||
/// Creates a new `TlsConnect`or.
|
/// Creates a new `TlsConnect`or.
|
||||||
///
|
///
|
||||||
/// The domain name is provided for certificate verification and SNI.
|
/// The domain name is provided for certificate verification and SNI.
|
||||||
fn make_tls_connect(&mut self, domain: &str) -> Result<Self::TlsConnect, Self::Error>;
|
fn make_tls_connect(&self, domain: &str) -> Result<Self::TlsConnect, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An asynchronous function wrapping a stream in a TLS session.
|
/// An asynchronous function wrapping a stream in a TLS session.
|
||||||
@@ -85,7 +85,7 @@ impl<S> MakeTlsConnect<S> for NoTls {
|
|||||||
type TlsConnect = NoTls;
|
type TlsConnect = NoTls;
|
||||||
type Error = NoTlsError;
|
type Error = NoTlsError;
|
||||||
|
|
||||||
fn make_tls_connect(&mut self, _: &str) -> Result<NoTls, NoTlsError> {
|
fn make_tls_connect(&self, _: &str) -> Result<NoTls, NoTlsError> {
|
||||||
Ok(NoTls)
|
Ok(NoTls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use std::sync::Arc;
|
|||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
use std::{env, io};
|
use std::{env, io};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use azure_core::request_options::{IfMatchCondition, MaxResults, Metadata, Range};
|
use azure_core::request_options::{IfMatchCondition, MaxResults, Metadata, Range};
|
||||||
use azure_core::{Continuable, HttpClient, RetryOptions, TransportOptions};
|
use azure_core::{Continuable, HttpClient, RetryOptions, TransportOptions};
|
||||||
use azure_storage::StorageCredentials;
|
use azure_storage::StorageCredentials;
|
||||||
@@ -37,6 +37,7 @@ use crate::metrics::{AttemptOutcome, RequestKind, start_measuring_requests};
|
|||||||
use crate::{
|
use crate::{
|
||||||
ConcurrencyLimiter, Download, DownloadError, DownloadKind, DownloadOpts, Listing, ListingMode,
|
ConcurrencyLimiter, Download, DownloadError, DownloadKind, DownloadOpts, Listing, ListingMode,
|
||||||
ListingObject, RemotePath, RemoteStorage, StorageMetadata, TimeTravelError, TimeoutOrCancel,
|
ListingObject, RemotePath, RemoteStorage, StorageMetadata, TimeTravelError, TimeoutOrCancel,
|
||||||
|
Version, VersionKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct AzureBlobStorage {
|
pub struct AzureBlobStorage {
|
||||||
@@ -405,6 +406,39 @@ impl AzureBlobStorage {
|
|||||||
pub fn container_name(&self) -> &str {
|
pub fn container_name(&self) -> &str {
|
||||||
&self.container_name
|
&self.container_name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_versions_with_permit(
|
||||||
|
&self,
|
||||||
|
_permit: &tokio::sync::SemaphorePermit<'_>,
|
||||||
|
prefix: Option<&RemotePath>,
|
||||||
|
mode: ListingMode,
|
||||||
|
max_keys: Option<NonZeroU32>,
|
||||||
|
cancel: &CancellationToken,
|
||||||
|
) -> Result<crate::VersionListing, DownloadError> {
|
||||||
|
let customize_builder = |mut builder: ListBlobsBuilder| {
|
||||||
|
builder = builder.include_versions(true);
|
||||||
|
// We do not return this info back to `VersionListing` yet.
|
||||||
|
builder = builder.include_deleted(true);
|
||||||
|
builder
|
||||||
|
};
|
||||||
|
let kind = RequestKind::ListVersions;
|
||||||
|
|
||||||
|
let mut stream = std::pin::pin!(self.list_streaming_for_fn(
|
||||||
|
prefix,
|
||||||
|
mode,
|
||||||
|
max_keys,
|
||||||
|
cancel,
|
||||||
|
kind,
|
||||||
|
customize_builder
|
||||||
|
));
|
||||||
|
let mut combined: crate::VersionListing =
|
||||||
|
stream.next().await.expect("At least one item required")?;
|
||||||
|
while let Some(list) = stream.next().await {
|
||||||
|
let list = list?;
|
||||||
|
combined.versions.extend(list.versions.into_iter());
|
||||||
|
}
|
||||||
|
Ok(combined)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait ListingCollector {
|
trait ListingCollector {
|
||||||
@@ -488,27 +522,10 @@ impl RemoteStorage for AzureBlobStorage {
|
|||||||
max_keys: Option<NonZeroU32>,
|
max_keys: Option<NonZeroU32>,
|
||||||
cancel: &CancellationToken,
|
cancel: &CancellationToken,
|
||||||
) -> std::result::Result<crate::VersionListing, DownloadError> {
|
) -> std::result::Result<crate::VersionListing, DownloadError> {
|
||||||
let customize_builder = |mut builder: ListBlobsBuilder| {
|
|
||||||
builder = builder.include_versions(true);
|
|
||||||
builder
|
|
||||||
};
|
|
||||||
let kind = RequestKind::ListVersions;
|
let kind = RequestKind::ListVersions;
|
||||||
|
let permit = self.permit(kind, cancel).await?;
|
||||||
let mut stream = std::pin::pin!(self.list_streaming_for_fn(
|
self.list_versions_with_permit(&permit, prefix, mode, max_keys, cancel)
|
||||||
prefix,
|
.await
|
||||||
mode,
|
|
||||||
max_keys,
|
|
||||||
cancel,
|
|
||||||
kind,
|
|
||||||
customize_builder
|
|
||||||
));
|
|
||||||
let mut combined: crate::VersionListing =
|
|
||||||
stream.next().await.expect("At least one item required")?;
|
|
||||||
while let Some(list) = stream.next().await {
|
|
||||||
let list = list?;
|
|
||||||
combined.versions.extend(list.versions.into_iter());
|
|
||||||
}
|
|
||||||
Ok(combined)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn head_object(
|
async fn head_object(
|
||||||
@@ -803,14 +820,158 @@ impl RemoteStorage for AzureBlobStorage {
|
|||||||
|
|
||||||
async fn time_travel_recover(
|
async fn time_travel_recover(
|
||||||
&self,
|
&self,
|
||||||
_prefix: Option<&RemotePath>,
|
prefix: Option<&RemotePath>,
|
||||||
_timestamp: SystemTime,
|
timestamp: SystemTime,
|
||||||
_done_if_after: SystemTime,
|
done_if_after: SystemTime,
|
||||||
_cancel: &CancellationToken,
|
cancel: &CancellationToken,
|
||||||
) -> Result<(), TimeTravelError> {
|
) -> Result<(), TimeTravelError> {
|
||||||
// TODO use Azure point in time recovery feature for this
|
let msg = "PLEASE NOTE: Azure Blob storage time-travel recovery may not work as expected "
|
||||||
// https://learn.microsoft.com/en-us/azure/storage/blobs/point-in-time-restore-overview
|
.to_string()
|
||||||
Err(TimeTravelError::Unimplemented)
|
+ "for some specific files. If a file gets deleted but then overwritten and we want to recover "
|
||||||
|
+ "to the time during the file was not present, this functionality will recover the file. Only "
|
||||||
|
+ "use the functionality for services that can tolerate this. For example, recovering a state of the "
|
||||||
|
+ "pageserver tenants.";
|
||||||
|
tracing::error!("{}", msg);
|
||||||
|
|
||||||
|
let kind = RequestKind::TimeTravel;
|
||||||
|
let permit = self.permit(kind, cancel).await?;
|
||||||
|
|
||||||
|
let mode = ListingMode::NoDelimiter;
|
||||||
|
let version_listing = self
|
||||||
|
.list_versions_with_permit(&permit, prefix, mode, None, cancel)
|
||||||
|
.await
|
||||||
|
.map_err(|err| match err {
|
||||||
|
DownloadError::Other(e) => TimeTravelError::Other(e),
|
||||||
|
DownloadError::Cancelled => TimeTravelError::Cancelled,
|
||||||
|
other => TimeTravelError::Other(other.into()),
|
||||||
|
})?;
|
||||||
|
let versions_and_deletes = version_listing.versions;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"Built list for time travel with {} versions and deletions",
|
||||||
|
versions_and_deletes.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Work on the list of references instead of the objects directly,
|
||||||
|
// otherwise we get lifetime errors in the sort_by_key call below.
|
||||||
|
let mut versions_and_deletes = versions_and_deletes.iter().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
versions_and_deletes.sort_by_key(|vd| (&vd.key, &vd.last_modified));
|
||||||
|
|
||||||
|
let mut vds_for_key = HashMap::<_, Vec<_>>::new();
|
||||||
|
|
||||||
|
for vd in &versions_and_deletes {
|
||||||
|
let Version { key, .. } = &vd;
|
||||||
|
let version_id = vd.version_id().map(|v| v.0.as_str());
|
||||||
|
if version_id == Some("null") {
|
||||||
|
return Err(TimeTravelError::Other(anyhow!(
|
||||||
|
"Received ListVersions response for key={key} with version_id='null', \
|
||||||
|
indicating either disabled versioning, or legacy objects with null version id values"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
tracing::trace!("Parsing version key={key} kind={:?}", vd.kind);
|
||||||
|
|
||||||
|
vds_for_key.entry(key).or_default().push(vd);
|
||||||
|
}
|
||||||
|
|
||||||
|
let warn_threshold = 3;
|
||||||
|
let max_retries = 10;
|
||||||
|
let is_permanent = |e: &_| matches!(e, TimeTravelError::Cancelled);
|
||||||
|
|
||||||
|
for (key, versions) in vds_for_key {
|
||||||
|
let last_vd = versions.last().unwrap();
|
||||||
|
let key = self.relative_path_to_name(key);
|
||||||
|
if last_vd.last_modified > done_if_after {
|
||||||
|
tracing::debug!("Key {key} has version later than done_if_after, skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// the version we want to restore to.
|
||||||
|
let version_to_restore_to =
|
||||||
|
match versions.binary_search_by_key(×tamp, |tpl| tpl.last_modified) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => e,
|
||||||
|
};
|
||||||
|
if version_to_restore_to == versions.len() {
|
||||||
|
tracing::debug!("Key {key} has no changes since timestamp, skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut do_delete = false;
|
||||||
|
if version_to_restore_to == 0 {
|
||||||
|
// All versions more recent, so the key didn't exist at the specified time point.
|
||||||
|
tracing::debug!(
|
||||||
|
"All {} versions more recent for {key}, deleting",
|
||||||
|
versions.len()
|
||||||
|
);
|
||||||
|
do_delete = true;
|
||||||
|
} else {
|
||||||
|
match &versions[version_to_restore_to - 1] {
|
||||||
|
Version {
|
||||||
|
kind: VersionKind::Version(version_id),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let source_url = format!(
|
||||||
|
"{}/{}?versionid={}",
|
||||||
|
self.client
|
||||||
|
.url()
|
||||||
|
.map_err(|e| TimeTravelError::Other(anyhow!("{e}")))?,
|
||||||
|
key,
|
||||||
|
version_id.0
|
||||||
|
);
|
||||||
|
tracing::debug!(
|
||||||
|
"Promoting old version {} for {key} at {}...",
|
||||||
|
version_id.0,
|
||||||
|
source_url
|
||||||
|
);
|
||||||
|
backoff::retry(
|
||||||
|
|| async {
|
||||||
|
let blob_client = self.client.blob_client(key.clone());
|
||||||
|
let op = blob_client.copy(Url::from_str(&source_url).unwrap());
|
||||||
|
tokio::select! {
|
||||||
|
res = op => res.map_err(|e| TimeTravelError::Other(e.into())),
|
||||||
|
_ = cancel.cancelled() => Err(TimeTravelError::Cancelled),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
is_permanent,
|
||||||
|
warn_threshold,
|
||||||
|
max_retries,
|
||||||
|
"copying object version for time_travel_recover",
|
||||||
|
cancel,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| TimeTravelError::Cancelled)
|
||||||
|
.and_then(|x| x)?;
|
||||||
|
tracing::info!(?version_id, %key, "Copied old version in Azure blob storage");
|
||||||
|
}
|
||||||
|
Version {
|
||||||
|
kind: VersionKind::DeletionMarker,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
do_delete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if do_delete {
|
||||||
|
if matches!(last_vd.kind, VersionKind::DeletionMarker) {
|
||||||
|
// Key has since been deleted (but there was some history), no need to do anything
|
||||||
|
tracing::debug!("Key {key} already deleted, skipping.");
|
||||||
|
} else {
|
||||||
|
tracing::debug!("Deleting {key}...");
|
||||||
|
|
||||||
|
self.delete(&RemotePath::from_string(&key).unwrap(), cancel)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
// delete_oid0 will use TimeoutOrCancel
|
||||||
|
if TimeoutOrCancel::caused_by_cancel(&e) {
|
||||||
|
TimeTravelError::Cancelled
|
||||||
|
} else {
|
||||||
|
TimeTravelError::Other(e)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1022,6 +1022,7 @@ impl RemoteStorage for S3Bucket {
|
|||||||
let Version { key, .. } = &vd;
|
let Version { key, .. } = &vd;
|
||||||
let version_id = vd.version_id().map(|v| v.0.as_str());
|
let version_id = vd.version_id().map(|v| v.0.as_str());
|
||||||
if version_id == Some("null") {
|
if version_id == Some("null") {
|
||||||
|
// TODO: check the behavior of using the SDK on a non-versioned container
|
||||||
return Err(TimeTravelError::Other(anyhow!(
|
return Err(TimeTravelError::Other(anyhow!(
|
||||||
"Received ListVersions response for key={key} with version_id='null', \
|
"Received ListVersions response for key={key} with version_id='null', \
|
||||||
indicating either disabled versioning, or legacy objects with null version id values"
|
indicating either disabled versioning, or legacy objects with null version id values"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use utils::pageserver_feedback::PageserverFeedback;
|
|||||||
use crate::membership::Configuration;
|
use crate::membership::Configuration;
|
||||||
use crate::{ServerInfo, Term};
|
use crate::{ServerInfo, Term};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct SafekeeperStatus {
|
pub struct SafekeeperStatus {
|
||||||
pub id: NodeId,
|
pub id: NodeId,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ use std::time::Duration;
|
|||||||
use tokio::sync::Notify;
|
use tokio::sync::Notify;
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
pub struct LeakyBucketConfig {
|
pub struct LeakyBucketConfig {
|
||||||
/// This is the "time cost" of a single request unit.
|
/// This is the "time cost" of a single request unit.
|
||||||
/// Should loosely represent how long it takes to handle a request unit in active resource time.
|
/// Should loosely represent how long it takes to handle a request unit in active resource time.
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ pub mod error;
|
|||||||
/// async timeout helper
|
/// async timeout helper
|
||||||
pub mod timeout;
|
pub mod timeout;
|
||||||
|
|
||||||
|
pub mod span;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
|
|
||||||
pub mod failpoint_support;
|
pub mod failpoint_support;
|
||||||
|
|||||||
19
libs/utils/src/span.rs
Normal file
19
libs/utils/src/span.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//! Tracing span helpers.
|
||||||
|
|
||||||
|
/// Records the given fields in the current span, as a single call. The fields must already have
|
||||||
|
/// been declared for the span (typically with empty values).
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! span_record {
|
||||||
|
($($tokens:tt)*) => {$crate::span_record_in!(::tracing::Span::current(), $($tokens)*)};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records the given fields in the given span, as a single call. The fields must already have been
|
||||||
|
/// declared for the span (typically with empty values).
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! span_record_in {
|
||||||
|
($span:expr, $($tokens:tt)*) => {
|
||||||
|
if let Some(meta) = $span.metadata() {
|
||||||
|
$span.record_all(&tracing::valueset!(meta.fields(), $($tokens)*));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -439,6 +439,7 @@ pub fn empty_shmem() -> crate::bindings::WalproposerShmemState {
|
|||||||
currentClusterSize: crate::bindings::pg_atomic_uint64 { value: 0 },
|
currentClusterSize: crate::bindings::pg_atomic_uint64 { value: 0 },
|
||||||
shard_ps_feedback: [empty_feedback; 128],
|
shard_ps_feedback: [empty_feedback; 128],
|
||||||
num_shards: 0,
|
num_shards: 0,
|
||||||
|
replica_promote: false,
|
||||||
min_ps_feedback: empty_feedback,
|
min_ps_feedback: empty_feedback,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ fail.workspace = true
|
|||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
hashlink.workspace = true
|
hashlink.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
|
http.workspace = true
|
||||||
http-utils.workspace = true
|
http-utils.workspace = true
|
||||||
humantime-serde.workspace = true
|
humantime-serde.workspace = true
|
||||||
humantime.workspace = true
|
humantime.workspace = true
|
||||||
@@ -93,6 +94,7 @@ tokio-util.workspace = true
|
|||||||
toml_edit = { workspace = true, features = [ "serde" ] }
|
toml_edit = { workspace = true, features = [ "serde" ] }
|
||||||
tonic.workspace = true
|
tonic.workspace = true
|
||||||
tonic-reflection.workspace = true
|
tonic-reflection.workspace = true
|
||||||
|
tower.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-utils.workspace = true
|
tracing-utils.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ bytes.workspace = true
|
|||||||
pageserver_api.workspace = true
|
pageserver_api.workspace = true
|
||||||
postgres_ffi.workspace = true
|
postgres_ffi.workspace = true
|
||||||
prost.workspace = true
|
prost.workspace = true
|
||||||
smallvec.workspace = true
|
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tonic.workspace = true
|
tonic.workspace = true
|
||||||
utils.workspace = true
|
utils.workspace = true
|
||||||
|
|||||||
@@ -9,10 +9,16 @@
|
|||||||
//! - Use more precise datatypes, e.g. Lsn and uints shorter than 32 bits.
|
//! - Use more precise datatypes, e.g. Lsn and uints shorter than 32 bits.
|
||||||
//!
|
//!
|
||||||
//! - Validate protocol invariants, via try_from() and try_into().
|
//! - Validate protocol invariants, via try_from() and try_into().
|
||||||
|
//!
|
||||||
|
//! Validation only happens on the receiver side, i.e. when converting from Protobuf to domain
|
||||||
|
//! types. This is where it matters -- the Protobuf types are less strict than the domain types, and
|
||||||
|
//! receivers should expect all sorts of junk from senders. This also allows the sender to use e.g.
|
||||||
|
//! stream combinators without dealing with errors, and avoids validating the same message twice.
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use postgres_ffi::Oid;
|
use postgres_ffi::Oid;
|
||||||
use smallvec::SmallVec;
|
|
||||||
// TODO: split out Lsn, RelTag, SlruKind, Oid and other basic types to a separate crate, to avoid
|
// TODO: split out Lsn, RelTag, SlruKind, Oid and other basic types to a separate crate, to avoid
|
||||||
// pulling in all of their other crate dependencies when building the client.
|
// pulling in all of their other crate dependencies when building the client.
|
||||||
use utils::lsn::Lsn;
|
use utils::lsn::Lsn;
|
||||||
@@ -48,7 +54,8 @@ pub struct ReadLsn {
|
|||||||
pub request_lsn: Lsn,
|
pub request_lsn: Lsn,
|
||||||
/// If given, the caller guarantees that the page has not been modified since this LSN. Must be
|
/// If given, the caller guarantees that the page has not been modified since this LSN. Must be
|
||||||
/// smaller than or equal to request_lsn. This allows the Pageserver to serve an old page
|
/// smaller than or equal to request_lsn. This allows the Pageserver to serve an old page
|
||||||
/// without waiting for the request LSN to arrive. Valid for all request types.
|
/// without waiting for the request LSN to arrive. If not given, the request will read at the
|
||||||
|
/// request_lsn and wait for it to arrive if necessary. Valid for all request types.
|
||||||
///
|
///
|
||||||
/// It is undefined behaviour to make a request such that the page was, in fact, modified
|
/// It is undefined behaviour to make a request such that the page was, in fact, modified
|
||||||
/// between request_lsn and not_modified_since_lsn. The Pageserver might detect it and return an
|
/// between request_lsn and not_modified_since_lsn. The Pageserver might detect it and return an
|
||||||
@@ -58,19 +65,14 @@ pub struct ReadLsn {
|
|||||||
pub not_modified_since_lsn: Option<Lsn>,
|
pub not_modified_since_lsn: Option<Lsn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadLsn {
|
impl Display for ReadLsn {
|
||||||
/// Validates the ReadLsn.
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
pub fn validate(&self) -> Result<(), ProtocolError> {
|
let req_lsn = self.request_lsn;
|
||||||
if self.request_lsn == Lsn::INVALID {
|
if let Some(mod_lsn) = self.not_modified_since_lsn {
|
||||||
return Err(ProtocolError::invalid("request_lsn", self.request_lsn));
|
write!(f, "{req_lsn}>={mod_lsn}")
|
||||||
|
} else {
|
||||||
|
req_lsn.fmt(f)
|
||||||
}
|
}
|
||||||
if self.not_modified_since_lsn > Some(self.request_lsn) {
|
|
||||||
return Err(ProtocolError::invalid(
|
|
||||||
"not_modified_since_lsn",
|
|
||||||
self.not_modified_since_lsn,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,27 +80,31 @@ impl TryFrom<proto::ReadLsn> for ReadLsn {
|
|||||||
type Error = ProtocolError;
|
type Error = ProtocolError;
|
||||||
|
|
||||||
fn try_from(pb: proto::ReadLsn) -> Result<Self, Self::Error> {
|
fn try_from(pb: proto::ReadLsn) -> Result<Self, Self::Error> {
|
||||||
let read_lsn = Self {
|
if pb.request_lsn == 0 {
|
||||||
|
return Err(ProtocolError::invalid("request_lsn", pb.request_lsn));
|
||||||
|
}
|
||||||
|
if pb.not_modified_since_lsn > pb.request_lsn {
|
||||||
|
return Err(ProtocolError::invalid(
|
||||||
|
"not_modified_since_lsn",
|
||||||
|
pb.not_modified_since_lsn,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
request_lsn: Lsn(pb.request_lsn),
|
request_lsn: Lsn(pb.request_lsn),
|
||||||
not_modified_since_lsn: match pb.not_modified_since_lsn {
|
not_modified_since_lsn: match pb.not_modified_since_lsn {
|
||||||
0 => None,
|
0 => None,
|
||||||
lsn => Some(Lsn(lsn)),
|
lsn => Some(Lsn(lsn)),
|
||||||
},
|
},
|
||||||
};
|
})
|
||||||
read_lsn.validate()?;
|
|
||||||
Ok(read_lsn)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<ReadLsn> for proto::ReadLsn {
|
impl From<ReadLsn> for proto::ReadLsn {
|
||||||
type Error = ProtocolError;
|
fn from(read_lsn: ReadLsn) -> Self {
|
||||||
|
Self {
|
||||||
fn try_from(read_lsn: ReadLsn) -> Result<Self, Self::Error> {
|
|
||||||
read_lsn.validate()?;
|
|
||||||
Ok(Self {
|
|
||||||
request_lsn: read_lsn.request_lsn.0,
|
request_lsn: read_lsn.request_lsn.0,
|
||||||
not_modified_since_lsn: read_lsn.not_modified_since_lsn.unwrap_or_default().0,
|
not_modified_since_lsn: read_lsn.not_modified_since_lsn.unwrap_or_default().0,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +159,15 @@ impl TryFrom<proto::CheckRelExistsRequest> for CheckRelExistsRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<CheckRelExistsRequest> for proto::CheckRelExistsRequest {
|
||||||
|
fn from(request: CheckRelExistsRequest) -> Self {
|
||||||
|
Self {
|
||||||
|
read_lsn: Some(request.read_lsn.into()),
|
||||||
|
rel: Some(request.rel.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type CheckRelExistsResponse = bool;
|
pub type CheckRelExistsResponse = bool;
|
||||||
|
|
||||||
impl From<proto::CheckRelExistsResponse> for CheckRelExistsResponse {
|
impl From<proto::CheckRelExistsResponse> for CheckRelExistsResponse {
|
||||||
@@ -190,14 +205,12 @@ impl TryFrom<proto::GetBaseBackupRequest> for GetBaseBackupRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<GetBaseBackupRequest> for proto::GetBaseBackupRequest {
|
impl From<GetBaseBackupRequest> for proto::GetBaseBackupRequest {
|
||||||
type Error = ProtocolError;
|
fn from(request: GetBaseBackupRequest) -> Self {
|
||||||
|
Self {
|
||||||
fn try_from(request: GetBaseBackupRequest) -> Result<Self, Self::Error> {
|
read_lsn: Some(request.read_lsn.into()),
|
||||||
Ok(Self {
|
|
||||||
read_lsn: Some(request.read_lsn.try_into()?),
|
|
||||||
replica: request.replica,
|
replica: request.replica,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,14 +227,9 @@ impl TryFrom<proto::GetBaseBackupResponseChunk> for GetBaseBackupResponseChunk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<GetBaseBackupResponseChunk> for proto::GetBaseBackupResponseChunk {
|
impl From<GetBaseBackupResponseChunk> for proto::GetBaseBackupResponseChunk {
|
||||||
type Error = ProtocolError;
|
fn from(chunk: GetBaseBackupResponseChunk) -> Self {
|
||||||
|
Self { chunk }
|
||||||
fn try_from(chunk: GetBaseBackupResponseChunk) -> Result<Self, Self::Error> {
|
|
||||||
if chunk.is_empty() {
|
|
||||||
return Err(ProtocolError::Missing("chunk"));
|
|
||||||
}
|
|
||||||
Ok(Self { chunk })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,14 +254,12 @@ impl TryFrom<proto::GetDbSizeRequest> for GetDbSizeRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<GetDbSizeRequest> for proto::GetDbSizeRequest {
|
impl From<GetDbSizeRequest> for proto::GetDbSizeRequest {
|
||||||
type Error = ProtocolError;
|
fn from(request: GetDbSizeRequest) -> Self {
|
||||||
|
Self {
|
||||||
fn try_from(request: GetDbSizeRequest) -> Result<Self, Self::Error> {
|
read_lsn: Some(request.read_lsn.into()),
|
||||||
Ok(Self {
|
|
||||||
read_lsn: Some(request.read_lsn.try_into()?),
|
|
||||||
db_oid: request.db_oid,
|
db_oid: request.db_oid,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +294,7 @@ pub struct GetPageRequest {
|
|||||||
/// Multiple pages will be executed as a single batch by the Pageserver, amortizing layer access
|
/// Multiple pages will be executed as a single batch by the Pageserver, amortizing layer access
|
||||||
/// costs and parallelizing them. This may increase the latency of any individual request, but
|
/// costs and parallelizing them. This may increase the latency of any individual request, but
|
||||||
/// improves the overall latency and throughput of the batch as a whole.
|
/// improves the overall latency and throughput of the batch as a whole.
|
||||||
pub block_numbers: SmallVec<[u32; 1]>,
|
pub block_numbers: Vec<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<proto::GetPageRequest> for GetPageRequest {
|
impl TryFrom<proto::GetPageRequest> for GetPageRequest {
|
||||||
@@ -306,25 +312,20 @@ impl TryFrom<proto::GetPageRequest> for GetPageRequest {
|
|||||||
.ok_or(ProtocolError::Missing("read_lsn"))?
|
.ok_or(ProtocolError::Missing("read_lsn"))?
|
||||||
.try_into()?,
|
.try_into()?,
|
||||||
rel: pb.rel.ok_or(ProtocolError::Missing("rel"))?.try_into()?,
|
rel: pb.rel.ok_or(ProtocolError::Missing("rel"))?.try_into()?,
|
||||||
block_numbers: pb.block_number.into(),
|
block_numbers: pb.block_number,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<GetPageRequest> for proto::GetPageRequest {
|
impl From<GetPageRequest> for proto::GetPageRequest {
|
||||||
type Error = ProtocolError;
|
fn from(request: GetPageRequest) -> Self {
|
||||||
|
Self {
|
||||||
fn try_from(request: GetPageRequest) -> Result<Self, Self::Error> {
|
|
||||||
if request.block_numbers.is_empty() {
|
|
||||||
return Err(ProtocolError::Missing("block_number"));
|
|
||||||
}
|
|
||||||
Ok(Self {
|
|
||||||
request_id: request.request_id,
|
request_id: request.request_id,
|
||||||
request_class: request.request_class.into(),
|
request_class: request.request_class.into(),
|
||||||
read_lsn: Some(request.read_lsn.try_into()?),
|
read_lsn: Some(request.read_lsn.into()),
|
||||||
rel: Some(request.rel.into()),
|
rel: Some(request.rel.into()),
|
||||||
block_number: request.block_numbers.into_vec(),
|
block_number: request.block_numbers,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,7 +397,7 @@ pub struct GetPageResponse {
|
|||||||
/// A string describing the status, if any.
|
/// A string describing the status, if any.
|
||||||
pub reason: Option<String>,
|
pub reason: Option<String>,
|
||||||
/// The 8KB page images, in the same order as the request. Empty if status != OK.
|
/// The 8KB page images, in the same order as the request. Empty if status != OK.
|
||||||
pub page_images: SmallVec<[Bytes; 1]>,
|
pub page_images: Vec<Bytes>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<proto::GetPageResponse> for GetPageResponse {
|
impl From<proto::GetPageResponse> for GetPageResponse {
|
||||||
@@ -405,7 +406,7 @@ impl From<proto::GetPageResponse> for GetPageResponse {
|
|||||||
request_id: pb.request_id,
|
request_id: pb.request_id,
|
||||||
status_code: pb.status_code.into(),
|
status_code: pb.status_code.into(),
|
||||||
reason: Some(pb.reason).filter(|r| !r.is_empty()),
|
reason: Some(pb.reason).filter(|r| !r.is_empty()),
|
||||||
page_images: pb.page_image.into(),
|
page_images: pb.page_image,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,7 +417,7 @@ impl From<GetPageResponse> for proto::GetPageResponse {
|
|||||||
request_id: response.request_id,
|
request_id: response.request_id,
|
||||||
status_code: response.status_code.into(),
|
status_code: response.status_code.into(),
|
||||||
reason: response.reason.unwrap_or_default(),
|
reason: response.reason.unwrap_or_default(),
|
||||||
page_image: response.page_images.into_vec(),
|
page_image: response.page_images,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -505,14 +506,12 @@ impl TryFrom<proto::GetRelSizeRequest> for GetRelSizeRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<GetRelSizeRequest> for proto::GetRelSizeRequest {
|
impl From<GetRelSizeRequest> for proto::GetRelSizeRequest {
|
||||||
type Error = ProtocolError;
|
fn from(request: GetRelSizeRequest) -> Self {
|
||||||
|
Self {
|
||||||
fn try_from(request: GetRelSizeRequest) -> Result<Self, Self::Error> {
|
read_lsn: Some(request.read_lsn.into()),
|
||||||
Ok(Self {
|
|
||||||
read_lsn: Some(request.read_lsn.try_into()?),
|
|
||||||
rel: Some(request.rel.into()),
|
rel: Some(request.rel.into()),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,15 +554,13 @@ impl TryFrom<proto::GetSlruSegmentRequest> for GetSlruSegmentRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<GetSlruSegmentRequest> for proto::GetSlruSegmentRequest {
|
impl From<GetSlruSegmentRequest> for proto::GetSlruSegmentRequest {
|
||||||
type Error = ProtocolError;
|
fn from(request: GetSlruSegmentRequest) -> Self {
|
||||||
|
Self {
|
||||||
fn try_from(request: GetSlruSegmentRequest) -> Result<Self, Self::Error> {
|
read_lsn: Some(request.read_lsn.into()),
|
||||||
Ok(Self {
|
|
||||||
read_lsn: Some(request.read_lsn.try_into()?),
|
|
||||||
kind: request.kind as u32,
|
kind: request.kind as u32,
|
||||||
segno: request.segno,
|
segno: request.segno,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,14 +577,9 @@ impl TryFrom<proto::GetSlruSegmentResponse> for GetSlruSegmentResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<GetSlruSegmentResponse> for proto::GetSlruSegmentResponse {
|
impl From<GetSlruSegmentResponse> for proto::GetSlruSegmentResponse {
|
||||||
type Error = ProtocolError;
|
fn from(segment: GetSlruSegmentResponse) -> Self {
|
||||||
|
Self { segment }
|
||||||
fn try_from(segment: GetSlruSegmentResponse) -> Result<Self, Self::Error> {
|
|
||||||
if segment.is_empty() {
|
|
||||||
return Err(ProtocolError::Missing("segment"));
|
|
||||||
}
|
|
||||||
Ok(Self { segment })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ license.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
async-trait.workspace = true
|
||||||
|
bytes.workspace = true
|
||||||
camino.workspace = true
|
camino.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
@@ -15,14 +17,17 @@ hdrhistogram.workspace = true
|
|||||||
humantime.workspace = true
|
humantime.workspace = true
|
||||||
humantime-serde.workspace = true
|
humantime-serde.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
reqwest.workspace=true
|
reqwest.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
tokio-stream.workspace = true
|
||||||
tokio-util.workspace = true
|
tokio-util.workspace = true
|
||||||
|
tonic.workspace = true
|
||||||
|
|
||||||
pageserver_client.workspace = true
|
pageserver_client.workspace = true
|
||||||
pageserver_api.workspace = true
|
pageserver_api.workspace = true
|
||||||
|
pageserver_page_api.workspace = true
|
||||||
utils = { path = "../../libs/utils/" }
|
utils = { path = "../../libs/utils/" }
|
||||||
workspace_hack = { version = "0.1", path = "../../workspace_hack" }
|
workspace_hack = { version = "0.1", path = "../../workspace_hack" }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::{HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
@@ -7,11 +7,15 @@ use std::sync::{Arc, Mutex};
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use bytes::Bytes;
|
||||||
use camino::Utf8PathBuf;
|
use camino::Utf8PathBuf;
|
||||||
use pageserver_api::key::Key;
|
use pageserver_api::key::Key;
|
||||||
use pageserver_api::keyspace::KeySpaceAccum;
|
use pageserver_api::keyspace::KeySpaceAccum;
|
||||||
use pageserver_api::models::{PagestreamGetPageRequest, PagestreamRequest};
|
use pageserver_api::models::{PagestreamGetPageRequest, PagestreamRequest};
|
||||||
|
use pageserver_api::reltag::RelTag;
|
||||||
use pageserver_api::shard::TenantShardId;
|
use pageserver_api::shard::TenantShardId;
|
||||||
|
use pageserver_page_api::proto;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
@@ -22,6 +26,12 @@ use utils::lsn::Lsn;
|
|||||||
use crate::util::tokio_thread_local_stats::AllThreadLocalStats;
|
use crate::util::tokio_thread_local_stats::AllThreadLocalStats;
|
||||||
use crate::util::{request_stats, tokio_thread_local_stats};
|
use crate::util::{request_stats, tokio_thread_local_stats};
|
||||||
|
|
||||||
|
#[derive(clap::ValueEnum, Clone, Debug)]
|
||||||
|
enum Protocol {
|
||||||
|
Libpq,
|
||||||
|
Grpc,
|
||||||
|
}
|
||||||
|
|
||||||
/// GetPage@LatestLSN, uniformly distributed across the compute-accessible keyspace.
|
/// GetPage@LatestLSN, uniformly distributed across the compute-accessible keyspace.
|
||||||
#[derive(clap::Parser)]
|
#[derive(clap::Parser)]
|
||||||
pub(crate) struct Args {
|
pub(crate) struct Args {
|
||||||
@@ -35,6 +45,8 @@ pub(crate) struct Args {
|
|||||||
num_clients: NonZeroUsize,
|
num_clients: NonZeroUsize,
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
runtime: Option<humantime::Duration>,
|
runtime: Option<humantime::Duration>,
|
||||||
|
#[clap(long, value_enum, default_value = "libpq")]
|
||||||
|
protocol: Protocol,
|
||||||
/// Each client sends requests at the given rate.
|
/// Each client sends requests at the given rate.
|
||||||
///
|
///
|
||||||
/// If a request takes too long and we should be issuing a new request already,
|
/// If a request takes too long and we should be issuing a new request already,
|
||||||
@@ -65,6 +77,16 @@ pub(crate) struct Args {
|
|||||||
#[clap(long, default_value = "1")]
|
#[clap(long, default_value = "1")]
|
||||||
queue_depth: NonZeroUsize,
|
queue_depth: NonZeroUsize,
|
||||||
|
|
||||||
|
/// Batch size of contiguous pages generated by each client. This is equivalent to how Postgres
|
||||||
|
/// will request page batches (e.g. prefetches or vectored reads). A batch counts as 1 RPS and
|
||||||
|
/// 1 queue depth.
|
||||||
|
///
|
||||||
|
/// The libpq protocol does not support client-side batching, and will submit batches as many
|
||||||
|
/// individual requests, in the hope that the server will batch them. Each batch still counts as
|
||||||
|
/// 1 RPS and 1 queue depth.
|
||||||
|
#[clap(long, default_value = "1")]
|
||||||
|
batch_size: NonZeroUsize,
|
||||||
|
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
only_relnode: Option<u32>,
|
only_relnode: Option<u32>,
|
||||||
|
|
||||||
@@ -303,7 +325,20 @@ async fn main_impl(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
client_libpq(args, worker_id, ss, cancel, rps_period, ranges, weights).await
|
let client: Box<dyn Client> = match args.protocol {
|
||||||
|
Protocol::Libpq => Box::new(
|
||||||
|
LibpqClient::new(args.page_service_connstring.clone(), worker_id.timeline)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
|
||||||
|
Protocol::Grpc => Box::new(
|
||||||
|
GrpcClient::new(args.page_service_connstring.clone(), worker_id.timeline)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
run_worker(args, client, ss, cancel, rps_period, ranges, weights).await
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -355,27 +390,28 @@ async fn main_impl(
|
|||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn client_libpq(
|
async fn run_worker(
|
||||||
args: &Args,
|
args: &Args,
|
||||||
worker_id: WorkerId,
|
mut client: Box<dyn Client>,
|
||||||
shared_state: Arc<SharedState>,
|
shared_state: Arc<SharedState>,
|
||||||
cancel: CancellationToken,
|
cancel: CancellationToken,
|
||||||
rps_period: Option<Duration>,
|
rps_period: Option<Duration>,
|
||||||
ranges: Vec<KeyRange>,
|
ranges: Vec<KeyRange>,
|
||||||
weights: rand::distributions::weighted::WeightedIndex<i128>,
|
weights: rand::distributions::weighted::WeightedIndex<i128>,
|
||||||
) {
|
) {
|
||||||
let client = pageserver_client::page_service::Client::new(args.page_service_connstring.clone())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let mut client = client
|
|
||||||
.pagestream(worker_id.timeline.tenant_id, worker_id.timeline.timeline_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
shared_state.start_work_barrier.wait().await;
|
shared_state.start_work_barrier.wait().await;
|
||||||
let client_start = Instant::now();
|
let client_start = Instant::now();
|
||||||
let mut ticks_processed = 0;
|
let mut ticks_processed = 0;
|
||||||
let mut inflight = VecDeque::new();
|
let mut req_id = 0;
|
||||||
|
let batch_size: usize = args.batch_size.into();
|
||||||
|
|
||||||
|
// Track inflight requests by request ID and start time. This times the request duration, and
|
||||||
|
// ensures responses match requests. We don't expect responses back in any particular order.
|
||||||
|
//
|
||||||
|
// NB: this does not check that all requests received a response, because we don't wait for the
|
||||||
|
// inflight requests to complete when the duration elapses.
|
||||||
|
let mut inflight: HashMap<u64, Instant> = HashMap::new();
|
||||||
|
|
||||||
while !cancel.is_cancelled() {
|
while !cancel.is_cancelled() {
|
||||||
// Detect if a request took longer than the RPS rate
|
// Detect if a request took longer than the RPS rate
|
||||||
if let Some(period) = &rps_period {
|
if let Some(period) = &rps_period {
|
||||||
@@ -391,36 +427,72 @@ async fn client_libpq(
|
|||||||
}
|
}
|
||||||
|
|
||||||
while inflight.len() < args.queue_depth.get() {
|
while inflight.len() < args.queue_depth.get() {
|
||||||
|
req_id += 1;
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let req = {
|
let (req_lsn, mod_lsn, rel, blks) = {
|
||||||
|
/// Converts a compact i128 key to a relation tag and block number.
|
||||||
|
fn key_to_block(key: i128) -> (RelTag, u32) {
|
||||||
|
let key = Key::from_i128(key);
|
||||||
|
assert!(key.is_rel_block_key());
|
||||||
|
key.to_rel_block()
|
||||||
|
.expect("we filter non-rel-block keys out above")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick a random page from a random relation.
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let r = &ranges[weights.sample(&mut rng)];
|
let r = &ranges[weights.sample(&mut rng)];
|
||||||
let key: i128 = rng.gen_range(r.start..r.end);
|
let key: i128 = rng.gen_range(r.start..r.end);
|
||||||
let key = Key::from_i128(key);
|
let (rel_tag, block_no) = key_to_block(key);
|
||||||
assert!(key.is_rel_block_key());
|
|
||||||
let (rel_tag, block_no) = key
|
let mut blks = VecDeque::with_capacity(batch_size);
|
||||||
.to_rel_block()
|
blks.push_back(block_no);
|
||||||
.expect("we filter non-rel-block keys out above");
|
|
||||||
PagestreamGetPageRequest {
|
// If requested, populate a batch of sequential pages. This is how Postgres will
|
||||||
hdr: PagestreamRequest {
|
// request page batches (e.g. prefetches). If we hit the end of the relation, we
|
||||||
reqid: 0,
|
// grow the batch towards the start too.
|
||||||
request_lsn: if rng.gen_bool(args.req_latest_probability) {
|
for i in 1..batch_size {
|
||||||
Lsn::MAX
|
let (r, b) = key_to_block(key + i as i128);
|
||||||
} else {
|
if r != rel_tag {
|
||||||
r.timeline_lsn
|
break; // went outside relation
|
||||||
},
|
}
|
||||||
not_modified_since: r.timeline_lsn,
|
blks.push_back(b)
|
||||||
},
|
|
||||||
rel: rel_tag,
|
|
||||||
blkno: block_no,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if blks.len() < batch_size {
|
||||||
|
// Grow batch backwards if needed.
|
||||||
|
for i in 1..batch_size {
|
||||||
|
let (r, b) = key_to_block(key - i as i128);
|
||||||
|
if r != rel_tag {
|
||||||
|
break; // went outside relation
|
||||||
|
}
|
||||||
|
blks.push_front(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We assume that the entire batch can fit within the relation.
|
||||||
|
assert_eq!(blks.len(), batch_size, "incomplete batch");
|
||||||
|
|
||||||
|
let req_lsn = if rng.gen_bool(args.req_latest_probability) {
|
||||||
|
Lsn::MAX
|
||||||
|
} else {
|
||||||
|
r.timeline_lsn
|
||||||
|
};
|
||||||
|
(req_lsn, r.timeline_lsn, rel_tag, blks.into())
|
||||||
};
|
};
|
||||||
client.getpage_send(req).await.unwrap();
|
client
|
||||||
inflight.push_back(start);
|
.send_get_page(req_id, req_lsn, mod_lsn, rel, blks)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let old = inflight.insert(req_id, start);
|
||||||
|
assert!(old.is_none(), "duplicate request ID {req_id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = inflight.pop_front().unwrap();
|
let (req_id, pages) = client.recv_get_page().await.unwrap();
|
||||||
client.getpage_recv().await.unwrap();
|
assert_eq!(pages.len(), batch_size, "unexpected page count");
|
||||||
|
assert!(pages.iter().all(|p| !p.is_empty()), "empty page");
|
||||||
|
let start = inflight
|
||||||
|
.remove(&req_id)
|
||||||
|
.expect("response for unknown request ID");
|
||||||
let end = Instant::now();
|
let end = Instant::now();
|
||||||
shared_state.live_stats.request_done();
|
shared_state.live_stats.request_done();
|
||||||
ticks_processed += 1;
|
ticks_processed += 1;
|
||||||
@@ -442,3 +514,154 @@ async fn client_libpq(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A benchmark client, to allow switching out the transport protocol.
|
||||||
|
///
|
||||||
|
/// For simplicity, this just uses separate asynchronous send/recv methods. The send method could
|
||||||
|
/// return a future that resolves when the response is received, but we don't really need it.
|
||||||
|
#[async_trait]
|
||||||
|
trait Client: Send {
|
||||||
|
/// Sends an asynchronous GetPage request to the pageserver.
|
||||||
|
async fn send_get_page(
|
||||||
|
&mut self,
|
||||||
|
req_id: u64,
|
||||||
|
req_lsn: Lsn,
|
||||||
|
mod_lsn: Lsn,
|
||||||
|
rel: RelTag,
|
||||||
|
blks: Vec<u32>,
|
||||||
|
) -> anyhow::Result<()>;
|
||||||
|
|
||||||
|
/// Receives the next GetPage response from the pageserver.
|
||||||
|
async fn recv_get_page(&mut self) -> anyhow::Result<(u64, Vec<Bytes>)>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A libpq-based Pageserver client.
|
||||||
|
struct LibpqClient {
|
||||||
|
inner: pageserver_client::page_service::PagestreamClient,
|
||||||
|
// Track sent batches, so we know how many responses to expect.
|
||||||
|
batch_sizes: VecDeque<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LibpqClient {
|
||||||
|
async fn new(connstring: String, ttid: TenantTimelineId) -> anyhow::Result<Self> {
|
||||||
|
let inner = pageserver_client::page_service::Client::new(connstring)
|
||||||
|
.await?
|
||||||
|
.pagestream(ttid.tenant_id, ttid.timeline_id)
|
||||||
|
.await?;
|
||||||
|
Ok(Self {
|
||||||
|
inner,
|
||||||
|
batch_sizes: VecDeque::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Client for LibpqClient {
|
||||||
|
async fn send_get_page(
|
||||||
|
&mut self,
|
||||||
|
req_id: u64,
|
||||||
|
req_lsn: Lsn,
|
||||||
|
mod_lsn: Lsn,
|
||||||
|
rel: RelTag,
|
||||||
|
blks: Vec<u32>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
// libpq doesn't support client-side batches, so we send a bunch of individual requests
|
||||||
|
// instead in the hope that the server will batch them for us. We use the same request ID
|
||||||
|
// for all, because we'll return a single batch response.
|
||||||
|
self.batch_sizes.push_back(blks.len());
|
||||||
|
for blkno in blks {
|
||||||
|
let req = PagestreamGetPageRequest {
|
||||||
|
hdr: PagestreamRequest {
|
||||||
|
reqid: req_id,
|
||||||
|
request_lsn: req_lsn,
|
||||||
|
not_modified_since: mod_lsn,
|
||||||
|
},
|
||||||
|
rel,
|
||||||
|
blkno,
|
||||||
|
};
|
||||||
|
self.inner.getpage_send(req).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn recv_get_page(&mut self) -> anyhow::Result<(u64, Vec<Bytes>)> {
|
||||||
|
let batch_size = self.batch_sizes.pop_front().unwrap();
|
||||||
|
let mut batch = Vec::with_capacity(batch_size);
|
||||||
|
let mut req_id = None;
|
||||||
|
for _ in 0..batch_size {
|
||||||
|
let resp = self.inner.getpage_recv().await?;
|
||||||
|
if req_id.is_none() {
|
||||||
|
req_id = Some(resp.req.hdr.reqid);
|
||||||
|
}
|
||||||
|
assert_eq!(req_id, Some(resp.req.hdr.reqid), "request ID mismatch");
|
||||||
|
batch.push(resp.page);
|
||||||
|
}
|
||||||
|
Ok((req_id.unwrap(), batch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A gRPC client using the raw, no-frills gRPC client.
|
||||||
|
struct GrpcClient {
|
||||||
|
req_tx: tokio::sync::mpsc::Sender<proto::GetPageRequest>,
|
||||||
|
resp_rx: tonic::Streaming<proto::GetPageResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GrpcClient {
|
||||||
|
async fn new(connstring: String, ttid: TenantTimelineId) -> anyhow::Result<Self> {
|
||||||
|
let mut client = pageserver_page_api::proto::PageServiceClient::connect(connstring).await?;
|
||||||
|
|
||||||
|
// The channel has a buffer size of 1, since 0 is not allowed. It does not matter, since the
|
||||||
|
// benchmark will control the queue depth (i.e. in-flight requests) anyway, and requests are
|
||||||
|
// buffered by Tonic and the OS too.
|
||||||
|
let (req_tx, req_rx) = tokio::sync::mpsc::channel(1);
|
||||||
|
let req_stream = tokio_stream::wrappers::ReceiverStream::new(req_rx);
|
||||||
|
let mut req = tonic::Request::new(req_stream);
|
||||||
|
let metadata = req.metadata_mut();
|
||||||
|
metadata.insert("neon-tenant-id", ttid.tenant_id.to_string().try_into()?);
|
||||||
|
metadata.insert("neon-timeline-id", ttid.timeline_id.to_string().try_into()?);
|
||||||
|
metadata.insert("neon-shard-id", "0000".try_into()?);
|
||||||
|
|
||||||
|
let resp = client.get_pages(req).await?;
|
||||||
|
let resp_stream = resp.into_inner();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
req_tx,
|
||||||
|
resp_rx: resp_stream,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Client for GrpcClient {
|
||||||
|
async fn send_get_page(
|
||||||
|
&mut self,
|
||||||
|
req_id: u64,
|
||||||
|
req_lsn: Lsn,
|
||||||
|
mod_lsn: Lsn,
|
||||||
|
rel: RelTag,
|
||||||
|
blks: Vec<u32>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let req = proto::GetPageRequest {
|
||||||
|
request_id: req_id,
|
||||||
|
request_class: proto::GetPageClass::Normal as i32,
|
||||||
|
read_lsn: Some(proto::ReadLsn {
|
||||||
|
request_lsn: req_lsn.0,
|
||||||
|
not_modified_since_lsn: mod_lsn.0,
|
||||||
|
}),
|
||||||
|
rel: Some(rel.into()),
|
||||||
|
block_number: blks,
|
||||||
|
};
|
||||||
|
self.req_tx.send(req).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn recv_get_page(&mut self) -> anyhow::Result<(u64, Vec<Bytes>)> {
|
||||||
|
let resp = self.resp_rx.message().await?.unwrap();
|
||||||
|
anyhow::ensure!(
|
||||||
|
resp.status_code == proto::GetPageStatusCode::Ok as i32,
|
||||||
|
"unexpected status code: {}",
|
||||||
|
resp.status_code
|
||||||
|
);
|
||||||
|
Ok((resp.request_id, resp.page_image))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,6 +65,30 @@ impl From<GetVectoredError> for BasebackupError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<BasebackupError> for postgres_backend::QueryError {
|
||||||
|
fn from(err: BasebackupError) -> Self {
|
||||||
|
use postgres_backend::QueryError;
|
||||||
|
use pq_proto::framed::ConnectionError;
|
||||||
|
match err {
|
||||||
|
BasebackupError::Client(err, _) => QueryError::Disconnected(ConnectionError::Io(err)),
|
||||||
|
BasebackupError::Server(err) => QueryError::Other(err),
|
||||||
|
BasebackupError::Shutdown => QueryError::Shutdown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BasebackupError> for tonic::Status {
|
||||||
|
fn from(err: BasebackupError) -> Self {
|
||||||
|
use tonic::Code;
|
||||||
|
let code = match &err {
|
||||||
|
BasebackupError::Client(_, _) => Code::Cancelled,
|
||||||
|
BasebackupError::Server(_) => Code::Internal,
|
||||||
|
BasebackupError::Shutdown => Code::Unavailable,
|
||||||
|
};
|
||||||
|
tonic::Status::new(code, err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create basebackup with non-rel data in it.
|
/// Create basebackup with non-rel data in it.
|
||||||
/// Only include relational data if 'full_backup' is true.
|
/// Only include relational data if 'full_backup' is true.
|
||||||
///
|
///
|
||||||
@@ -248,7 +272,7 @@ where
|
|||||||
async fn flush(&mut self) -> Result<(), BasebackupError> {
|
async fn flush(&mut self) -> Result<(), BasebackupError> {
|
||||||
let nblocks = self.buf.len() / BLCKSZ as usize;
|
let nblocks = self.buf.len() / BLCKSZ as usize;
|
||||||
let (kind, segno) = self.current_segment.take().unwrap();
|
let (kind, segno) = self.current_segment.take().unwrap();
|
||||||
let segname = format!("{}/{:>04X}", kind.to_str(), segno);
|
let segname = format!("{kind}/{segno:>04X}");
|
||||||
let header = new_tar_header(&segname, self.buf.len() as u64)?;
|
let header = new_tar_header(&segname, self.buf.len() as u64)?;
|
||||||
self.ar
|
self.ar
|
||||||
.append(&header, self.buf.as_slice())
|
.append(&header, self.buf.as_slice())
|
||||||
@@ -347,7 +371,7 @@ where
|
|||||||
.await?
|
.await?
|
||||||
.partition(
|
.partition(
|
||||||
self.timeline.get_shard_identity(),
|
self.timeline.get_shard_identity(),
|
||||||
Timeline::MAX_GET_VECTORED_KEYS * BLCKSZ as u64,
|
self.timeline.conf.max_get_vectored_keys.get() as u64 * BLCKSZ as u64,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut slru_builder = SlruSegmentsBuilder::new(&mut self.ar);
|
let mut slru_builder = SlruSegmentsBuilder::new(&mut self.ar);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use pageserver::deletion_queue::DeletionQueue;
|
|||||||
use pageserver::disk_usage_eviction_task::{self, launch_disk_usage_global_eviction_task};
|
use pageserver::disk_usage_eviction_task::{self, launch_disk_usage_global_eviction_task};
|
||||||
use pageserver::feature_resolver::FeatureResolver;
|
use pageserver::feature_resolver::FeatureResolver;
|
||||||
use pageserver::metrics::{STARTUP_DURATION, STARTUP_IS_LOADING};
|
use pageserver::metrics::{STARTUP_DURATION, STARTUP_IS_LOADING};
|
||||||
|
use pageserver::page_service::GrpcPageServiceHandler;
|
||||||
use pageserver::task_mgr::{
|
use pageserver::task_mgr::{
|
||||||
BACKGROUND_RUNTIME, COMPUTE_REQUEST_RUNTIME, MGMT_REQUEST_RUNTIME, WALRECEIVER_RUNTIME,
|
BACKGROUND_RUNTIME, COMPUTE_REQUEST_RUNTIME, MGMT_REQUEST_RUNTIME, WALRECEIVER_RUNTIME,
|
||||||
};
|
};
|
||||||
@@ -158,7 +159,6 @@ fn main() -> anyhow::Result<()> {
|
|||||||
// (maybe we should automate this with a visitor?).
|
// (maybe we should automate this with a visitor?).
|
||||||
info!(?conf.virtual_file_io_engine, "starting with virtual_file IO engine");
|
info!(?conf.virtual_file_io_engine, "starting with virtual_file IO engine");
|
||||||
info!(?conf.virtual_file_io_mode, "starting with virtual_file IO mode");
|
info!(?conf.virtual_file_io_mode, "starting with virtual_file IO mode");
|
||||||
info!(?conf.wal_receiver_protocol, "starting with WAL receiver protocol");
|
|
||||||
info!(?conf.validate_wal_contiguity, "starting with WAL contiguity validation");
|
info!(?conf.validate_wal_contiguity, "starting with WAL contiguity validation");
|
||||||
info!(?conf.page_service_pipelining, "starting with page service pipelining config");
|
info!(?conf.page_service_pipelining, "starting with page service pipelining config");
|
||||||
info!(?conf.get_vectored_concurrent_io, "starting with get_vectored IO concurrency config");
|
info!(?conf.get_vectored_concurrent_io, "starting with get_vectored IO concurrency config");
|
||||||
@@ -573,7 +573,8 @@ fn start_pageserver(
|
|||||||
tokio::sync::mpsc::unbounded_channel();
|
tokio::sync::mpsc::unbounded_channel();
|
||||||
let deletion_queue_client = deletion_queue.new_client();
|
let deletion_queue_client = deletion_queue.new_client();
|
||||||
let background_purges = mgr::BackgroundPurges::default();
|
let background_purges = mgr::BackgroundPurges::default();
|
||||||
let tenant_manager = BACKGROUND_RUNTIME.block_on(mgr::init_tenant_mgr(
|
|
||||||
|
let tenant_manager = mgr::init(
|
||||||
conf,
|
conf,
|
||||||
background_purges.clone(),
|
background_purges.clone(),
|
||||||
TenantSharedResources {
|
TenantSharedResources {
|
||||||
@@ -584,10 +585,10 @@ fn start_pageserver(
|
|||||||
basebackup_prepare_sender,
|
basebackup_prepare_sender,
|
||||||
feature_resolver,
|
feature_resolver,
|
||||||
},
|
},
|
||||||
order,
|
|
||||||
shutdown_pageserver.clone(),
|
shutdown_pageserver.clone(),
|
||||||
))?;
|
);
|
||||||
let tenant_manager = Arc::new(tenant_manager);
|
let tenant_manager = Arc::new(tenant_manager);
|
||||||
|
BACKGROUND_RUNTIME.block_on(mgr::init_tenant_mgr(tenant_manager.clone(), order))?;
|
||||||
|
|
||||||
let basebackup_cache = BasebackupCache::spawn(
|
let basebackup_cache = BasebackupCache::spawn(
|
||||||
BACKGROUND_RUNTIME.handle(),
|
BACKGROUND_RUNTIME.handle(),
|
||||||
@@ -804,7 +805,7 @@ fn start_pageserver(
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
basebackup_cache.clone(),
|
basebackup_cache,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Spawn a Pageserver gRPC server task. It will spawn separate tasks for
|
// Spawn a Pageserver gRPC server task. It will spawn separate tasks for
|
||||||
@@ -815,13 +816,12 @@ fn start_pageserver(
|
|||||||
// necessary?
|
// necessary?
|
||||||
let mut page_service_grpc = None;
|
let mut page_service_grpc = None;
|
||||||
if let Some(grpc_listener) = grpc_listener {
|
if let Some(grpc_listener) = grpc_listener {
|
||||||
page_service_grpc = Some(page_service::spawn_grpc(
|
page_service_grpc = Some(GrpcPageServiceHandler::spawn(
|
||||||
conf,
|
|
||||||
tenant_manager.clone(),
|
tenant_manager.clone(),
|
||||||
grpc_auth,
|
grpc_auth,
|
||||||
otel_guard.as_ref().map(|g| g.dispatch.clone()),
|
otel_guard.as_ref().map(|g| g.dispatch.clone()),
|
||||||
|
conf.get_vectored_concurrent_io,
|
||||||
grpc_listener,
|
grpc_listener,
|
||||||
basebackup_cache,
|
|
||||||
)?);
|
)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ use std::time::Duration;
|
|||||||
use anyhow::{Context, bail, ensure};
|
use anyhow::{Context, bail, ensure};
|
||||||
use camino::{Utf8Path, Utf8PathBuf};
|
use camino::{Utf8Path, Utf8PathBuf};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use pageserver_api::config::{DiskUsageEvictionTaskConfig, MaxVectoredReadBytes, PostHogConfig};
|
use pageserver_api::config::{
|
||||||
|
DiskUsageEvictionTaskConfig, MaxGetVectoredKeys, MaxVectoredReadBytes,
|
||||||
|
PageServicePipeliningConfig, PageServicePipeliningConfigPipelined, PostHogConfig,
|
||||||
|
};
|
||||||
use pageserver_api::models::ImageCompressionAlgorithm;
|
use pageserver_api::models::ImageCompressionAlgorithm;
|
||||||
use pageserver_api::shard::TenantShardId;
|
use pageserver_api::shard::TenantShardId;
|
||||||
use pem::Pem;
|
use pem::Pem;
|
||||||
@@ -24,7 +27,6 @@ use reqwest::Url;
|
|||||||
use storage_broker::Uri;
|
use storage_broker::Uri;
|
||||||
use utils::id::{NodeId, TimelineId};
|
use utils::id::{NodeId, TimelineId};
|
||||||
use utils::logging::{LogFormat, SecretString};
|
use utils::logging::{LogFormat, SecretString};
|
||||||
use utils::postgres_client::PostgresClientProtocol;
|
|
||||||
|
|
||||||
use crate::tenant::storage_layer::inmemory_layer::IndexEntry;
|
use crate::tenant::storage_layer::inmemory_layer::IndexEntry;
|
||||||
use crate::tenant::{TENANTS_SEGMENT_NAME, TIMELINES_SEGMENT_NAME};
|
use crate::tenant::{TENANTS_SEGMENT_NAME, TIMELINES_SEGMENT_NAME};
|
||||||
@@ -185,6 +187,9 @@ pub struct PageServerConf {
|
|||||||
|
|
||||||
pub max_vectored_read_bytes: MaxVectoredReadBytes,
|
pub max_vectored_read_bytes: MaxVectoredReadBytes,
|
||||||
|
|
||||||
|
/// Maximum number of keys to be read in a single get_vectored call.
|
||||||
|
pub max_get_vectored_keys: MaxGetVectoredKeys,
|
||||||
|
|
||||||
pub image_compression: ImageCompressionAlgorithm,
|
pub image_compression: ImageCompressionAlgorithm,
|
||||||
|
|
||||||
/// Whether to offload archived timelines automatically
|
/// Whether to offload archived timelines automatically
|
||||||
@@ -205,8 +210,6 @@ pub struct PageServerConf {
|
|||||||
/// Optionally disable disk syncs (unsafe!)
|
/// Optionally disable disk syncs (unsafe!)
|
||||||
pub no_sync: bool,
|
pub no_sync: bool,
|
||||||
|
|
||||||
pub wal_receiver_protocol: PostgresClientProtocol,
|
|
||||||
|
|
||||||
pub page_service_pipelining: pageserver_api::config::PageServicePipeliningConfig,
|
pub page_service_pipelining: pageserver_api::config::PageServicePipeliningConfig,
|
||||||
|
|
||||||
pub get_vectored_concurrent_io: pageserver_api::config::GetVectoredConcurrentIo,
|
pub get_vectored_concurrent_io: pageserver_api::config::GetVectoredConcurrentIo,
|
||||||
@@ -404,6 +407,7 @@ impl PageServerConf {
|
|||||||
secondary_download_concurrency,
|
secondary_download_concurrency,
|
||||||
ingest_batch_size,
|
ingest_batch_size,
|
||||||
max_vectored_read_bytes,
|
max_vectored_read_bytes,
|
||||||
|
max_get_vectored_keys,
|
||||||
image_compression,
|
image_compression,
|
||||||
timeline_offloading,
|
timeline_offloading,
|
||||||
ephemeral_bytes_per_memory_kb,
|
ephemeral_bytes_per_memory_kb,
|
||||||
@@ -414,7 +418,6 @@ impl PageServerConf {
|
|||||||
virtual_file_io_engine,
|
virtual_file_io_engine,
|
||||||
tenant_config,
|
tenant_config,
|
||||||
no_sync,
|
no_sync,
|
||||||
wal_receiver_protocol,
|
|
||||||
page_service_pipelining,
|
page_service_pipelining,
|
||||||
get_vectored_concurrent_io,
|
get_vectored_concurrent_io,
|
||||||
enable_read_path_debugging,
|
enable_read_path_debugging,
|
||||||
@@ -470,13 +473,13 @@ impl PageServerConf {
|
|||||||
secondary_download_concurrency,
|
secondary_download_concurrency,
|
||||||
ingest_batch_size,
|
ingest_batch_size,
|
||||||
max_vectored_read_bytes,
|
max_vectored_read_bytes,
|
||||||
|
max_get_vectored_keys,
|
||||||
image_compression,
|
image_compression,
|
||||||
timeline_offloading,
|
timeline_offloading,
|
||||||
ephemeral_bytes_per_memory_kb,
|
ephemeral_bytes_per_memory_kb,
|
||||||
import_pgdata_upcall_api,
|
import_pgdata_upcall_api,
|
||||||
import_pgdata_upcall_api_token: import_pgdata_upcall_api_token.map(SecretString::from),
|
import_pgdata_upcall_api_token: import_pgdata_upcall_api_token.map(SecretString::from),
|
||||||
import_pgdata_aws_endpoint_url,
|
import_pgdata_aws_endpoint_url,
|
||||||
wal_receiver_protocol,
|
|
||||||
page_service_pipelining,
|
page_service_pipelining,
|
||||||
get_vectored_concurrent_io,
|
get_vectored_concurrent_io,
|
||||||
tracing,
|
tracing,
|
||||||
@@ -598,6 +601,19 @@ impl PageServerConf {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
if let PageServicePipeliningConfig::Pipelined(PageServicePipeliningConfigPipelined {
|
||||||
|
max_batch_size,
|
||||||
|
..
|
||||||
|
}) = conf.page_service_pipelining
|
||||||
|
{
|
||||||
|
if max_batch_size.get() > conf.max_get_vectored_keys.get() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"`max_batch_size` ({max_batch_size}) must be less than or equal to `max_get_vectored_keys` ({})",
|
||||||
|
conf.max_get_vectored_keys.get()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(conf)
|
Ok(conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,6 +701,7 @@ impl ConfigurableSemaphore {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use camino::Utf8PathBuf;
|
use camino::Utf8PathBuf;
|
||||||
|
use rstest::rstest;
|
||||||
use utils::id::NodeId;
|
use utils::id::NodeId;
|
||||||
|
|
||||||
use super::PageServerConf;
|
use super::PageServerConf;
|
||||||
@@ -724,4 +741,28 @@ mod tests {
|
|||||||
PageServerConf::parse_and_validate(NodeId(0), config_toml, &workdir)
|
PageServerConf::parse_and_validate(NodeId(0), config_toml, &workdir)
|
||||||
.expect_err("parse_and_validate should fail for endpoint without scheme");
|
.expect_err("parse_and_validate should fail for endpoint without scheme");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(32, 32, true)]
|
||||||
|
#[case(64, 32, false)]
|
||||||
|
#[case(64, 64, true)]
|
||||||
|
#[case(128, 128, true)]
|
||||||
|
fn test_config_max_batch_size_is_valid(
|
||||||
|
#[case] max_batch_size: usize,
|
||||||
|
#[case] max_get_vectored_keys: usize,
|
||||||
|
#[case] is_valid: bool,
|
||||||
|
) {
|
||||||
|
let input = format!(
|
||||||
|
r#"
|
||||||
|
control_plane_api = "http://localhost:6666"
|
||||||
|
max_get_vectored_keys = {max_get_vectored_keys}
|
||||||
|
page_service_pipelining = {{ mode="pipelined", execution="concurrent-futures", max_batch_size={max_batch_size}, batching="uniform-lsn" }}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let config_toml = toml_edit::de::from_str::<pageserver_api::config::ConfigToml>(&input)
|
||||||
|
.expect("config has valid fields");
|
||||||
|
let workdir = Utf8PathBuf::from("/nonexistent");
|
||||||
|
let result = PageServerConf::parse_and_validate(NodeId(0), config_toml, &workdir);
|
||||||
|
assert_eq!(result.is_ok(), is_valid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use pageserver_api::config::NodeMetadata;
|
||||||
use posthog_client_lite::{
|
use posthog_client_lite::{
|
||||||
FeatureResolverBackgroundLoop, PostHogClientConfig, PostHogEvaluationError,
|
CaptureEvent, FeatureResolverBackgroundLoop, PostHogClientConfig, PostHogEvaluationError,
|
||||||
|
PostHogFlagFilterPropertyValue,
|
||||||
};
|
};
|
||||||
|
use remote_storage::RemoteStorageKind;
|
||||||
|
use serde_json::json;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use utils::id::TenantId;
|
use utils::id::TenantId;
|
||||||
|
|
||||||
use crate::config::PageServerConf;
|
use crate::{config::PageServerConf, metrics::FEATURE_FLAG_EVALUATION};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FeatureResolver {
|
pub struct FeatureResolver {
|
||||||
inner: Option<Arc<FeatureResolverBackgroundLoop>>,
|
inner: Option<Arc<FeatureResolverBackgroundLoop>>,
|
||||||
|
internal_properties: Option<Arc<HashMap<String, PostHogFlagFilterPropertyValue>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeatureResolver {
|
impl FeatureResolver {
|
||||||
pub fn new_disabled() -> Self {
|
pub fn new_disabled() -> Self {
|
||||||
Self { inner: None }
|
Self {
|
||||||
|
inner: None,
|
||||||
|
internal_properties: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn(
|
pub fn spawn(
|
||||||
@@ -36,14 +44,142 @@ impl FeatureResolver {
|
|||||||
shutdown_pageserver,
|
shutdown_pageserver,
|
||||||
);
|
);
|
||||||
let inner = Arc::new(inner);
|
let inner = Arc::new(inner);
|
||||||
// TODO: make this configurable
|
|
||||||
inner.clone().spawn(handle, Duration::from_secs(60));
|
// The properties shared by all tenants on this pageserver.
|
||||||
Ok(FeatureResolver { inner: Some(inner) })
|
let internal_properties = {
|
||||||
|
let mut properties = HashMap::new();
|
||||||
|
properties.insert(
|
||||||
|
"pageserver_id".to_string(),
|
||||||
|
PostHogFlagFilterPropertyValue::String(conf.id.to_string()),
|
||||||
|
);
|
||||||
|
if let Some(availability_zone) = &conf.availability_zone {
|
||||||
|
properties.insert(
|
||||||
|
"availability_zone".to_string(),
|
||||||
|
PostHogFlagFilterPropertyValue::String(availability_zone.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Infer region based on the remote storage config.
|
||||||
|
if let Some(remote_storage) = &conf.remote_storage_config {
|
||||||
|
match &remote_storage.storage {
|
||||||
|
RemoteStorageKind::AwsS3(config) => {
|
||||||
|
properties.insert(
|
||||||
|
"region".to_string(),
|
||||||
|
PostHogFlagFilterPropertyValue::String(format!(
|
||||||
|
"aws-{}",
|
||||||
|
config.bucket_region
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
RemoteStorageKind::AzureContainer(config) => {
|
||||||
|
properties.insert(
|
||||||
|
"region".to_string(),
|
||||||
|
PostHogFlagFilterPropertyValue::String(format!(
|
||||||
|
"azure-{}",
|
||||||
|
config.container_region
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
RemoteStorageKind::LocalFs { .. } => {
|
||||||
|
properties.insert(
|
||||||
|
"region".to_string(),
|
||||||
|
PostHogFlagFilterPropertyValue::String("local".to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: move this to a background task so that we don't block startup in case of slow disk
|
||||||
|
let metadata_path = conf.metadata_path();
|
||||||
|
match std::fs::read_to_string(&metadata_path) {
|
||||||
|
Ok(metadata_str) => match serde_json::from_str::<NodeMetadata>(&metadata_str) {
|
||||||
|
Ok(metadata) => {
|
||||||
|
properties.insert(
|
||||||
|
"hostname".to_string(),
|
||||||
|
PostHogFlagFilterPropertyValue::String(metadata.http_host),
|
||||||
|
);
|
||||||
|
if let Some(cplane_region) = metadata.other.get("region_id") {
|
||||||
|
if let Some(cplane_region) = cplane_region.as_str() {
|
||||||
|
// This region contains the cell number
|
||||||
|
properties.insert(
|
||||||
|
"neon_region".to_string(),
|
||||||
|
PostHogFlagFilterPropertyValue::String(
|
||||||
|
cplane_region.to_string(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to parse metadata.json: {}", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to read metadata.json: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Arc::new(properties)
|
||||||
|
};
|
||||||
|
let fake_tenants = {
|
||||||
|
let mut tenants = Vec::new();
|
||||||
|
for i in 0..10 {
|
||||||
|
let distinct_id = format!(
|
||||||
|
"fake_tenant_{}_{}_{}",
|
||||||
|
conf.availability_zone.as_deref().unwrap_or_default(),
|
||||||
|
conf.id,
|
||||||
|
i
|
||||||
|
);
|
||||||
|
let properties = Self::collect_properties_inner(
|
||||||
|
distinct_id.clone(),
|
||||||
|
Some(&internal_properties),
|
||||||
|
);
|
||||||
|
tenants.push(CaptureEvent {
|
||||||
|
event: "initial_tenant_report".to_string(),
|
||||||
|
distinct_id,
|
||||||
|
properties: json!({ "$set": properties }), // use `$set` to set the person properties instead of the event properties
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tenants
|
||||||
|
};
|
||||||
|
// TODO: make refresh period configurable
|
||||||
|
inner
|
||||||
|
.clone()
|
||||||
|
.spawn(handle, Duration::from_secs(60), fake_tenants);
|
||||||
|
Ok(FeatureResolver {
|
||||||
|
inner: Some(inner),
|
||||||
|
internal_properties: Some(internal_properties),
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(FeatureResolver { inner: None })
|
Ok(FeatureResolver {
|
||||||
|
inner: None,
|
||||||
|
internal_properties: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn collect_properties_inner(
|
||||||
|
tenant_id: String,
|
||||||
|
internal_properties: Option<&HashMap<String, PostHogFlagFilterPropertyValue>>,
|
||||||
|
) -> HashMap<String, PostHogFlagFilterPropertyValue> {
|
||||||
|
let mut properties = HashMap::new();
|
||||||
|
if let Some(internal_properties) = internal_properties {
|
||||||
|
for (key, value) in internal_properties.iter() {
|
||||||
|
properties.insert(key.clone(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
properties.insert(
|
||||||
|
"tenant_id".to_string(),
|
||||||
|
PostHogFlagFilterPropertyValue::String(tenant_id),
|
||||||
|
);
|
||||||
|
properties
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect all properties availble for the feature flag evaluation.
|
||||||
|
pub(crate) fn collect_properties(
|
||||||
|
&self,
|
||||||
|
tenant_id: TenantId,
|
||||||
|
) -> HashMap<String, PostHogFlagFilterPropertyValue> {
|
||||||
|
Self::collect_properties_inner(tenant_id.to_string(), self.internal_properties.as_deref())
|
||||||
|
}
|
||||||
|
|
||||||
/// Evaluate a multivariate feature flag. Currently, we do not support any properties.
|
/// Evaluate a multivariate feature flag. Currently, we do not support any properties.
|
||||||
///
|
///
|
||||||
/// Error handling: the caller should inspect the error and decide the behavior when a feature flag
|
/// Error handling: the caller should inspect the error and decide the behavior when a feature flag
|
||||||
@@ -55,11 +191,24 @@ impl FeatureResolver {
|
|||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
) -> Result<String, PostHogEvaluationError> {
|
) -> Result<String, PostHogEvaluationError> {
|
||||||
if let Some(inner) = &self.inner {
|
if let Some(inner) = &self.inner {
|
||||||
inner.feature_store().evaluate_multivariate(
|
let res = inner.feature_store().evaluate_multivariate(
|
||||||
flag_key,
|
flag_key,
|
||||||
&tenant_id.to_string(),
|
&tenant_id.to_string(),
|
||||||
&HashMap::new(),
|
&self.collect_properties(tenant_id),
|
||||||
)
|
);
|
||||||
|
match &res {
|
||||||
|
Ok(value) => {
|
||||||
|
FEATURE_FLAG_EVALUATION
|
||||||
|
.with_label_values(&[flag_key, "ok", value])
|
||||||
|
.inc();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
FEATURE_FLAG_EVALUATION
|
||||||
|
.with_label_values(&[flag_key, "error", e.as_variant_str()])
|
||||||
|
.inc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res
|
||||||
} else {
|
} else {
|
||||||
Err(PostHogEvaluationError::NotAvailable(
|
Err(PostHogEvaluationError::NotAvailable(
|
||||||
"PostHog integration is not enabled".to_string(),
|
"PostHog integration is not enabled".to_string(),
|
||||||
@@ -80,11 +229,24 @@ impl FeatureResolver {
|
|||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
) -> Result<(), PostHogEvaluationError> {
|
) -> Result<(), PostHogEvaluationError> {
|
||||||
if let Some(inner) = &self.inner {
|
if let Some(inner) = &self.inner {
|
||||||
inner.feature_store().evaluate_boolean(
|
let res = inner.feature_store().evaluate_boolean(
|
||||||
flag_key,
|
flag_key,
|
||||||
&tenant_id.to_string(),
|
&tenant_id.to_string(),
|
||||||
&HashMap::new(),
|
&self.collect_properties(tenant_id),
|
||||||
)
|
);
|
||||||
|
match &res {
|
||||||
|
Ok(()) => {
|
||||||
|
FEATURE_FLAG_EVALUATION
|
||||||
|
.with_label_values(&[flag_key, "ok", "true"])
|
||||||
|
.inc();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
FEATURE_FLAG_EVALUATION
|
||||||
|
.with_label_values(&[flag_key, "error", e.as_variant_str()])
|
||||||
|
.inc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res
|
||||||
} else {
|
} else {
|
||||||
Err(PostHogEvaluationError::NotAvailable(
|
Err(PostHogEvaluationError::NotAvailable(
|
||||||
"PostHog integration is not enabled".to_string(),
|
"PostHog integration is not enabled".to_string(),
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ use pageserver_api::models::{
|
|||||||
use pageserver_api::shard::{ShardCount, TenantShardId};
|
use pageserver_api::shard::{ShardCount, TenantShardId};
|
||||||
use remote_storage::{DownloadError, GenericRemoteStorage, TimeTravelError};
|
use remote_storage::{DownloadError, GenericRemoteStorage, TimeTravelError};
|
||||||
use scopeguard::defer;
|
use scopeguard::defer;
|
||||||
|
use serde_json::json;
|
||||||
use tenant_size_model::svg::SvgBranchKind;
|
use tenant_size_model::svg::SvgBranchKind;
|
||||||
use tenant_size_model::{SizeResult, StorageModel};
|
use tenant_size_model::{SizeResult, StorageModel};
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
@@ -3679,23 +3680,24 @@ async fn tenant_evaluate_feature_flag(
|
|||||||
let tenant = state
|
let tenant = state
|
||||||
.tenant_manager
|
.tenant_manager
|
||||||
.get_attached_tenant_shard(tenant_shard_id)?;
|
.get_attached_tenant_shard(tenant_shard_id)?;
|
||||||
|
let properties = tenant.feature_resolver.collect_properties(tenant_shard_id.tenant_id);
|
||||||
if as_type == "boolean" {
|
if as_type == "boolean" {
|
||||||
let result = tenant.feature_resolver.evaluate_boolean(&flag, tenant_shard_id.tenant_id);
|
let result = tenant.feature_resolver.evaluate_boolean(&flag, tenant_shard_id.tenant_id);
|
||||||
let result = result.map(|_| true).map_err(|e| e.to_string());
|
let result = result.map(|_| true).map_err(|e| e.to_string());
|
||||||
json_response(StatusCode::OK, result)
|
json_response(StatusCode::OK, json!({ "result": result, "properties": properties }))
|
||||||
} else if as_type == "multivariate" {
|
} else if as_type == "multivariate" {
|
||||||
let result = tenant.feature_resolver.evaluate_multivariate(&flag, tenant_shard_id.tenant_id).map_err(|e| e.to_string());
|
let result = tenant.feature_resolver.evaluate_multivariate(&flag, tenant_shard_id.tenant_id).map_err(|e| e.to_string());
|
||||||
json_response(StatusCode::OK, result)
|
json_response(StatusCode::OK, json!({ "result": result, "properties": properties }))
|
||||||
} else {
|
} else {
|
||||||
// Auto infer the type of the feature flag.
|
// Auto infer the type of the feature flag.
|
||||||
let is_boolean = tenant.feature_resolver.is_feature_flag_boolean(&flag).map_err(|e| ApiError::InternalServerError(anyhow::anyhow!("{e}")))?;
|
let is_boolean = tenant.feature_resolver.is_feature_flag_boolean(&flag).map_err(|e| ApiError::InternalServerError(anyhow::anyhow!("{e}")))?;
|
||||||
if is_boolean {
|
if is_boolean {
|
||||||
let result = tenant.feature_resolver.evaluate_boolean(&flag, tenant_shard_id.tenant_id);
|
let result = tenant.feature_resolver.evaluate_boolean(&flag, tenant_shard_id.tenant_id);
|
||||||
let result = result.map(|_| true).map_err(|e| e.to_string());
|
let result = result.map(|_| true).map_err(|e| e.to_string());
|
||||||
json_response(StatusCode::OK, result)
|
json_response(StatusCode::OK, json!({ "result": result, "properties": properties }))
|
||||||
} else {
|
} else {
|
||||||
let result = tenant.feature_resolver.evaluate_multivariate(&flag, tenant_shard_id.tenant_id).map_err(|e| e.to_string());
|
let result = tenant.feature_resolver.evaluate_multivariate(&flag, tenant_shard_id.tenant_id).map_err(|e| e.to_string());
|
||||||
json_response(StatusCode::OK, result)
|
json_response(StatusCode::OK, json!({ "result": result, "properties": properties }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use metrics::{
|
|||||||
register_int_gauge, register_int_gauge_vec, register_uint_gauge, register_uint_gauge_vec,
|
register_int_gauge, register_int_gauge_vec, register_uint_gauge, register_uint_gauge_vec,
|
||||||
};
|
};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use pageserver_api::config::defaults::DEFAULT_MAX_GET_VECTORED_KEYS;
|
||||||
use pageserver_api::config::{
|
use pageserver_api::config::{
|
||||||
PageServicePipeliningConfig, PageServicePipeliningConfigPipelined,
|
PageServicePipeliningConfig, PageServicePipeliningConfigPipelined,
|
||||||
PageServiceProtocolPipelinedBatchingStrategy, PageServiceProtocolPipelinedExecutionStrategy,
|
PageServiceProtocolPipelinedBatchingStrategy, PageServiceProtocolPipelinedExecutionStrategy,
|
||||||
@@ -32,7 +33,6 @@ use crate::config::PageServerConf;
|
|||||||
use crate::context::{PageContentKind, RequestContext};
|
use crate::context::{PageContentKind, RequestContext};
|
||||||
use crate::pgdatadir_mapping::DatadirModificationStats;
|
use crate::pgdatadir_mapping::DatadirModificationStats;
|
||||||
use crate::task_mgr::TaskKind;
|
use crate::task_mgr::TaskKind;
|
||||||
use crate::tenant::Timeline;
|
|
||||||
use crate::tenant::layer_map::LayerMap;
|
use crate::tenant::layer_map::LayerMap;
|
||||||
use crate::tenant::mgr::TenantSlot;
|
use crate::tenant::mgr::TenantSlot;
|
||||||
use crate::tenant::storage_layer::{InMemoryLayer, PersistentLayerDesc};
|
use crate::tenant::storage_layer::{InMemoryLayer, PersistentLayerDesc};
|
||||||
@@ -446,6 +446,15 @@ static PAGE_CACHE_ERRORS: Lazy<IntCounterVec> = Lazy::new(|| {
|
|||||||
.expect("failed to define a metric")
|
.expect("failed to define a metric")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub(crate) static FEATURE_FLAG_EVALUATION: Lazy<CounterVec> = Lazy::new(|| {
|
||||||
|
register_counter_vec!(
|
||||||
|
"pageserver_feature_flag_evaluation",
|
||||||
|
"Number of times a feature flag is evaluated",
|
||||||
|
&["flag_key", "status", "value"],
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
#[derive(IntoStaticStr)]
|
#[derive(IntoStaticStr)]
|
||||||
#[strum(serialize_all = "kebab_case")]
|
#[strum(serialize_all = "kebab_case")]
|
||||||
pub(crate) enum PageCacheErrorKind {
|
pub(crate) enum PageCacheErrorKind {
|
||||||
@@ -1044,6 +1053,15 @@ pub(crate) static TENANT_STATE_METRIC: Lazy<UIntGaugeVec> = Lazy::new(|| {
|
|||||||
.expect("Failed to register pageserver_tenant_states_count metric")
|
.expect("Failed to register pageserver_tenant_states_count metric")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub(crate) static TIMELINE_STATE_METRIC: Lazy<UIntGaugeVec> = Lazy::new(|| {
|
||||||
|
register_uint_gauge_vec!(
|
||||||
|
"pageserver_timeline_states_count",
|
||||||
|
"Count of timelines per state",
|
||||||
|
&["state"]
|
||||||
|
)
|
||||||
|
.expect("Failed to register pageserver_timeline_states_count metric")
|
||||||
|
});
|
||||||
|
|
||||||
/// A set of broken tenants.
|
/// A set of broken tenants.
|
||||||
///
|
///
|
||||||
/// These are expected to be so rare that a set is fine. Set as in a new timeseries per each broken
|
/// These are expected to be so rare that a set is fine. Set as in a new timeseries per each broken
|
||||||
@@ -1939,7 +1957,7 @@ static SMGR_QUERY_TIME_GLOBAL: Lazy<HistogramVec> = Lazy::new(|| {
|
|||||||
});
|
});
|
||||||
|
|
||||||
static PAGE_SERVICE_BATCH_SIZE_BUCKETS_GLOBAL: Lazy<Vec<f64>> = Lazy::new(|| {
|
static PAGE_SERVICE_BATCH_SIZE_BUCKETS_GLOBAL: Lazy<Vec<f64>> = Lazy::new(|| {
|
||||||
(1..=u32::try_from(Timeline::MAX_GET_VECTORED_KEYS).unwrap())
|
(1..=u32::try_from(DEFAULT_MAX_GET_VECTORED_KEYS).unwrap())
|
||||||
.map(|v| v.into())
|
.map(|v| v.into())
|
||||||
.collect()
|
.collect()
|
||||||
});
|
});
|
||||||
@@ -1957,7 +1975,7 @@ static PAGE_SERVICE_BATCH_SIZE_BUCKETS_PER_TIMELINE: Lazy<Vec<f64>> = Lazy::new(
|
|||||||
let mut buckets = Vec::new();
|
let mut buckets = Vec::new();
|
||||||
for i in 0.. {
|
for i in 0.. {
|
||||||
let bucket = 1 << i;
|
let bucket = 1 << i;
|
||||||
if bucket > u32::try_from(Timeline::MAX_GET_VECTORED_KEYS).unwrap() {
|
if bucket > u32::try_from(DEFAULT_MAX_GET_VECTORED_KEYS).unwrap() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
buckets.push(bucket.into());
|
buckets.push(bucket.into());
|
||||||
@@ -2846,7 +2864,6 @@ pub(crate) struct WalIngestMetrics {
|
|||||||
pub(crate) records_received: IntCounter,
|
pub(crate) records_received: IntCounter,
|
||||||
pub(crate) records_observed: IntCounter,
|
pub(crate) records_observed: IntCounter,
|
||||||
pub(crate) records_committed: IntCounter,
|
pub(crate) records_committed: IntCounter,
|
||||||
pub(crate) records_filtered: IntCounter,
|
|
||||||
pub(crate) values_committed_metadata_images: IntCounter,
|
pub(crate) values_committed_metadata_images: IntCounter,
|
||||||
pub(crate) values_committed_metadata_deltas: IntCounter,
|
pub(crate) values_committed_metadata_deltas: IntCounter,
|
||||||
pub(crate) values_committed_data_images: IntCounter,
|
pub(crate) values_committed_data_images: IntCounter,
|
||||||
@@ -2902,11 +2919,6 @@ pub(crate) static WAL_INGEST: Lazy<WalIngestMetrics> = Lazy::new(|| {
|
|||||||
"Number of WAL records which resulted in writes to pageserver storage"
|
"Number of WAL records which resulted in writes to pageserver storage"
|
||||||
)
|
)
|
||||||
.expect("failed to define a metric"),
|
.expect("failed to define a metric"),
|
||||||
records_filtered: register_int_counter!(
|
|
||||||
"pageserver_wal_ingest_records_filtered",
|
|
||||||
"Number of WAL records filtered out due to sharding"
|
|
||||||
)
|
|
||||||
.expect("failed to define a metric"),
|
|
||||||
values_committed_metadata_images: values_committed.with_label_values(&["metadata", "image"]),
|
values_committed_metadata_images: values_committed.with_label_values(&["metadata", "image"]),
|
||||||
values_committed_metadata_deltas: values_committed.with_label_values(&["metadata", "delta"]),
|
values_committed_metadata_deltas: values_committed.with_label_values(&["metadata", "delta"]),
|
||||||
values_committed_data_images: values_committed.with_label_values(&["data", "image"]),
|
values_committed_data_images: values_committed.with_label_values(&["data", "image"]),
|
||||||
@@ -3322,6 +3334,8 @@ impl TimelineMetrics {
|
|||||||
&timeline_id,
|
&timeline_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TIMELINE_STATE_METRIC.with_label_values(&["active"]).inc();
|
||||||
|
|
||||||
TimelineMetrics {
|
TimelineMetrics {
|
||||||
tenant_id,
|
tenant_id,
|
||||||
shard_id,
|
shard_id,
|
||||||
@@ -3476,6 +3490,8 @@ impl TimelineMetrics {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TIMELINE_STATE_METRIC.with_label_values(&["active"]).dec();
|
||||||
|
|
||||||
let tenant_id = &self.tenant_id;
|
let tenant_id = &self.tenant_id;
|
||||||
let timeline_id = &self.timeline_id;
|
let timeline_id = &self.timeline_id;
|
||||||
let shard_id = &self.shard_id;
|
let shard_id = &self.shard_id;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -431,10 +431,10 @@ impl Timeline {
|
|||||||
GetVectoredError::InvalidLsn(e) => {
|
GetVectoredError::InvalidLsn(e) => {
|
||||||
Err(anyhow::anyhow!("invalid LSN: {e:?}").into())
|
Err(anyhow::anyhow!("invalid LSN: {e:?}").into())
|
||||||
}
|
}
|
||||||
// NB: this should never happen in practice because we limit MAX_GET_VECTORED_KEYS
|
// NB: this should never happen in practice because we limit batch size to be smaller than max_get_vectored_keys
|
||||||
// TODO: we can prevent this error class by moving this check into the type system
|
// TODO: we can prevent this error class by moving this check into the type system
|
||||||
GetVectoredError::Oversized(err) => {
|
GetVectoredError::Oversized(err, max) => {
|
||||||
Err(anyhow::anyhow!("batching oversized: {err:?}").into())
|
Err(anyhow::anyhow!("batching oversized: {err} > {max}").into())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -471,8 +471,19 @@ impl Timeline {
|
|||||||
|
|
||||||
let rels = self.list_rels(spcnode, dbnode, version, ctx).await?;
|
let rels = self.list_rels(spcnode, dbnode, version, ctx).await?;
|
||||||
|
|
||||||
|
if rels.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-deserialize the rel directory to avoid duplicated work in `get_relsize_cached`.
|
||||||
|
let reldir_key = rel_dir_to_key(spcnode, dbnode);
|
||||||
|
let buf = version.get(self, reldir_key, ctx).await?;
|
||||||
|
let reldir = RelDirectory::des(&buf)?;
|
||||||
|
|
||||||
for rel in rels {
|
for rel in rels {
|
||||||
let n_blocks = self.get_rel_size(rel, version, ctx).await?;
|
let n_blocks = self
|
||||||
|
.get_rel_size_in_reldir(rel, version, Some((reldir_key, &reldir)), ctx)
|
||||||
|
.await?;
|
||||||
total_blocks += n_blocks as usize;
|
total_blocks += n_blocks as usize;
|
||||||
}
|
}
|
||||||
Ok(total_blocks)
|
Ok(total_blocks)
|
||||||
@@ -487,6 +498,19 @@ impl Timeline {
|
|||||||
tag: RelTag,
|
tag: RelTag,
|
||||||
version: Version<'_>,
|
version: Version<'_>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
|
) -> Result<BlockNumber, PageReconstructError> {
|
||||||
|
self.get_rel_size_in_reldir(tag, version, None, ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get size of a relation file. The relation must exist, otherwise an error is returned.
|
||||||
|
///
|
||||||
|
/// See [`Self::get_rel_exists_in_reldir`] on why we need `deserialized_reldir_v1`.
|
||||||
|
pub(crate) async fn get_rel_size_in_reldir(
|
||||||
|
&self,
|
||||||
|
tag: RelTag,
|
||||||
|
version: Version<'_>,
|
||||||
|
deserialized_reldir_v1: Option<(Key, &RelDirectory)>,
|
||||||
|
ctx: &RequestContext,
|
||||||
) -> Result<BlockNumber, PageReconstructError> {
|
) -> Result<BlockNumber, PageReconstructError> {
|
||||||
if tag.relnode == 0 {
|
if tag.relnode == 0 {
|
||||||
return Err(PageReconstructError::Other(
|
return Err(PageReconstructError::Other(
|
||||||
@@ -499,7 +523,9 @@ impl Timeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tag.forknum == FSM_FORKNUM || tag.forknum == VISIBILITYMAP_FORKNUM)
|
if (tag.forknum == FSM_FORKNUM || tag.forknum == VISIBILITYMAP_FORKNUM)
|
||||||
&& !self.get_rel_exists(tag, version, ctx).await?
|
&& !self
|
||||||
|
.get_rel_exists_in_reldir(tag, version, deserialized_reldir_v1, ctx)
|
||||||
|
.await?
|
||||||
{
|
{
|
||||||
// FIXME: Postgres sometimes calls smgrcreate() to create
|
// FIXME: Postgres sometimes calls smgrcreate() to create
|
||||||
// FSM, and smgrnblocks() on it immediately afterwards,
|
// FSM, and smgrnblocks() on it immediately afterwards,
|
||||||
@@ -521,11 +547,28 @@ impl Timeline {
|
|||||||
///
|
///
|
||||||
/// Only shard 0 has a full view of the relations. Other shards only know about relations that
|
/// Only shard 0 has a full view of the relations. Other shards only know about relations that
|
||||||
/// the shard stores pages for.
|
/// the shard stores pages for.
|
||||||
|
///
|
||||||
pub(crate) async fn get_rel_exists(
|
pub(crate) async fn get_rel_exists(
|
||||||
&self,
|
&self,
|
||||||
tag: RelTag,
|
tag: RelTag,
|
||||||
version: Version<'_>,
|
version: Version<'_>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
|
) -> Result<bool, PageReconstructError> {
|
||||||
|
self.get_rel_exists_in_reldir(tag, version, None, ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does the relation exist? With a cached deserialized `RelDirectory`.
|
||||||
|
///
|
||||||
|
/// There are some cases where the caller loops across all relations. In that specific case,
|
||||||
|
/// the caller should obtain the deserialized `RelDirectory` first and then call this function
|
||||||
|
/// to avoid duplicated work of deserliazation. This is a hack and should be removed by introducing
|
||||||
|
/// a new API (e.g., `get_rel_exists_batched`).
|
||||||
|
pub(crate) async fn get_rel_exists_in_reldir(
|
||||||
|
&self,
|
||||||
|
tag: RelTag,
|
||||||
|
version: Version<'_>,
|
||||||
|
deserialized_reldir_v1: Option<(Key, &RelDirectory)>,
|
||||||
|
ctx: &RequestContext,
|
||||||
) -> Result<bool, PageReconstructError> {
|
) -> Result<bool, PageReconstructError> {
|
||||||
if tag.relnode == 0 {
|
if tag.relnode == 0 {
|
||||||
return Err(PageReconstructError::Other(
|
return Err(PageReconstructError::Other(
|
||||||
@@ -568,6 +611,17 @@ impl Timeline {
|
|||||||
// fetch directory listing (old)
|
// fetch directory listing (old)
|
||||||
|
|
||||||
let key = rel_dir_to_key(tag.spcnode, tag.dbnode);
|
let key = rel_dir_to_key(tag.spcnode, tag.dbnode);
|
||||||
|
|
||||||
|
if let Some((cached_key, dir)) = deserialized_reldir_v1 {
|
||||||
|
if cached_key == key {
|
||||||
|
return Ok(dir.rels.contains(&(tag.relnode, tag.forknum)));
|
||||||
|
} else if cfg!(test) || cfg!(feature = "testing") {
|
||||||
|
panic!("cached reldir key mismatch: {cached_key} != {key}");
|
||||||
|
} else {
|
||||||
|
warn!("cached reldir key mismatch: {cached_key} != {key}");
|
||||||
|
}
|
||||||
|
// Fallback to reading the directory from the datadir.
|
||||||
|
}
|
||||||
let buf = version.get(self, key, ctx).await?;
|
let buf = version.get(self, key, ctx).await?;
|
||||||
|
|
||||||
let dir = RelDirectory::des(&buf)?;
|
let dir = RelDirectory::des(&buf)?;
|
||||||
@@ -665,7 +719,7 @@ impl Timeline {
|
|||||||
|
|
||||||
let batches = keyspace.partition(
|
let batches = keyspace.partition(
|
||||||
self.get_shard_identity(),
|
self.get_shard_identity(),
|
||||||
Timeline::MAX_GET_VECTORED_KEYS * BLCKSZ as u64,
|
self.conf.max_get_vectored_keys.get() as u64 * BLCKSZ as u64,
|
||||||
);
|
);
|
||||||
|
|
||||||
let io_concurrency = IoConcurrency::spawn_from_conf(
|
let io_concurrency = IoConcurrency::spawn_from_conf(
|
||||||
@@ -905,7 +959,7 @@ impl Timeline {
|
|||||||
|
|
||||||
let batches = keyspace.partition(
|
let batches = keyspace.partition(
|
||||||
self.get_shard_identity(),
|
self.get_shard_identity(),
|
||||||
Timeline::MAX_GET_VECTORED_KEYS * BLCKSZ as u64,
|
self.conf.max_get_vectored_keys.get() as u64 * BLCKSZ as u64,
|
||||||
);
|
);
|
||||||
|
|
||||||
let io_concurrency = IoConcurrency::spawn_from_conf(
|
let io_concurrency = IoConcurrency::spawn_from_conf(
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ use crate::l0_flush::L0FlushGlobalState;
|
|||||||
use crate::metrics::{
|
use crate::metrics::{
|
||||||
BROKEN_TENANTS_SET, CIRCUIT_BREAKERS_BROKEN, CIRCUIT_BREAKERS_UNBROKEN, CONCURRENT_INITDBS,
|
BROKEN_TENANTS_SET, CIRCUIT_BREAKERS_BROKEN, CIRCUIT_BREAKERS_UNBROKEN, CONCURRENT_INITDBS,
|
||||||
INITDB_RUN_TIME, INITDB_SEMAPHORE_ACQUISITION_TIME, TENANT, TENANT_OFFLOADED_TIMELINES,
|
INITDB_RUN_TIME, INITDB_SEMAPHORE_ACQUISITION_TIME, TENANT, TENANT_OFFLOADED_TIMELINES,
|
||||||
TENANT_STATE_METRIC, TENANT_SYNTHETIC_SIZE_METRIC, remove_tenant_metrics,
|
TENANT_STATE_METRIC, TENANT_SYNTHETIC_SIZE_METRIC, TIMELINE_STATE_METRIC,
|
||||||
|
remove_tenant_metrics,
|
||||||
};
|
};
|
||||||
use crate::task_mgr::TaskKind;
|
use crate::task_mgr::TaskKind;
|
||||||
use crate::tenant::config::LocationMode;
|
use crate::tenant::config::LocationMode;
|
||||||
@@ -544,6 +545,28 @@ pub struct OffloadedTimeline {
|
|||||||
|
|
||||||
/// Part of the `OffloadedTimeline` object's lifecycle: this needs to be set before we drop it
|
/// Part of the `OffloadedTimeline` object's lifecycle: this needs to be set before we drop it
|
||||||
pub deleted_from_ancestor: AtomicBool,
|
pub deleted_from_ancestor: AtomicBool,
|
||||||
|
|
||||||
|
_metrics_guard: OffloadedTimelineMetricsGuard,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increases the offloaded timeline count metric when created, and decreases when dropped.
|
||||||
|
struct OffloadedTimelineMetricsGuard;
|
||||||
|
|
||||||
|
impl OffloadedTimelineMetricsGuard {
|
||||||
|
fn new() -> Self {
|
||||||
|
TIMELINE_STATE_METRIC
|
||||||
|
.with_label_values(&["offloaded"])
|
||||||
|
.inc();
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for OffloadedTimelineMetricsGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
TIMELINE_STATE_METRIC
|
||||||
|
.with_label_values(&["offloaded"])
|
||||||
|
.dec();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OffloadedTimeline {
|
impl OffloadedTimeline {
|
||||||
@@ -576,6 +599,8 @@ impl OffloadedTimeline {
|
|||||||
|
|
||||||
delete_progress: timeline.delete_progress.clone(),
|
delete_progress: timeline.delete_progress.clone(),
|
||||||
deleted_from_ancestor: AtomicBool::new(false),
|
deleted_from_ancestor: AtomicBool::new(false),
|
||||||
|
|
||||||
|
_metrics_guard: OffloadedTimelineMetricsGuard::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fn from_manifest(tenant_shard_id: TenantShardId, manifest: &OffloadedTimelineManifest) -> Self {
|
fn from_manifest(tenant_shard_id: TenantShardId, manifest: &OffloadedTimelineManifest) -> Self {
|
||||||
@@ -595,6 +620,7 @@ impl OffloadedTimeline {
|
|||||||
archived_at,
|
archived_at,
|
||||||
delete_progress: TimelineDeleteProgress::default(),
|
delete_progress: TimelineDeleteProgress::default(),
|
||||||
deleted_from_ancestor: AtomicBool::new(false),
|
deleted_from_ancestor: AtomicBool::new(false),
|
||||||
|
_metrics_guard: OffloadedTimelineMetricsGuard::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn manifest(&self) -> OffloadedTimelineManifest {
|
fn manifest(&self) -> OffloadedTimelineManifest {
|
||||||
@@ -7197,7 +7223,7 @@ mod tests {
|
|||||||
let end = desc
|
let end = desc
|
||||||
.key_range
|
.key_range
|
||||||
.start
|
.start
|
||||||
.add(Timeline::MAX_GET_VECTORED_KEYS.try_into().unwrap());
|
.add(tenant.conf.max_get_vectored_keys.get() as u32);
|
||||||
reads.push(KeySpace {
|
reads.push(KeySpace {
|
||||||
ranges: vec![start..end],
|
ranges: vec![start..end],
|
||||||
});
|
});
|
||||||
@@ -11260,11 +11286,11 @@ mod tests {
|
|||||||
let mut keyspaces_at_lsn: HashMap<Lsn, KeySpaceRandomAccum> = HashMap::default();
|
let mut keyspaces_at_lsn: HashMap<Lsn, KeySpaceRandomAccum> = HashMap::default();
|
||||||
let mut used_keys: HashSet<Key> = HashSet::default();
|
let mut used_keys: HashSet<Key> = HashSet::default();
|
||||||
|
|
||||||
while used_keys.len() < Timeline::MAX_GET_VECTORED_KEYS as usize {
|
while used_keys.len() < tenant.conf.max_get_vectored_keys.get() {
|
||||||
let selected_lsn = interesting_lsns.choose(&mut random).expect("not empty");
|
let selected_lsn = interesting_lsns.choose(&mut random).expect("not empty");
|
||||||
let mut selected_key = start_key.add(random.gen_range(0..KEY_DIMENSION_SIZE));
|
let mut selected_key = start_key.add(random.gen_range(0..KEY_DIMENSION_SIZE));
|
||||||
|
|
||||||
while used_keys.len() < Timeline::MAX_GET_VECTORED_KEYS as usize {
|
while used_keys.len() < tenant.conf.max_get_vectored_keys.get() {
|
||||||
if used_keys.contains(&selected_key)
|
if used_keys.contains(&selected_key)
|
||||||
|| selected_key >= start_key.add(KEY_DIMENSION_SIZE)
|
|| selected_key >= start_key.add(KEY_DIMENSION_SIZE)
|
||||||
{
|
{
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -817,8 +817,8 @@ pub(crate) enum GetVectoredError {
|
|||||||
#[error("timeline shutting down")]
|
#[error("timeline shutting down")]
|
||||||
Cancelled,
|
Cancelled,
|
||||||
|
|
||||||
#[error("requested too many keys: {0} > {}", Timeline::MAX_GET_VECTORED_KEYS)]
|
#[error("requested too many keys: {0} > {1}")]
|
||||||
Oversized(u64),
|
Oversized(u64, u64),
|
||||||
|
|
||||||
#[error("requested at invalid LSN: {0}")]
|
#[error("requested at invalid LSN: {0}")]
|
||||||
InvalidLsn(Lsn),
|
InvalidLsn(Lsn),
|
||||||
@@ -950,6 +950,18 @@ pub(crate) enum WaitLsnError {
|
|||||||
Timeout(String),
|
Timeout(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<WaitLsnError> for tonic::Status {
|
||||||
|
fn from(err: WaitLsnError) -> Self {
|
||||||
|
use tonic::Code;
|
||||||
|
let code = match &err {
|
||||||
|
WaitLsnError::Timeout(_) => Code::Internal,
|
||||||
|
WaitLsnError::BadState(_) => Code::Internal,
|
||||||
|
WaitLsnError::Shutdown => Code::Unavailable,
|
||||||
|
};
|
||||||
|
tonic::Status::new(code, err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The impls below achieve cancellation mapping for errors.
|
// The impls below achieve cancellation mapping for errors.
|
||||||
// Perhaps there's a way of achieving this with less cruft.
|
// Perhaps there's a way of achieving this with less cruft.
|
||||||
|
|
||||||
@@ -1007,7 +1019,7 @@ impl From<GetVectoredError> for PageReconstructError {
|
|||||||
match e {
|
match e {
|
||||||
GetVectoredError::Cancelled => PageReconstructError::Cancelled,
|
GetVectoredError::Cancelled => PageReconstructError::Cancelled,
|
||||||
GetVectoredError::InvalidLsn(_) => PageReconstructError::Other(anyhow!("Invalid LSN")),
|
GetVectoredError::InvalidLsn(_) => PageReconstructError::Other(anyhow!("Invalid LSN")),
|
||||||
err @ GetVectoredError::Oversized(_) => PageReconstructError::Other(err.into()),
|
err @ GetVectoredError::Oversized(_, _) => PageReconstructError::Other(err.into()),
|
||||||
GetVectoredError::MissingKey(err) => PageReconstructError::MissingKey(err),
|
GetVectoredError::MissingKey(err) => PageReconstructError::MissingKey(err),
|
||||||
GetVectoredError::GetReadyAncestorError(err) => PageReconstructError::from(err),
|
GetVectoredError::GetReadyAncestorError(err) => PageReconstructError::from(err),
|
||||||
GetVectoredError::Other(err) => PageReconstructError::Other(err),
|
GetVectoredError::Other(err) => PageReconstructError::Other(err),
|
||||||
@@ -1043,8 +1055,8 @@ pub(crate) enum WaitLsnWaiter<'a> {
|
|||||||
/// Argument to [`Timeline::shutdown`].
|
/// Argument to [`Timeline::shutdown`].
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub(crate) enum ShutdownMode {
|
pub(crate) enum ShutdownMode {
|
||||||
/// Graceful shutdown, may do a lot of I/O as we flush any open layers to disk and then
|
/// Graceful shutdown, may do a lot of I/O as we flush any open layers to disk. This method can
|
||||||
/// also to remote storage. This method can easily take multiple seconds for a busy timeline.
|
/// take multiple seconds for a busy timeline.
|
||||||
///
|
///
|
||||||
/// While we are flushing, we continue to accept read I/O for LSNs ingested before
|
/// While we are flushing, we continue to accept read I/O for LSNs ingested before
|
||||||
/// the call to [`Timeline::shutdown`].
|
/// the call to [`Timeline::shutdown`].
|
||||||
@@ -1187,7 +1199,6 @@ impl Timeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const MAX_GET_VECTORED_KEYS: u64 = 32;
|
|
||||||
pub(crate) const LAYERS_VISITED_WARN_THRESHOLD: u32 = 100;
|
pub(crate) const LAYERS_VISITED_WARN_THRESHOLD: u32 = 100;
|
||||||
|
|
||||||
/// Look up multiple page versions at a given LSN
|
/// Look up multiple page versions at a given LSN
|
||||||
@@ -1202,9 +1213,12 @@ impl Timeline {
|
|||||||
) -> Result<BTreeMap<Key, Result<Bytes, PageReconstructError>>, GetVectoredError> {
|
) -> Result<BTreeMap<Key, Result<Bytes, PageReconstructError>>, GetVectoredError> {
|
||||||
let total_keyspace = query.total_keyspace();
|
let total_keyspace = query.total_keyspace();
|
||||||
|
|
||||||
let key_count = total_keyspace.total_raw_size().try_into().unwrap();
|
let key_count = total_keyspace.total_raw_size();
|
||||||
if key_count > Timeline::MAX_GET_VECTORED_KEYS {
|
if key_count > self.conf.max_get_vectored_keys.get() {
|
||||||
return Err(GetVectoredError::Oversized(key_count));
|
return Err(GetVectoredError::Oversized(
|
||||||
|
key_count as u64,
|
||||||
|
self.conf.max_get_vectored_keys.get() as u64,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
for range in &total_keyspace.ranges {
|
for range in &total_keyspace.ranges {
|
||||||
@@ -2492,6 +2506,13 @@ impl Timeline {
|
|||||||
// Preparing basebackup doesn't make sense for shards other than shard zero.
|
// Preparing basebackup doesn't make sense for shards other than shard zero.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if !self.is_active() {
|
||||||
|
// May happen during initial timeline creation.
|
||||||
|
// Such timeline is not in the global timeline map yet,
|
||||||
|
// so basebackup cache will not be able to find it.
|
||||||
|
// TODO(diko): We can prepare such timelines in finish_creation().
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let res = self
|
let res = self
|
||||||
.basebackup_prepare_sender
|
.basebackup_prepare_sender
|
||||||
@@ -2831,21 +2852,6 @@ impl Timeline {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the effective WAL receiver protocol to use for this tenant.
|
|
||||||
///
|
|
||||||
/// Priority order is:
|
|
||||||
/// 1. Tenant config override
|
|
||||||
/// 2. Default value for tenant config override
|
|
||||||
/// 3. Pageserver config override
|
|
||||||
/// 4. Pageserver config default
|
|
||||||
pub fn resolve_wal_receiver_protocol(&self) -> PostgresClientProtocol {
|
|
||||||
let tenant_conf = self.tenant_conf.load().tenant_conf.clone();
|
|
||||||
tenant_conf
|
|
||||||
.wal_receiver_protocol_override
|
|
||||||
.or(self.conf.default_tenant_conf.wal_receiver_protocol_override)
|
|
||||||
.unwrap_or(self.conf.wal_receiver_protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn tenant_conf_updated(&self, new_conf: &AttachedTenantConf) {
|
pub(super) fn tenant_conf_updated(&self, new_conf: &AttachedTenantConf) {
|
||||||
// NB: Most tenant conf options are read by background loops, so,
|
// NB: Most tenant conf options are read by background loops, so,
|
||||||
// changes will automatically be picked up.
|
// changes will automatically be picked up.
|
||||||
@@ -3201,10 +3207,16 @@ impl Timeline {
|
|||||||
guard.is_none(),
|
guard.is_none(),
|
||||||
"multiple launches / re-launches of WAL receiver are not supported"
|
"multiple launches / re-launches of WAL receiver are not supported"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let protocol = PostgresClientProtocol::Interpreted {
|
||||||
|
format: utils::postgres_client::InterpretedFormat::Protobuf,
|
||||||
|
compression: Some(utils::postgres_client::Compression::Zstd { level: 1 }),
|
||||||
|
};
|
||||||
|
|
||||||
*guard = Some(WalReceiver::start(
|
*guard = Some(WalReceiver::start(
|
||||||
Arc::clone(self),
|
Arc::clone(self),
|
||||||
WalReceiverConf {
|
WalReceiverConf {
|
||||||
protocol: self.resolve_wal_receiver_protocol(),
|
protocol,
|
||||||
wal_connect_timeout,
|
wal_connect_timeout,
|
||||||
lagging_wal_timeout,
|
lagging_wal_timeout,
|
||||||
max_lsn_wal_lag,
|
max_lsn_wal_lag,
|
||||||
@@ -5258,7 +5270,7 @@ impl Timeline {
|
|||||||
key = key.next();
|
key = key.next();
|
||||||
|
|
||||||
// Maybe flush `key_rest_accum`
|
// Maybe flush `key_rest_accum`
|
||||||
if key_request_accum.raw_size() >= Timeline::MAX_GET_VECTORED_KEYS
|
if key_request_accum.raw_size() >= self.conf.max_get_vectored_keys.get() as u64
|
||||||
|| (last_key_in_range && key_request_accum.raw_size() > 0)
|
|| (last_key_in_range && key_request_accum.raw_size() > 0)
|
||||||
{
|
{
|
||||||
let query =
|
let query =
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ pub async fn doit(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracing::info!("Import plan executed. Flushing remote changes and notifying storcon");
|
||||||
|
|
||||||
timeline
|
timeline
|
||||||
.remote_client
|
.remote_client
|
||||||
.schedule_index_upload_for_file_changes()?;
|
.schedule_index_upload_for_file_changes()?;
|
||||||
@@ -199,8 +201,8 @@ async fn prepare_import(
|
|||||||
.await;
|
.await;
|
||||||
match res {
|
match res {
|
||||||
Ok(_) => break,
|
Ok(_) => break,
|
||||||
Err(err) => {
|
Err(_err) => {
|
||||||
info!(?err, "indefinitely waiting for pgdata to finish");
|
info!("indefinitely waiting for pgdata to finish");
|
||||||
if tokio::time::timeout(std::time::Duration::from_secs(10), cancel.cancelled())
|
if tokio::time::timeout(std::time::Duration::from_secs(10), cancel.cancelled())
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ async fn run_v1(
|
|||||||
.unwrap(),
|
.unwrap(),
|
||||||
import_job_concurrency: base.import_job_concurrency,
|
import_job_concurrency: base.import_job_concurrency,
|
||||||
import_job_checkpoint_threshold: base.import_job_checkpoint_threshold,
|
import_job_checkpoint_threshold: base.import_job_checkpoint_threshold,
|
||||||
|
import_job_max_byte_range_size: base.import_job_max_byte_range_size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => timeline.conf.timeline_import_config.clone(),
|
None => timeline.conf.timeline_import_config.clone(),
|
||||||
@@ -130,7 +131,15 @@ async fn run_v1(
|
|||||||
|
|
||||||
pausable_failpoint!("import-timeline-pre-execute-pausable");
|
pausable_failpoint!("import-timeline-pre-execute-pausable");
|
||||||
|
|
||||||
|
let jobs_count = import_progress.as_ref().map(|p| p.jobs);
|
||||||
let start_from_job_idx = import_progress.map(|progress| progress.completed);
|
let start_from_job_idx = import_progress.map(|progress| progress.completed);
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
start_from_job_idx=?start_from_job_idx,
|
||||||
|
jobs=?jobs_count,
|
||||||
|
"Executing import plan"
|
||||||
|
);
|
||||||
|
|
||||||
plan.execute(timeline, start_from_job_idx, plan_hash, &import_config, ctx)
|
plan.execute(timeline, start_from_job_idx, plan_hash, &import_config, ctx)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -433,6 +442,7 @@ impl Plan {
|
|||||||
|
|
||||||
let mut last_completed_job_idx = start_after_job_idx.unwrap_or(0);
|
let mut last_completed_job_idx = start_after_job_idx.unwrap_or(0);
|
||||||
let checkpoint_every: usize = import_config.import_job_checkpoint_threshold.into();
|
let checkpoint_every: usize = import_config.import_job_checkpoint_threshold.into();
|
||||||
|
let max_byte_range_size: usize = import_config.import_job_max_byte_range_size.into();
|
||||||
|
|
||||||
// Run import jobs concurrently up to the limit specified by the pageserver configuration.
|
// Run import jobs concurrently up to the limit specified by the pageserver configuration.
|
||||||
// Note that we process completed futures in the oreder of insertion. This will be the
|
// Note that we process completed futures in the oreder of insertion. This will be the
|
||||||
@@ -448,7 +458,7 @@ impl Plan {
|
|||||||
|
|
||||||
work.push_back(tokio::task::spawn(async move {
|
work.push_back(tokio::task::spawn(async move {
|
||||||
let _permit = permit;
|
let _permit = permit;
|
||||||
let res = job.run(job_timeline, &ctx).await;
|
let res = job.run(job_timeline, max_byte_range_size, &ctx).await;
|
||||||
(job_idx, res)
|
(job_idx, res)
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
@@ -463,6 +473,8 @@ impl Plan {
|
|||||||
last_completed_job_idx = job_idx;
|
last_completed_job_idx = job_idx;
|
||||||
|
|
||||||
if last_completed_job_idx % checkpoint_every == 0 {
|
if last_completed_job_idx % checkpoint_every == 0 {
|
||||||
|
tracing::info!(last_completed_job_idx, jobs=%jobs_in_plan, "Checkpointing import status");
|
||||||
|
|
||||||
let progress = ShardImportProgressV1 {
|
let progress = ShardImportProgressV1 {
|
||||||
jobs: jobs_in_plan,
|
jobs: jobs_in_plan,
|
||||||
completed: last_completed_job_idx,
|
completed: last_completed_job_idx,
|
||||||
@@ -669,6 +681,7 @@ trait ImportTask {
|
|||||||
async fn doit(
|
async fn doit(
|
||||||
self,
|
self,
|
||||||
layer_writer: &mut ImageLayerWriter,
|
layer_writer: &mut ImageLayerWriter,
|
||||||
|
max_byte_range_size: usize,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<usize>;
|
) -> anyhow::Result<usize>;
|
||||||
}
|
}
|
||||||
@@ -705,6 +718,7 @@ impl ImportTask for ImportSingleKeyTask {
|
|||||||
async fn doit(
|
async fn doit(
|
||||||
self,
|
self,
|
||||||
layer_writer: &mut ImageLayerWriter,
|
layer_writer: &mut ImageLayerWriter,
|
||||||
|
_max_byte_range_size: usize,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<usize> {
|
) -> anyhow::Result<usize> {
|
||||||
layer_writer.put_image(self.key, self.buf, ctx).await?;
|
layer_writer.put_image(self.key, self.buf, ctx).await?;
|
||||||
@@ -758,10 +772,9 @@ impl ImportTask for ImportRelBlocksTask {
|
|||||||
async fn doit(
|
async fn doit(
|
||||||
self,
|
self,
|
||||||
layer_writer: &mut ImageLayerWriter,
|
layer_writer: &mut ImageLayerWriter,
|
||||||
|
max_byte_range_size: usize,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<usize> {
|
) -> anyhow::Result<usize> {
|
||||||
const MAX_BYTE_RANGE_SIZE: usize = 128 * 1024 * 1024;
|
|
||||||
|
|
||||||
debug!("Importing relation file");
|
debug!("Importing relation file");
|
||||||
|
|
||||||
let (rel_tag, start_blk) = self.key_range.start.to_rel_block()?;
|
let (rel_tag, start_blk) = self.key_range.start.to_rel_block()?;
|
||||||
@@ -786,7 +799,7 @@ impl ImportTask for ImportRelBlocksTask {
|
|||||||
assert_eq!(key.len(), 1);
|
assert_eq!(key.len(), 1);
|
||||||
assert!(!acc.is_empty());
|
assert!(!acc.is_empty());
|
||||||
assert!(acc_end > acc_start);
|
assert!(acc_end > acc_start);
|
||||||
if acc_end == start && end - acc_start <= MAX_BYTE_RANGE_SIZE {
|
if acc_end == start && end - acc_start <= max_byte_range_size {
|
||||||
acc.push(key.pop().unwrap());
|
acc.push(key.pop().unwrap());
|
||||||
Ok((acc, acc_start, end))
|
Ok((acc, acc_start, end))
|
||||||
} else {
|
} else {
|
||||||
@@ -850,6 +863,7 @@ impl ImportTask for ImportSlruBlocksTask {
|
|||||||
async fn doit(
|
async fn doit(
|
||||||
self,
|
self,
|
||||||
layer_writer: &mut ImageLayerWriter,
|
layer_writer: &mut ImageLayerWriter,
|
||||||
|
_max_byte_range_size: usize,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<usize> {
|
) -> anyhow::Result<usize> {
|
||||||
debug!("Importing SLRU segment file {}", self.path);
|
debug!("Importing SLRU segment file {}", self.path);
|
||||||
@@ -896,12 +910,13 @@ impl ImportTask for AnyImportTask {
|
|||||||
async fn doit(
|
async fn doit(
|
||||||
self,
|
self,
|
||||||
layer_writer: &mut ImageLayerWriter,
|
layer_writer: &mut ImageLayerWriter,
|
||||||
|
max_byte_range_size: usize,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<usize> {
|
) -> anyhow::Result<usize> {
|
||||||
match self {
|
match self {
|
||||||
Self::SingleKey(t) => t.doit(layer_writer, ctx).await,
|
Self::SingleKey(t) => t.doit(layer_writer, max_byte_range_size, ctx).await,
|
||||||
Self::RelBlocks(t) => t.doit(layer_writer, ctx).await,
|
Self::RelBlocks(t) => t.doit(layer_writer, max_byte_range_size, ctx).await,
|
||||||
Self::SlruBlocks(t) => t.doit(layer_writer, ctx).await,
|
Self::SlruBlocks(t) => t.doit(layer_writer, max_byte_range_size, ctx).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -942,7 +957,12 @@ impl ChunkProcessingJob {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(self, timeline: Arc<Timeline>, ctx: &RequestContext) -> anyhow::Result<()> {
|
async fn run(
|
||||||
|
self,
|
||||||
|
timeline: Arc<Timeline>,
|
||||||
|
max_byte_range_size: usize,
|
||||||
|
ctx: &RequestContext,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
let mut writer = ImageLayerWriter::new(
|
let mut writer = ImageLayerWriter::new(
|
||||||
timeline.conf,
|
timeline.conf,
|
||||||
timeline.timeline_id,
|
timeline.timeline_id,
|
||||||
@@ -957,7 +977,7 @@ impl ChunkProcessingJob {
|
|||||||
|
|
||||||
let mut nimages = 0;
|
let mut nimages = 0;
|
||||||
for task in self.tasks {
|
for task in self.tasks {
|
||||||
nimages += task.doit(&mut writer, ctx).await?;
|
nimages += task.doit(&mut writer, max_byte_range_size, ctx).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let resident_layer = if nimages > 0 {
|
let resident_layer = if nimages > 0 {
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ impl WalReceiver {
|
|||||||
}
|
}
|
||||||
connection_manager_state.shutdown().await;
|
connection_manager_state.shutdown().await;
|
||||||
*loop_status.write().unwrap() = None;
|
*loop_status.write().unwrap() = None;
|
||||||
debug!("task exits");
|
info!("task exits");
|
||||||
}
|
}
|
||||||
.instrument(info_span!(parent: None, "wal_connection_manager", tenant_id = %tenant_shard_id.tenant_id, shard_id = %tenant_shard_id.shard_slug(), timeline_id = %timeline_id))
|
.instrument(info_span!(parent: None, "wal_connection_manager", tenant_id = %tenant_shard_id.tenant_id, shard_id = %tenant_shard_id.shard_slug(), timeline_id = %timeline_id))
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ use utils::backoff::{
|
|||||||
};
|
};
|
||||||
use utils::id::{NodeId, TenantTimelineId};
|
use utils::id::{NodeId, TenantTimelineId};
|
||||||
use utils::lsn::Lsn;
|
use utils::lsn::Lsn;
|
||||||
use utils::postgres_client::{
|
use utils::postgres_client::{ConnectionConfigArgs, wal_stream_connection_config};
|
||||||
ConnectionConfigArgs, PostgresClientProtocol, wal_stream_connection_config,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::walreceiver_connection::{WalConnectionStatus, WalReceiverError};
|
use super::walreceiver_connection::{WalConnectionStatus, WalReceiverError};
|
||||||
use super::{TaskEvent, TaskHandle, TaskStateUpdate, WalReceiverConf};
|
use super::{TaskEvent, TaskHandle, TaskStateUpdate, WalReceiverConf};
|
||||||
@@ -991,19 +989,12 @@ impl ConnectionManagerState {
|
|||||||
return None; // no connection string, ignore sk
|
return None; // no connection string, ignore sk
|
||||||
}
|
}
|
||||||
|
|
||||||
let (shard_number, shard_count, shard_stripe_size) = match self.conf.protocol {
|
let shard_identity = self.timeline.get_shard_identity();
|
||||||
PostgresClientProtocol::Vanilla => {
|
let (shard_number, shard_count, shard_stripe_size) = (
|
||||||
(None, None, None)
|
Some(shard_identity.number.0),
|
||||||
},
|
Some(shard_identity.count.0),
|
||||||
PostgresClientProtocol::Interpreted { .. } => {
|
Some(shard_identity.stripe_size.0),
|
||||||
let shard_identity = self.timeline.get_shard_identity();
|
);
|
||||||
(
|
|
||||||
Some(shard_identity.number.0),
|
|
||||||
Some(shard_identity.count.0),
|
|
||||||
Some(shard_identity.stripe_size.0),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let connection_conf_args = ConnectionConfigArgs {
|
let connection_conf_args = ConnectionConfigArgs {
|
||||||
protocol: self.conf.protocol,
|
protocol: self.conf.protocol,
|
||||||
@@ -1120,8 +1111,8 @@ impl ReconnectReason {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use pageserver_api::config::defaults::DEFAULT_WAL_RECEIVER_PROTOCOL;
|
|
||||||
use url::Host;
|
use url::Host;
|
||||||
|
use utils::postgres_client::PostgresClientProtocol;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tenant::harness::{TIMELINE_ID, TenantHarness};
|
use crate::tenant::harness::{TIMELINE_ID, TenantHarness};
|
||||||
@@ -1552,6 +1543,11 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to create an empty timeline for dummy wal connection manager");
|
.expect("Failed to create an empty timeline for dummy wal connection manager");
|
||||||
|
|
||||||
|
let protocol = PostgresClientProtocol::Interpreted {
|
||||||
|
format: utils::postgres_client::InterpretedFormat::Protobuf,
|
||||||
|
compression: Some(utils::postgres_client::Compression::Zstd { level: 1 }),
|
||||||
|
};
|
||||||
|
|
||||||
ConnectionManagerState {
|
ConnectionManagerState {
|
||||||
id: TenantTimelineId {
|
id: TenantTimelineId {
|
||||||
tenant_id: harness.tenant_shard_id.tenant_id,
|
tenant_id: harness.tenant_shard_id.tenant_id,
|
||||||
@@ -1560,7 +1556,7 @@ mod tests {
|
|||||||
timeline,
|
timeline,
|
||||||
cancel: CancellationToken::new(),
|
cancel: CancellationToken::new(),
|
||||||
conf: WalReceiverConf {
|
conf: WalReceiverConf {
|
||||||
protocol: DEFAULT_WAL_RECEIVER_PROTOCOL,
|
protocol,
|
||||||
wal_connect_timeout: Duration::from_secs(1),
|
wal_connect_timeout: Duration::from_secs(1),
|
||||||
lagging_wal_timeout: Duration::from_secs(1),
|
lagging_wal_timeout: Duration::from_secs(1),
|
||||||
max_lsn_wal_lag: NonZeroU64::new(1024 * 1024).unwrap(),
|
max_lsn_wal_lag: NonZeroU64::new(1024 * 1024).unwrap(),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use postgres_backend::is_expected_io_error;
|
|||||||
use postgres_connection::PgConnectionConfig;
|
use postgres_connection::PgConnectionConfig;
|
||||||
use postgres_ffi::WAL_SEGMENT_SIZE;
|
use postgres_ffi::WAL_SEGMENT_SIZE;
|
||||||
use postgres_ffi::v14::xlog_utils::normalize_lsn;
|
use postgres_ffi::v14::xlog_utils::normalize_lsn;
|
||||||
use postgres_ffi::waldecoder::{WalDecodeError, WalStreamDecoder};
|
use postgres_ffi::waldecoder::WalDecodeError;
|
||||||
use postgres_protocol::message::backend::ReplicationMessage;
|
use postgres_protocol::message::backend::ReplicationMessage;
|
||||||
use postgres_types::PgLsn;
|
use postgres_types::PgLsn;
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
@@ -31,7 +31,7 @@ use utils::lsn::Lsn;
|
|||||||
use utils::pageserver_feedback::PageserverFeedback;
|
use utils::pageserver_feedback::PageserverFeedback;
|
||||||
use utils::postgres_client::PostgresClientProtocol;
|
use utils::postgres_client::PostgresClientProtocol;
|
||||||
use utils::sync::gate::GateError;
|
use utils::sync::gate::GateError;
|
||||||
use wal_decoder::models::{FlushUncommittedRecords, InterpretedWalRecord, InterpretedWalRecords};
|
use wal_decoder::models::{FlushUncommittedRecords, InterpretedWalRecords};
|
||||||
use wal_decoder::wire_format::FromWireFormat;
|
use wal_decoder::wire_format::FromWireFormat;
|
||||||
|
|
||||||
use super::TaskStateUpdate;
|
use super::TaskStateUpdate;
|
||||||
@@ -275,8 +275,6 @@ pub(super) async fn handle_walreceiver_connection(
|
|||||||
let copy_stream = replication_client.copy_both_simple(&query).await?;
|
let copy_stream = replication_client.copy_both_simple(&query).await?;
|
||||||
let mut physical_stream = pin!(ReplicationStream::new(copy_stream));
|
let mut physical_stream = pin!(ReplicationStream::new(copy_stream));
|
||||||
|
|
||||||
let mut waldecoder = WalStreamDecoder::new(startpoint, timeline.pg_version);
|
|
||||||
|
|
||||||
let mut walingest = WalIngest::new(timeline.as_ref(), startpoint, &ctx)
|
let mut walingest = WalIngest::new(timeline.as_ref(), startpoint, &ctx)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| match e.kind {
|
.map_err(|e| match e.kind {
|
||||||
@@ -284,19 +282,22 @@ pub(super) async fn handle_walreceiver_connection(
|
|||||||
_ => WalReceiverError::Other(e.into()),
|
_ => WalReceiverError::Other(e.into()),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let shard = vec![*timeline.get_shard_identity()];
|
let (format, compression) = match protocol {
|
||||||
|
|
||||||
let interpreted_proto_config = match protocol {
|
|
||||||
PostgresClientProtocol::Vanilla => None,
|
|
||||||
PostgresClientProtocol::Interpreted {
|
PostgresClientProtocol::Interpreted {
|
||||||
format,
|
format,
|
||||||
compression,
|
compression,
|
||||||
} => Some((format, compression)),
|
} => (format, compression),
|
||||||
|
PostgresClientProtocol::Vanilla => {
|
||||||
|
return Err(WalReceiverError::Other(anyhow!(
|
||||||
|
"Vanilla WAL receiver protocol is no longer supported for ingest"
|
||||||
|
)));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut expected_wal_start = startpoint;
|
let mut expected_wal_start = startpoint;
|
||||||
while let Some(replication_message) = {
|
while let Some(replication_message) = {
|
||||||
select! {
|
select! {
|
||||||
|
biased;
|
||||||
_ = cancellation.cancelled() => {
|
_ = cancellation.cancelled() => {
|
||||||
debug!("walreceiver interrupted");
|
debug!("walreceiver interrupted");
|
||||||
None
|
None
|
||||||
@@ -312,16 +313,6 @@ pub(super) async fn handle_walreceiver_connection(
|
|||||||
// Update the connection status before processing the message. If the message processing
|
// Update the connection status before processing the message. If the message processing
|
||||||
// fails (e.g. in walingest), we still want to know latests LSNs from the safekeeper.
|
// fails (e.g. in walingest), we still want to know latests LSNs from the safekeeper.
|
||||||
match &replication_message {
|
match &replication_message {
|
||||||
ReplicationMessage::XLogData(xlog_data) => {
|
|
||||||
connection_status.latest_connection_update = now;
|
|
||||||
connection_status.commit_lsn = Some(Lsn::from(xlog_data.wal_end()));
|
|
||||||
connection_status.streaming_lsn = Some(Lsn::from(
|
|
||||||
xlog_data.wal_start() + xlog_data.data().len() as u64,
|
|
||||||
));
|
|
||||||
if !xlog_data.data().is_empty() {
|
|
||||||
connection_status.latest_wal_update = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ReplicationMessage::PrimaryKeepAlive(keepalive) => {
|
ReplicationMessage::PrimaryKeepAlive(keepalive) => {
|
||||||
connection_status.latest_connection_update = now;
|
connection_status.latest_connection_update = now;
|
||||||
connection_status.commit_lsn = Some(Lsn::from(keepalive.wal_end()));
|
connection_status.commit_lsn = Some(Lsn::from(keepalive.wal_end()));
|
||||||
@@ -352,7 +343,6 @@ pub(super) async fn handle_walreceiver_connection(
|
|||||||
// were interpreted.
|
// were interpreted.
|
||||||
let streaming_lsn = Lsn::from(raw.streaming_lsn());
|
let streaming_lsn = Lsn::from(raw.streaming_lsn());
|
||||||
|
|
||||||
let (format, compression) = interpreted_proto_config.unwrap();
|
|
||||||
let batch = InterpretedWalRecords::from_wire(raw.data(), format, compression)
|
let batch = InterpretedWalRecords::from_wire(raw.data(), format, compression)
|
||||||
.await
|
.await
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
@@ -508,138 +498,6 @@ pub(super) async fn handle_walreceiver_connection(
|
|||||||
Some(streaming_lsn)
|
Some(streaming_lsn)
|
||||||
}
|
}
|
||||||
|
|
||||||
ReplicationMessage::XLogData(xlog_data) => {
|
|
||||||
async fn commit(
|
|
||||||
modification: &mut DatadirModification<'_>,
|
|
||||||
uncommitted: &mut u64,
|
|
||||||
filtered: &mut u64,
|
|
||||||
ctx: &RequestContext,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let stats = modification.stats();
|
|
||||||
modification.commit(ctx).await?;
|
|
||||||
WAL_INGEST
|
|
||||||
.records_committed
|
|
||||||
.inc_by(*uncommitted - *filtered);
|
|
||||||
WAL_INGEST.inc_values_committed(&stats);
|
|
||||||
*uncommitted = 0;
|
|
||||||
*filtered = 0;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass the WAL data to the decoder, and see if we can decode
|
|
||||||
// more records as a result.
|
|
||||||
let data = xlog_data.data();
|
|
||||||
let startlsn = Lsn::from(xlog_data.wal_start());
|
|
||||||
let endlsn = startlsn + data.len() as u64;
|
|
||||||
|
|
||||||
trace!("received XLogData between {startlsn} and {endlsn}");
|
|
||||||
|
|
||||||
WAL_INGEST.bytes_received.inc_by(data.len() as u64);
|
|
||||||
waldecoder.feed_bytes(data);
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut modification = timeline.begin_modification(startlsn);
|
|
||||||
let mut uncommitted_records = 0;
|
|
||||||
let mut filtered_records = 0;
|
|
||||||
|
|
||||||
while let Some((next_record_lsn, recdata)) = waldecoder.poll_decode()? {
|
|
||||||
// It is important to deal with the aligned records as lsn in getPage@LSN is
|
|
||||||
// aligned and can be several bytes bigger. Without this alignment we are
|
|
||||||
// at risk of hitting a deadlock.
|
|
||||||
if !next_record_lsn.is_aligned() {
|
|
||||||
return Err(WalReceiverError::Other(anyhow!("LSN not aligned")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deserialize and interpret WAL record
|
|
||||||
let interpreted = InterpretedWalRecord::from_bytes_filtered(
|
|
||||||
recdata,
|
|
||||||
&shard,
|
|
||||||
next_record_lsn,
|
|
||||||
modification.tline.pg_version,
|
|
||||||
)?
|
|
||||||
.remove(timeline.get_shard_identity())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if matches!(interpreted.flush_uncommitted, FlushUncommittedRecords::Yes)
|
|
||||||
&& uncommitted_records > 0
|
|
||||||
{
|
|
||||||
// Special case: legacy PG database creations operate by reading pages from a 'template' database:
|
|
||||||
// these are the only kinds of WAL record that require reading data blocks while ingesting. Ensure
|
|
||||||
// all earlier writes of data blocks are visible by committing any modification in flight.
|
|
||||||
commit(
|
|
||||||
&mut modification,
|
|
||||||
&mut uncommitted_records,
|
|
||||||
&mut filtered_records,
|
|
||||||
&ctx,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ingest the records without immediately committing them.
|
|
||||||
timeline.metrics.wal_records_received.inc();
|
|
||||||
let ingested = walingest
|
|
||||||
.ingest_record(interpreted, &mut modification, &ctx)
|
|
||||||
.await
|
|
||||||
.with_context(|| {
|
|
||||||
format!("could not ingest record at {next_record_lsn}")
|
|
||||||
})
|
|
||||||
.inspect_err(|err| {
|
|
||||||
// TODO: we can't differentiate cancellation errors with
|
|
||||||
// anyhow::Error, so just ignore it if we're cancelled.
|
|
||||||
if !cancellation.is_cancelled() && !timeline.is_stopping() {
|
|
||||||
critical!("{err:?}")
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
if !ingested {
|
|
||||||
tracing::debug!("ingest: filtered out record @ LSN {next_record_lsn}");
|
|
||||||
WAL_INGEST.records_filtered.inc();
|
|
||||||
filtered_records += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: this cannot be made pausable_failpoint without fixing the
|
|
||||||
// failpoint library; in tests, the added amount of debugging will cause us
|
|
||||||
// to timeout the tests.
|
|
||||||
fail_point!("walreceiver-after-ingest");
|
|
||||||
|
|
||||||
last_rec_lsn = next_record_lsn;
|
|
||||||
|
|
||||||
// Commit every ingest_batch_size records. Even if we filtered out
|
|
||||||
// all records, we still need to call commit to advance the LSN.
|
|
||||||
uncommitted_records += 1;
|
|
||||||
if uncommitted_records >= ingest_batch_size
|
|
||||||
|| modification.approx_pending_bytes()
|
|
||||||
> DatadirModification::MAX_PENDING_BYTES
|
|
||||||
{
|
|
||||||
commit(
|
|
||||||
&mut modification,
|
|
||||||
&mut uncommitted_records,
|
|
||||||
&mut filtered_records,
|
|
||||||
&ctx,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit the remaining records.
|
|
||||||
if uncommitted_records > 0 {
|
|
||||||
commit(
|
|
||||||
&mut modification,
|
|
||||||
&mut uncommitted_records,
|
|
||||||
&mut filtered_records,
|
|
||||||
&ctx,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !caught_up && endlsn >= end_of_wal {
|
|
||||||
info!("caught up at LSN {endlsn}");
|
|
||||||
caught_up = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(endlsn)
|
|
||||||
}
|
|
||||||
|
|
||||||
ReplicationMessage::PrimaryKeepAlive(keepalive) => {
|
ReplicationMessage::PrimaryKeepAlive(keepalive) => {
|
||||||
let wal_end = keepalive.wal_end();
|
let wal_end = keepalive.wal_end();
|
||||||
let timestamp = keepalive.timestamp();
|
let timestamp = keepalive.timestamp();
|
||||||
|
|||||||
@@ -1092,13 +1092,15 @@ communicator_prefetch_register_bufferv(BufferTag tag, neon_request_lsns *frlsns,
|
|||||||
MyPState->ring_last <= ring_index);
|
MyPState->ring_last <= ring_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* internal version. Returns the ring index */
|
/* Internal version. Returns the ring index of the last block (result of this function is used only
|
||||||
|
* when nblocks==1)
|
||||||
|
*/
|
||||||
static uint64
|
static uint64
|
||||||
prefetch_register_bufferv(BufferTag tag, neon_request_lsns *frlsns,
|
prefetch_register_bufferv(BufferTag tag, neon_request_lsns *frlsns,
|
||||||
BlockNumber nblocks, const bits8 *mask,
|
BlockNumber nblocks, const bits8 *mask,
|
||||||
bool is_prefetch)
|
bool is_prefetch)
|
||||||
{
|
{
|
||||||
uint64 min_ring_index;
|
uint64 last_ring_index;
|
||||||
PrefetchRequest hashkey;
|
PrefetchRequest hashkey;
|
||||||
#ifdef USE_ASSERT_CHECKING
|
#ifdef USE_ASSERT_CHECKING
|
||||||
bool any_hits = false;
|
bool any_hits = false;
|
||||||
@@ -1122,13 +1124,12 @@ Retry:
|
|||||||
MyPState->ring_unused - MyPState->ring_receive;
|
MyPState->ring_unused - MyPState->ring_receive;
|
||||||
MyNeonCounters->getpage_prefetches_buffered =
|
MyNeonCounters->getpage_prefetches_buffered =
|
||||||
MyPState->n_responses_buffered;
|
MyPState->n_responses_buffered;
|
||||||
|
last_ring_index = UINT64_MAX;
|
||||||
|
|
||||||
min_ring_index = UINT64_MAX;
|
|
||||||
for (int i = 0; i < nblocks; i++)
|
for (int i = 0; i < nblocks; i++)
|
||||||
{
|
{
|
||||||
PrefetchRequest *slot = NULL;
|
PrefetchRequest *slot = NULL;
|
||||||
PrfHashEntry *entry = NULL;
|
PrfHashEntry *entry = NULL;
|
||||||
uint64 ring_index;
|
|
||||||
neon_request_lsns *lsns;
|
neon_request_lsns *lsns;
|
||||||
|
|
||||||
if (PointerIsValid(mask) && BITMAP_ISSET(mask, i))
|
if (PointerIsValid(mask) && BITMAP_ISSET(mask, i))
|
||||||
@@ -1152,12 +1153,12 @@ Retry:
|
|||||||
if (entry != NULL)
|
if (entry != NULL)
|
||||||
{
|
{
|
||||||
slot = entry->slot;
|
slot = entry->slot;
|
||||||
ring_index = slot->my_ring_index;
|
last_ring_index = slot->my_ring_index;
|
||||||
Assert(slot == GetPrfSlot(ring_index));
|
Assert(slot == GetPrfSlot(last_ring_index));
|
||||||
|
|
||||||
Assert(slot->status != PRFS_UNUSED);
|
Assert(slot->status != PRFS_UNUSED);
|
||||||
Assert(MyPState->ring_last <= ring_index &&
|
Assert(MyPState->ring_last <= last_ring_index &&
|
||||||
ring_index < MyPState->ring_unused);
|
last_ring_index < MyPState->ring_unused);
|
||||||
Assert(BufferTagsEqual(&slot->buftag, &hashkey.buftag));
|
Assert(BufferTagsEqual(&slot->buftag, &hashkey.buftag));
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1169,9 +1170,9 @@ Retry:
|
|||||||
if (!neon_prefetch_response_usable(lsns, slot))
|
if (!neon_prefetch_response_usable(lsns, slot))
|
||||||
{
|
{
|
||||||
/* Wait for the old request to finish and discard it */
|
/* Wait for the old request to finish and discard it */
|
||||||
if (!prefetch_wait_for(ring_index))
|
if (!prefetch_wait_for(last_ring_index))
|
||||||
goto Retry;
|
goto Retry;
|
||||||
prefetch_set_unused(ring_index);
|
prefetch_set_unused(last_ring_index);
|
||||||
entry = NULL;
|
entry = NULL;
|
||||||
slot = NULL;
|
slot = NULL;
|
||||||
pgBufferUsage.prefetch.expired += 1;
|
pgBufferUsage.prefetch.expired += 1;
|
||||||
@@ -1188,13 +1189,12 @@ Retry:
|
|||||||
*/
|
*/
|
||||||
if (slot->status == PRFS_TAG_REMAINS)
|
if (slot->status == PRFS_TAG_REMAINS)
|
||||||
{
|
{
|
||||||
prefetch_set_unused(ring_index);
|
prefetch_set_unused(last_ring_index);
|
||||||
entry = NULL;
|
entry = NULL;
|
||||||
slot = NULL;
|
slot = NULL;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
min_ring_index = Min(min_ring_index, ring_index);
|
|
||||||
/* The buffered request is good enough, return that index */
|
/* The buffered request is good enough, return that index */
|
||||||
if (is_prefetch)
|
if (is_prefetch)
|
||||||
pgBufferUsage.prefetch.duplicates++;
|
pgBufferUsage.prefetch.duplicates++;
|
||||||
@@ -1283,12 +1283,12 @@ Retry:
|
|||||||
* The next buffer pointed to by `ring_unused` is now definitely empty, so
|
* The next buffer pointed to by `ring_unused` is now definitely empty, so
|
||||||
* we can insert the new request to it.
|
* we can insert the new request to it.
|
||||||
*/
|
*/
|
||||||
ring_index = MyPState->ring_unused;
|
last_ring_index = MyPState->ring_unused;
|
||||||
|
|
||||||
Assert(MyPState->ring_last <= ring_index &&
|
Assert(MyPState->ring_last <= last_ring_index &&
|
||||||
ring_index <= MyPState->ring_unused);
|
last_ring_index <= MyPState->ring_unused);
|
||||||
|
|
||||||
slot = GetPrfSlotNoCheck(ring_index);
|
slot = GetPrfSlotNoCheck(last_ring_index);
|
||||||
|
|
||||||
Assert(slot->status == PRFS_UNUSED);
|
Assert(slot->status == PRFS_UNUSED);
|
||||||
|
|
||||||
@@ -1298,11 +1298,9 @@ Retry:
|
|||||||
*/
|
*/
|
||||||
slot->buftag = hashkey.buftag;
|
slot->buftag = hashkey.buftag;
|
||||||
slot->shard_no = get_shard_number(&tag);
|
slot->shard_no = get_shard_number(&tag);
|
||||||
slot->my_ring_index = ring_index;
|
slot->my_ring_index = last_ring_index;
|
||||||
slot->flags = 0;
|
slot->flags = 0;
|
||||||
|
|
||||||
min_ring_index = Min(min_ring_index, ring_index);
|
|
||||||
|
|
||||||
if (is_prefetch)
|
if (is_prefetch)
|
||||||
MyNeonCounters->getpage_prefetch_requests_total++;
|
MyNeonCounters->getpage_prefetch_requests_total++;
|
||||||
else
|
else
|
||||||
@@ -1315,11 +1313,12 @@ Retry:
|
|||||||
MyPState->ring_unused - MyPState->ring_receive;
|
MyPState->ring_unused - MyPState->ring_receive;
|
||||||
|
|
||||||
Assert(any_hits);
|
Assert(any_hits);
|
||||||
|
Assert(last_ring_index != UINT64_MAX);
|
||||||
|
|
||||||
Assert(GetPrfSlot(min_ring_index)->status == PRFS_REQUESTED ||
|
Assert(GetPrfSlot(last_ring_index)->status == PRFS_REQUESTED ||
|
||||||
GetPrfSlot(min_ring_index)->status == PRFS_RECEIVED);
|
GetPrfSlot(last_ring_index)->status == PRFS_RECEIVED);
|
||||||
Assert(MyPState->ring_last <= min_ring_index &&
|
Assert(MyPState->ring_last <= last_ring_index &&
|
||||||
min_ring_index < MyPState->ring_unused);
|
last_ring_index < MyPState->ring_unused);
|
||||||
|
|
||||||
if (flush_every_n_requests > 0 &&
|
if (flush_every_n_requests > 0 &&
|
||||||
MyPState->ring_unused - MyPState->ring_flush >= flush_every_n_requests)
|
MyPState->ring_unused - MyPState->ring_flush >= flush_every_n_requests)
|
||||||
@@ -1335,7 +1334,7 @@ Retry:
|
|||||||
MyPState->ring_flush = MyPState->ring_unused;
|
MyPState->ring_flush = MyPState->ring_unused;
|
||||||
}
|
}
|
||||||
|
|
||||||
return min_ring_index;
|
return last_ring_index;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#if PG_MAJORVERSION_NUM >= 15
|
#if PG_MAJORVERSION_NUM >= 15
|
||||||
#include "access/xlogrecovery.h"
|
#include "access/xlogrecovery.h"
|
||||||
#endif
|
#endif
|
||||||
|
#include "executor/instrument.h"
|
||||||
#include "replication/logical.h"
|
#include "replication/logical.h"
|
||||||
#include "replication/logicallauncher.h"
|
#include "replication/logicallauncher.h"
|
||||||
#include "replication/slot.h"
|
#include "replication/slot.h"
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
#include "file_cache.h"
|
#include "file_cache.h"
|
||||||
#include "neon.h"
|
#include "neon.h"
|
||||||
#include "neon_lwlsncache.h"
|
#include "neon_lwlsncache.h"
|
||||||
|
#include "neon_perf_counters.h"
|
||||||
#include "control_plane_connector.h"
|
#include "control_plane_connector.h"
|
||||||
#include "logical_replication_monitor.h"
|
#include "logical_replication_monitor.h"
|
||||||
#include "unstable_extensions.h"
|
#include "unstable_extensions.h"
|
||||||
@@ -46,6 +48,13 @@ void _PG_init(void);
|
|||||||
|
|
||||||
|
|
||||||
static int running_xacts_overflow_policy;
|
static int running_xacts_overflow_policy;
|
||||||
|
static bool monitor_query_exec_time = false;
|
||||||
|
|
||||||
|
static ExecutorStart_hook_type prev_ExecutorStart = NULL;
|
||||||
|
static ExecutorEnd_hook_type prev_ExecutorEnd = NULL;
|
||||||
|
|
||||||
|
static void neon_ExecutorStart(QueryDesc *queryDesc, int eflags);
|
||||||
|
static void neon_ExecutorEnd(QueryDesc *queryDesc);
|
||||||
|
|
||||||
#if PG_MAJORVERSION_NUM >= 16
|
#if PG_MAJORVERSION_NUM >= 16
|
||||||
static shmem_startup_hook_type prev_shmem_startup_hook;
|
static shmem_startup_hook_type prev_shmem_startup_hook;
|
||||||
@@ -470,6 +479,16 @@ _PG_init(void)
|
|||||||
0,
|
0,
|
||||||
NULL, NULL, NULL);
|
NULL, NULL, NULL);
|
||||||
|
|
||||||
|
DefineCustomBoolVariable(
|
||||||
|
"neon.monitor_query_exec_time",
|
||||||
|
"Collect infortmation about query execution time",
|
||||||
|
NULL,
|
||||||
|
&monitor_query_exec_time,
|
||||||
|
false,
|
||||||
|
PGC_USERSET,
|
||||||
|
0,
|
||||||
|
NULL, NULL, NULL);
|
||||||
|
|
||||||
DefineCustomBoolVariable(
|
DefineCustomBoolVariable(
|
||||||
"neon.allow_replica_misconfig",
|
"neon.allow_replica_misconfig",
|
||||||
"Allow replica startup when some critical GUCs have smaller value than on primary node",
|
"Allow replica startup when some critical GUCs have smaller value than on primary node",
|
||||||
@@ -508,6 +527,11 @@ _PG_init(void)
|
|||||||
EmitWarningsOnPlaceholders("neon");
|
EmitWarningsOnPlaceholders("neon");
|
||||||
|
|
||||||
ReportSearchPath();
|
ReportSearchPath();
|
||||||
|
|
||||||
|
prev_ExecutorStart = ExecutorStart_hook;
|
||||||
|
ExecutorStart_hook = neon_ExecutorStart;
|
||||||
|
prev_ExecutorEnd = ExecutorEnd_hook;
|
||||||
|
ExecutorEnd_hook = neon_ExecutorEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
PG_FUNCTION_INFO_V1(pg_cluster_size);
|
PG_FUNCTION_INFO_V1(pg_cluster_size);
|
||||||
@@ -581,3 +605,55 @@ neon_shmem_startup_hook(void)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ExecutorStart hook: start up tracking if needed
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
neon_ExecutorStart(QueryDesc *queryDesc, int eflags)
|
||||||
|
{
|
||||||
|
if (prev_ExecutorStart)
|
||||||
|
prev_ExecutorStart(queryDesc, eflags);
|
||||||
|
else
|
||||||
|
standard_ExecutorStart(queryDesc, eflags);
|
||||||
|
|
||||||
|
if (monitor_query_exec_time)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Set up to track total elapsed time in ExecutorRun. Make sure the
|
||||||
|
* space is allocated in the per-query context so it will go away at
|
||||||
|
* ExecutorEnd.
|
||||||
|
*/
|
||||||
|
if (queryDesc->totaltime == NULL)
|
||||||
|
{
|
||||||
|
MemoryContext oldcxt;
|
||||||
|
|
||||||
|
oldcxt = MemoryContextSwitchTo(queryDesc->estate->es_query_cxt);
|
||||||
|
queryDesc->totaltime = InstrAlloc(1, INSTRUMENT_TIMER, false);
|
||||||
|
MemoryContextSwitchTo(oldcxt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ExecutorEnd hook: store results if needed
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
neon_ExecutorEnd(QueryDesc *queryDesc)
|
||||||
|
{
|
||||||
|
if (monitor_query_exec_time && queryDesc->totaltime)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Make sure stats accumulation is done. (Note: it's okay if several
|
||||||
|
* levels of hook all do this.)
|
||||||
|
*/
|
||||||
|
InstrEndLoop(queryDesc->totaltime);
|
||||||
|
|
||||||
|
inc_query_time(queryDesc->totaltime->total*1000000); /* convert to usec */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev_ExecutorEnd)
|
||||||
|
prev_ExecutorEnd(queryDesc);
|
||||||
|
else
|
||||||
|
standard_ExecutorEnd(queryDesc);
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,6 +71,27 @@ inc_iohist(IOHistogram hist, uint64 latency_us)
|
|||||||
hist->wait_us_count++;
|
hist->wait_us_count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
inc_qthist(QTHistogram hist, uint64 elapsed_us)
|
||||||
|
{
|
||||||
|
int lo = 0;
|
||||||
|
int hi = NUM_QT_BUCKETS - 1;
|
||||||
|
|
||||||
|
/* Find the right bucket with binary search */
|
||||||
|
while (lo < hi)
|
||||||
|
{
|
||||||
|
int mid = (lo + hi) / 2;
|
||||||
|
|
||||||
|
if (elapsed_us < qt_bucket_thresholds[mid])
|
||||||
|
hi = mid;
|
||||||
|
else
|
||||||
|
lo = mid + 1;
|
||||||
|
}
|
||||||
|
hist->elapsed_us_bucket[lo]++;
|
||||||
|
hist->elapsed_us_sum += elapsed_us;
|
||||||
|
hist->elapsed_us_count++;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Count a GetPage wait operation.
|
* Count a GetPage wait operation.
|
||||||
*/
|
*/
|
||||||
@@ -98,6 +119,13 @@ inc_page_cache_write_wait(uint64 latency)
|
|||||||
inc_iohist(&MyNeonCounters->file_cache_write_hist, latency);
|
inc_iohist(&MyNeonCounters->file_cache_write_hist, latency);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
inc_query_time(uint64 elapsed)
|
||||||
|
{
|
||||||
|
inc_qthist(&MyNeonCounters->query_time_hist, elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Support functions for the views, neon_backend_perf_counters and
|
* Support functions for the views, neon_backend_perf_counters and
|
||||||
* neon_perf_counters.
|
* neon_perf_counters.
|
||||||
@@ -112,11 +140,11 @@ typedef struct
|
|||||||
} metric_t;
|
} metric_t;
|
||||||
|
|
||||||
static int
|
static int
|
||||||
histogram_to_metrics(IOHistogram histogram,
|
io_histogram_to_metrics(IOHistogram histogram,
|
||||||
metric_t *metrics,
|
metric_t *metrics,
|
||||||
const char *count,
|
const char *count,
|
||||||
const char *sum,
|
const char *sum,
|
||||||
const char *bucket)
|
const char *bucket)
|
||||||
{
|
{
|
||||||
int i = 0;
|
int i = 0;
|
||||||
uint64 bucket_accum = 0;
|
uint64 bucket_accum = 0;
|
||||||
@@ -145,10 +173,44 @@ histogram_to_metrics(IOHistogram histogram,
|
|||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
qt_histogram_to_metrics(QTHistogram histogram,
|
||||||
|
metric_t *metrics,
|
||||||
|
const char *count,
|
||||||
|
const char *sum,
|
||||||
|
const char *bucket)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
uint64 bucket_accum = 0;
|
||||||
|
|
||||||
|
metrics[i].name = count;
|
||||||
|
metrics[i].is_bucket = false;
|
||||||
|
metrics[i].value = (double) histogram->elapsed_us_count;
|
||||||
|
i++;
|
||||||
|
metrics[i].name = sum;
|
||||||
|
metrics[i].is_bucket = false;
|
||||||
|
metrics[i].value = (double) histogram->elapsed_us_sum / 1000000.0;
|
||||||
|
i++;
|
||||||
|
for (int bucketno = 0; bucketno < NUM_QT_BUCKETS; bucketno++)
|
||||||
|
{
|
||||||
|
uint64 threshold = qt_bucket_thresholds[bucketno];
|
||||||
|
|
||||||
|
bucket_accum += histogram->elapsed_us_bucket[bucketno];
|
||||||
|
|
||||||
|
metrics[i].name = bucket;
|
||||||
|
metrics[i].is_bucket = true;
|
||||||
|
metrics[i].bucket_le = (threshold == UINT64_MAX) ? INFINITY : ((double) threshold) / 1000000.0;
|
||||||
|
metrics[i].value = (double) bucket_accum;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
static metric_t *
|
static metric_t *
|
||||||
neon_perf_counters_to_metrics(neon_per_backend_counters *counters)
|
neon_perf_counters_to_metrics(neon_per_backend_counters *counters)
|
||||||
{
|
{
|
||||||
#define NUM_METRICS ((2 + NUM_IO_WAIT_BUCKETS) * 3 + 12)
|
#define NUM_METRICS ((2 + NUM_IO_WAIT_BUCKETS) * 3 + (2 + NUM_QT_BUCKETS) + 12)
|
||||||
metric_t *metrics = palloc((NUM_METRICS + 1) * sizeof(metric_t));
|
metric_t *metrics = palloc((NUM_METRICS + 1) * sizeof(metric_t));
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
@@ -159,10 +221,10 @@ neon_perf_counters_to_metrics(neon_per_backend_counters *counters)
|
|||||||
i++; \
|
i++; \
|
||||||
} while (false)
|
} while (false)
|
||||||
|
|
||||||
i += histogram_to_metrics(&counters->getpage_hist, &metrics[i],
|
i += io_histogram_to_metrics(&counters->getpage_hist, &metrics[i],
|
||||||
"getpage_wait_seconds_count",
|
"getpage_wait_seconds_count",
|
||||||
"getpage_wait_seconds_sum",
|
"getpage_wait_seconds_sum",
|
||||||
"getpage_wait_seconds_bucket");
|
"getpage_wait_seconds_bucket");
|
||||||
|
|
||||||
APPEND_METRIC(getpage_prefetch_requests_total);
|
APPEND_METRIC(getpage_prefetch_requests_total);
|
||||||
APPEND_METRIC(getpage_sync_requests_total);
|
APPEND_METRIC(getpage_sync_requests_total);
|
||||||
@@ -178,14 +240,19 @@ neon_perf_counters_to_metrics(neon_per_backend_counters *counters)
|
|||||||
|
|
||||||
APPEND_METRIC(file_cache_hits_total);
|
APPEND_METRIC(file_cache_hits_total);
|
||||||
|
|
||||||
i += histogram_to_metrics(&counters->file_cache_read_hist, &metrics[i],
|
i += io_histogram_to_metrics(&counters->file_cache_read_hist, &metrics[i],
|
||||||
"file_cache_read_wait_seconds_count",
|
"file_cache_read_wait_seconds_count",
|
||||||
"file_cache_read_wait_seconds_sum",
|
"file_cache_read_wait_seconds_sum",
|
||||||
"file_cache_read_wait_seconds_bucket");
|
"file_cache_read_wait_seconds_bucket");
|
||||||
i += histogram_to_metrics(&counters->file_cache_write_hist, &metrics[i],
|
i += io_histogram_to_metrics(&counters->file_cache_write_hist, &metrics[i],
|
||||||
"file_cache_write_wait_seconds_count",
|
"file_cache_write_wait_seconds_count",
|
||||||
"file_cache_write_wait_seconds_sum",
|
"file_cache_write_wait_seconds_sum",
|
||||||
"file_cache_write_wait_seconds_bucket");
|
"file_cache_write_wait_seconds_bucket");
|
||||||
|
|
||||||
|
i += qt_histogram_to_metrics(&counters->query_time_hist, &metrics[i],
|
||||||
|
"query_time_seconds_count",
|
||||||
|
"query_time_seconds_sum",
|
||||||
|
"query_time_seconds_bucket");
|
||||||
|
|
||||||
Assert(i == NUM_METRICS);
|
Assert(i == NUM_METRICS);
|
||||||
|
|
||||||
@@ -257,7 +324,7 @@ neon_get_backend_perf_counters(PG_FUNCTION_ARGS)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
histogram_merge_into(IOHistogram into, IOHistogram from)
|
io_histogram_merge_into(IOHistogram into, IOHistogram from)
|
||||||
{
|
{
|
||||||
into->wait_us_count += from->wait_us_count;
|
into->wait_us_count += from->wait_us_count;
|
||||||
into->wait_us_sum += from->wait_us_sum;
|
into->wait_us_sum += from->wait_us_sum;
|
||||||
@@ -265,6 +332,15 @@ histogram_merge_into(IOHistogram into, IOHistogram from)
|
|||||||
into->wait_us_bucket[bucketno] += from->wait_us_bucket[bucketno];
|
into->wait_us_bucket[bucketno] += from->wait_us_bucket[bucketno];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
qt_histogram_merge_into(QTHistogram into, QTHistogram from)
|
||||||
|
{
|
||||||
|
into->elapsed_us_count += from->elapsed_us_count;
|
||||||
|
into->elapsed_us_sum += from->elapsed_us_sum;
|
||||||
|
for (int bucketno = 0; bucketno < NUM_QT_BUCKETS; bucketno++)
|
||||||
|
into->elapsed_us_bucket[bucketno] += from->elapsed_us_bucket[bucketno];
|
||||||
|
}
|
||||||
|
|
||||||
PG_FUNCTION_INFO_V1(neon_get_perf_counters);
|
PG_FUNCTION_INFO_V1(neon_get_perf_counters);
|
||||||
Datum
|
Datum
|
||||||
neon_get_perf_counters(PG_FUNCTION_ARGS)
|
neon_get_perf_counters(PG_FUNCTION_ARGS)
|
||||||
@@ -283,7 +359,7 @@ neon_get_perf_counters(PG_FUNCTION_ARGS)
|
|||||||
{
|
{
|
||||||
neon_per_backend_counters *counters = &neon_per_backend_counters_shared[procno];
|
neon_per_backend_counters *counters = &neon_per_backend_counters_shared[procno];
|
||||||
|
|
||||||
histogram_merge_into(&totals.getpage_hist, &counters->getpage_hist);
|
io_histogram_merge_into(&totals.getpage_hist, &counters->getpage_hist);
|
||||||
totals.getpage_prefetch_requests_total += counters->getpage_prefetch_requests_total;
|
totals.getpage_prefetch_requests_total += counters->getpage_prefetch_requests_total;
|
||||||
totals.getpage_sync_requests_total += counters->getpage_sync_requests_total;
|
totals.getpage_sync_requests_total += counters->getpage_sync_requests_total;
|
||||||
totals.getpage_prefetch_misses_total += counters->getpage_prefetch_misses_total;
|
totals.getpage_prefetch_misses_total += counters->getpage_prefetch_misses_total;
|
||||||
@@ -294,13 +370,13 @@ neon_get_perf_counters(PG_FUNCTION_ARGS)
|
|||||||
totals.pageserver_open_requests += counters->pageserver_open_requests;
|
totals.pageserver_open_requests += counters->pageserver_open_requests;
|
||||||
totals.getpage_prefetches_buffered += counters->getpage_prefetches_buffered;
|
totals.getpage_prefetches_buffered += counters->getpage_prefetches_buffered;
|
||||||
totals.file_cache_hits_total += counters->file_cache_hits_total;
|
totals.file_cache_hits_total += counters->file_cache_hits_total;
|
||||||
histogram_merge_into(&totals.file_cache_read_hist, &counters->file_cache_read_hist);
|
|
||||||
histogram_merge_into(&totals.file_cache_write_hist, &counters->file_cache_write_hist);
|
|
||||||
|
|
||||||
totals.compute_getpage_stuck_requests_total += counters->compute_getpage_stuck_requests_total;
|
totals.compute_getpage_stuck_requests_total += counters->compute_getpage_stuck_requests_total;
|
||||||
totals.compute_getpage_max_inflight_stuck_time_ms = Max(
|
totals.compute_getpage_max_inflight_stuck_time_ms = Max(
|
||||||
totals.compute_getpage_max_inflight_stuck_time_ms,
|
totals.compute_getpage_max_inflight_stuck_time_ms,
|
||||||
counters->compute_getpage_max_inflight_stuck_time_ms);
|
counters->compute_getpage_max_inflight_stuck_time_ms);
|
||||||
|
io_histogram_merge_into(&totals.file_cache_read_hist, &counters->file_cache_read_hist);
|
||||||
|
io_histogram_merge_into(&totals.file_cache_write_hist, &counters->file_cache_write_hist);
|
||||||
|
qt_histogram_merge_into(&totals.query_time_hist, &counters->query_time_hist);
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics = neon_perf_counters_to_metrics(&totals);
|
metrics = neon_perf_counters_to_metrics(&totals);
|
||||||
|
|||||||
@@ -36,6 +36,28 @@ typedef struct IOHistogramData
|
|||||||
|
|
||||||
typedef IOHistogramData *IOHistogram;
|
typedef IOHistogramData *IOHistogram;
|
||||||
|
|
||||||
|
static const uint64 qt_bucket_thresholds[] = {
|
||||||
|
2, 3, 6, 10, /* 0 us - 10 us */
|
||||||
|
20, 30, 60, 100, /* 10 us - 100 us */
|
||||||
|
200, 300, 600, 1000, /* 100 us - 1 ms */
|
||||||
|
2000, 3000, 6000, 10000, /* 1 ms - 10 ms */
|
||||||
|
20000, 30000, 60000, 100000, /* 10 ms - 100 ms */
|
||||||
|
200000, 300000, 600000, 1000000, /* 100 ms - 1 s */
|
||||||
|
2000000, 3000000, 6000000, 10000000, /* 1 s - 10 s */
|
||||||
|
20000000, 30000000, 60000000, 100000000, /* 10 s - 100 s */
|
||||||
|
UINT64_MAX,
|
||||||
|
};
|
||||||
|
#define NUM_QT_BUCKETS (lengthof(qt_bucket_thresholds))
|
||||||
|
|
||||||
|
typedef struct QTHistogramData
|
||||||
|
{
|
||||||
|
uint64 elapsed_us_count;
|
||||||
|
uint64 elapsed_us_sum;
|
||||||
|
uint64 elapsed_us_bucket[NUM_QT_BUCKETS];
|
||||||
|
} QTHistogramData;
|
||||||
|
|
||||||
|
typedef QTHistogramData *QTHistogram;
|
||||||
|
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
@@ -127,6 +149,11 @@ typedef struct
|
|||||||
/* LFC I/O time buckets */
|
/* LFC I/O time buckets */
|
||||||
IOHistogramData file_cache_read_hist;
|
IOHistogramData file_cache_read_hist;
|
||||||
IOHistogramData file_cache_write_hist;
|
IOHistogramData file_cache_write_hist;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Histogram of query execution time.
|
||||||
|
*/
|
||||||
|
QTHistogramData query_time_hist;
|
||||||
} neon_per_backend_counters;
|
} neon_per_backend_counters;
|
||||||
|
|
||||||
/* Pointer to the shared memory array of neon_per_backend_counters structs */
|
/* Pointer to the shared memory array of neon_per_backend_counters structs */
|
||||||
@@ -149,6 +176,7 @@ extern neon_per_backend_counters *neon_per_backend_counters_shared;
|
|||||||
extern void inc_getpage_wait(uint64 latency);
|
extern void inc_getpage_wait(uint64 latency);
|
||||||
extern void inc_page_cache_read_wait(uint64 latency);
|
extern void inc_page_cache_read_wait(uint64 latency);
|
||||||
extern void inc_page_cache_write_wait(uint64 latency);
|
extern void inc_page_cache_write_wait(uint64 latency);
|
||||||
|
extern void inc_query_time(uint64 elapsed);
|
||||||
|
|
||||||
extern Size NeonPerfCountersShmemSize(void);
|
extern Size NeonPerfCountersShmemSize(void);
|
||||||
extern void NeonPerfCountersShmemInit(void);
|
extern void NeonPerfCountersShmemInit(void);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include "funcapi.h"
|
#include "funcapi.h"
|
||||||
#include "miscadmin.h"
|
#include "miscadmin.h"
|
||||||
|
#include "access/xlog.h"
|
||||||
#include "utils/tuplestore.h"
|
#include "utils/tuplestore.h"
|
||||||
|
|
||||||
#include "neon_pgversioncompat.h"
|
#include "neon_pgversioncompat.h"
|
||||||
@@ -41,5 +42,12 @@ InitMaterializedSRF(FunctionCallInfo fcinfo, bits32 flags)
|
|||||||
rsinfo->setDesc = stored_tupdesc;
|
rsinfo->setDesc = stored_tupdesc;
|
||||||
MemoryContextSwitchTo(old_context);
|
MemoryContextSwitchTo(old_context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TimeLineID GetWALInsertionTimeLine(void)
|
||||||
|
{
|
||||||
|
return ThisTimeLineID + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ InitBufferTag(BufferTag *tag, const RelFileNode *rnode,
|
|||||||
|
|
||||||
#if PG_MAJORVERSION_NUM < 15
|
#if PG_MAJORVERSION_NUM < 15
|
||||||
extern void InitMaterializedSRF(FunctionCallInfo fcinfo, bits32 flags);
|
extern void InitMaterializedSRF(FunctionCallInfo fcinfo, bits32 flags);
|
||||||
|
extern TimeLineID GetWALInsertionTimeLine(void);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif /* NEON_PGVERSIONCOMPAT_H */
|
#endif /* NEON_PGVERSIONCOMPAT_H */
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ struct NeonWALReader
|
|||||||
WALSegmentContext segcxt;
|
WALSegmentContext segcxt;
|
||||||
WALOpenSegment seg;
|
WALOpenSegment seg;
|
||||||
int wre_errno;
|
int wre_errno;
|
||||||
|
TimeLineID local_active_tlid;
|
||||||
/* Explains failure to read, static for simplicity. */
|
/* Explains failure to read, static for simplicity. */
|
||||||
char err_msg[NEON_WALREADER_ERR_MSG_LEN];
|
char err_msg[NEON_WALREADER_ERR_MSG_LEN];
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ struct NeonWALReader
|
|||||||
|
|
||||||
/* palloc and initialize NeonWALReader */
|
/* palloc and initialize NeonWALReader */
|
||||||
NeonWALReader *
|
NeonWALReader *
|
||||||
NeonWALReaderAllocate(int wal_segment_size, XLogRecPtr available_lsn, char *log_prefix)
|
NeonWALReaderAllocate(int wal_segment_size, XLogRecPtr available_lsn, char *log_prefix, TimeLineID tlid)
|
||||||
{
|
{
|
||||||
NeonWALReader *reader;
|
NeonWALReader *reader;
|
||||||
|
|
||||||
@@ -118,6 +119,7 @@ NeonWALReaderAllocate(int wal_segment_size, XLogRecPtr available_lsn, char *log_
|
|||||||
MemoryContextAllocZero(TopMemoryContext, sizeof(NeonWALReader));
|
MemoryContextAllocZero(TopMemoryContext, sizeof(NeonWALReader));
|
||||||
|
|
||||||
reader->available_lsn = available_lsn;
|
reader->available_lsn = available_lsn;
|
||||||
|
reader->local_active_tlid = tlid;
|
||||||
reader->seg.ws_file = -1;
|
reader->seg.ws_file = -1;
|
||||||
reader->seg.ws_segno = 0;
|
reader->seg.ws_segno = 0;
|
||||||
reader->seg.ws_tli = 0;
|
reader->seg.ws_tli = 0;
|
||||||
@@ -577,6 +579,17 @@ NeonWALReaderIsRemConnEstablished(NeonWALReader *state)
|
|||||||
return state->rem_state == RS_ESTABLISHED;
|
return state->rem_state == RS_ESTABLISHED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Whether remote connection is established. Once this is done, until successful
|
||||||
|
* local read or error socket is stable and user can update socket events
|
||||||
|
* instead of readding it each time.
|
||||||
|
*/
|
||||||
|
TimeLineID
|
||||||
|
NeonWALReaderLocalActiveTimeLineID(NeonWALReader *state)
|
||||||
|
{
|
||||||
|
return state->local_active_tlid;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Returns events user should wait on connection socket or 0 if remote
|
* Returns events user should wait on connection socket or 0 if remote
|
||||||
* connection is not active.
|
* connection is not active.
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ typedef enum
|
|||||||
NEON_WALREAD_ERROR,
|
NEON_WALREAD_ERROR,
|
||||||
} NeonWALReadResult;
|
} NeonWALReadResult;
|
||||||
|
|
||||||
extern NeonWALReader *NeonWALReaderAllocate(int wal_segment_size, XLogRecPtr available_lsn, char *log_prefix);
|
extern NeonWALReader *NeonWALReaderAllocate(int wal_segment_size, XLogRecPtr available_lsn, char *log_prefix, TimeLineID tlid);
|
||||||
extern void NeonWALReaderFree(NeonWALReader *state);
|
extern void NeonWALReaderFree(NeonWALReader *state);
|
||||||
extern void NeonWALReaderResetRemote(NeonWALReader *state);
|
extern void NeonWALReaderResetRemote(NeonWALReader *state);
|
||||||
|
extern TimeLineID NeonWALReaderLocalActiveTimeLineID(NeonWALReader *state);
|
||||||
extern NeonWALReadResult NeonWALRead(NeonWALReader *state, char *buf, XLogRecPtr startptr, Size count, TimeLineID tli);
|
extern NeonWALReadResult NeonWALRead(NeonWALReader *state, char *buf, XLogRecPtr startptr, Size count, TimeLineID tli);
|
||||||
extern pgsocket NeonWALReaderSocket(NeonWALReader *state);
|
extern pgsocket NeonWALReaderSocket(NeonWALReader *state);
|
||||||
extern uint32 NeonWALReaderEvents(NeonWALReader *state);
|
extern uint32 NeonWALReaderEvents(NeonWALReader *state);
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ WalProposerCreate(WalProposerConfig *config, walproposer_api api)
|
|||||||
wp = palloc0(sizeof(WalProposer));
|
wp = palloc0(sizeof(WalProposer));
|
||||||
wp->config = config;
|
wp->config = config;
|
||||||
wp->api = api;
|
wp->api = api;
|
||||||
|
wp->localTimeLineID = config->pgTimeline;
|
||||||
wp->state = WPS_COLLECTING_TERMS;
|
wp->state = WPS_COLLECTING_TERMS;
|
||||||
wp->mconf.generation = INVALID_GENERATION;
|
wp->mconf.generation = INVALID_GENERATION;
|
||||||
wp->mconf.members.len = 0;
|
wp->mconf.members.len = 0;
|
||||||
@@ -119,6 +120,10 @@ WalProposerCreate(WalProposerConfig *config, walproposer_api api)
|
|||||||
{
|
{
|
||||||
wp_log(FATAL, "failed to parse neon.safekeepers generation number: %m");
|
wp_log(FATAL, "failed to parse neon.safekeepers generation number: %m");
|
||||||
}
|
}
|
||||||
|
if (*endptr != ':')
|
||||||
|
{
|
||||||
|
wp_log(FATAL, "failed to parse neon.safekeepers: no colon after generation");
|
||||||
|
}
|
||||||
/* Skip past : to the first hostname. */
|
/* Skip past : to the first hostname. */
|
||||||
host = endptr + 1;
|
host = endptr + 1;
|
||||||
}
|
}
|
||||||
@@ -1130,7 +1135,7 @@ VotesCollectedMset(WalProposer *wp, MemberSet *mset, Safekeeper **msk, StringInf
|
|||||||
wp->propTermStartLsn = sk->voteResponse.flushLsn;
|
wp->propTermStartLsn = sk->voteResponse.flushLsn;
|
||||||
wp->donor = sk;
|
wp->donor = sk;
|
||||||
}
|
}
|
||||||
wp->truncateLsn = Max(wp->safekeeper[i].voteResponse.truncateLsn, wp->truncateLsn);
|
wp->truncateLsn = Max(sk->voteResponse.truncateLsn, wp->truncateLsn);
|
||||||
|
|
||||||
if (n_votes > 0)
|
if (n_votes > 0)
|
||||||
appendStringInfoString(s, ", ");
|
appendStringInfoString(s, ", ");
|
||||||
@@ -1380,7 +1385,7 @@ ProcessPropStartPos(WalProposer *wp)
|
|||||||
* we must bail out, as clog and other non rel data is inconsistent.
|
* we must bail out, as clog and other non rel data is inconsistent.
|
||||||
*/
|
*/
|
||||||
walprop_shared = wp->api.get_shmem_state(wp);
|
walprop_shared = wp->api.get_shmem_state(wp);
|
||||||
if (!wp->config->syncSafekeepers)
|
if (!wp->config->syncSafekeepers && !walprop_shared->replica_promote)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
* Basebackup LSN always points to the beginning of the record (not
|
* Basebackup LSN always points to the beginning of the record (not
|
||||||
|
|||||||
@@ -391,6 +391,7 @@ typedef struct WalproposerShmemState
|
|||||||
/* last feedback from each shard */
|
/* last feedback from each shard */
|
||||||
PageserverFeedback shard_ps_feedback[MAX_SHARDS];
|
PageserverFeedback shard_ps_feedback[MAX_SHARDS];
|
||||||
int num_shards;
|
int num_shards;
|
||||||
|
bool replica_promote;
|
||||||
|
|
||||||
/* aggregated feedback with min LSNs across shards */
|
/* aggregated feedback with min LSNs across shards */
|
||||||
PageserverFeedback min_ps_feedback;
|
PageserverFeedback min_ps_feedback;
|
||||||
@@ -806,6 +807,9 @@ typedef struct WalProposer
|
|||||||
/* Safekeepers walproposer is connecting to. */
|
/* Safekeepers walproposer is connecting to. */
|
||||||
Safekeeper safekeeper[MAX_SAFEKEEPERS];
|
Safekeeper safekeeper[MAX_SAFEKEEPERS];
|
||||||
|
|
||||||
|
/* Current local TimeLineId in use */
|
||||||
|
TimeLineID localTimeLineID;
|
||||||
|
|
||||||
/* WAL has been generated up to this point */
|
/* WAL has been generated up to this point */
|
||||||
XLogRecPtr availableLsn;
|
XLogRecPtr availableLsn;
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
#include "storage/proc.h"
|
#include "storage/proc.h"
|
||||||
#include "storage/ipc.h"
|
#include "storage/ipc.h"
|
||||||
#include "storage/lwlock.h"
|
#include "storage/lwlock.h"
|
||||||
|
#include "storage/pg_shmem.h"
|
||||||
#include "storage/shmem.h"
|
#include "storage/shmem.h"
|
||||||
#include "storage/spin.h"
|
#include "storage/spin.h"
|
||||||
#include "tcop/tcopprot.h"
|
#include "tcop/tcopprot.h"
|
||||||
@@ -159,12 +160,19 @@ WalProposerMain(Datum main_arg)
|
|||||||
{
|
{
|
||||||
WalProposer *wp;
|
WalProposer *wp;
|
||||||
|
|
||||||
|
if (*wal_acceptors_list == '\0')
|
||||||
|
{
|
||||||
|
wpg_log(WARNING, "Safekeepers list is empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
init_walprop_config(false);
|
init_walprop_config(false);
|
||||||
walprop_pg_init_bgworker();
|
walprop_pg_init_bgworker();
|
||||||
am_walproposer = true;
|
am_walproposer = true;
|
||||||
walprop_pg_load_libpqwalreceiver();
|
walprop_pg_load_libpqwalreceiver();
|
||||||
|
|
||||||
wp = WalProposerCreate(&walprop_config, walprop_pg);
|
wp = WalProposerCreate(&walprop_config, walprop_pg);
|
||||||
|
wp->localTimeLineID = GetWALInsertionTimeLine();
|
||||||
wp->last_reconnect_attempt = walprop_pg_get_current_timestamp(wp);
|
wp->last_reconnect_attempt = walprop_pg_get_current_timestamp(wp);
|
||||||
|
|
||||||
walprop_pg_init_walsender();
|
walprop_pg_init_walsender();
|
||||||
@@ -272,6 +280,30 @@ split_safekeepers_list(char *safekeepers_list, char *safekeepers[])
|
|||||||
return n_safekeepers;
|
return n_safekeepers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static char *split_off_safekeepers_generation(char *safekeepers_list, uint32 *generation)
|
||||||
|
{
|
||||||
|
char *endptr;
|
||||||
|
|
||||||
|
if (strncmp(safekeepers_list, "g#", 2) != 0)
|
||||||
|
{
|
||||||
|
return safekeepers_list;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errno = 0;
|
||||||
|
*generation = strtoul(safekeepers_list + 2, &endptr, 10);
|
||||||
|
if (errno != 0)
|
||||||
|
{
|
||||||
|
wp_log(FATAL, "failed to parse neon.safekeepers generation number: %m");
|
||||||
|
}
|
||||||
|
if (*endptr != ':')
|
||||||
|
{
|
||||||
|
wp_log(FATAL, "failed to parse neon.safekeepers: no colon after generation");
|
||||||
|
}
|
||||||
|
return endptr + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Accept two coma-separated strings with list of safekeeper host:port addresses.
|
* Accept two coma-separated strings with list of safekeeper host:port addresses.
|
||||||
* Split them into arrays and return false if two sets do not match, ignoring the order.
|
* Split them into arrays and return false if two sets do not match, ignoring the order.
|
||||||
@@ -283,6 +315,16 @@ safekeepers_cmp(char *old, char *new)
|
|||||||
char *safekeepers_new[MAX_SAFEKEEPERS];
|
char *safekeepers_new[MAX_SAFEKEEPERS];
|
||||||
int len_old = 0;
|
int len_old = 0;
|
||||||
int len_new = 0;
|
int len_new = 0;
|
||||||
|
uint32 gen_old = INVALID_GENERATION;
|
||||||
|
uint32 gen_new = INVALID_GENERATION;
|
||||||
|
|
||||||
|
old = split_off_safekeepers_generation(old, &gen_old);
|
||||||
|
new = split_off_safekeepers_generation(new, &gen_new);
|
||||||
|
|
||||||
|
if (gen_old != gen_new)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
len_old = split_safekeepers_list(old, safekeepers_old);
|
len_old = split_safekeepers_list(old, safekeepers_old);
|
||||||
len_new = split_safekeepers_list(new, safekeepers_new);
|
len_new = split_safekeepers_list(new, safekeepers_new);
|
||||||
@@ -316,6 +358,9 @@ assign_neon_safekeepers(const char *newval, void *extra)
|
|||||||
char *newval_copy;
|
char *newval_copy;
|
||||||
char *oldval;
|
char *oldval;
|
||||||
|
|
||||||
|
if (newval && *newval != '\0' && UsedShmemSegAddr && walprop_shared && RecoveryInProgress())
|
||||||
|
walprop_shared->replica_promote = true;
|
||||||
|
|
||||||
if (!am_walproposer)
|
if (!am_walproposer)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -506,16 +551,15 @@ BackpressureThrottlingTime(void)
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Register a background worker proposing WAL to wal acceptors.
|
* Register a background worker proposing WAL to wal acceptors.
|
||||||
|
* We start walproposer bgworker even for replicas in order to support possible replica promotion.
|
||||||
|
* When pg_promote() function is called, then walproposer bgworker registered with BgWorkerStart_RecoveryFinished
|
||||||
|
* is automatically launched when promotion is completed.
|
||||||
*/
|
*/
|
||||||
static void
|
static void
|
||||||
walprop_register_bgworker(void)
|
walprop_register_bgworker(void)
|
||||||
{
|
{
|
||||||
BackgroundWorker bgw;
|
BackgroundWorker bgw;
|
||||||
|
|
||||||
/* If no wal acceptors are specified, don't start the background worker. */
|
|
||||||
if (*wal_acceptors_list == '\0')
|
|
||||||
return;
|
|
||||||
|
|
||||||
memset(&bgw, 0, sizeof(bgw));
|
memset(&bgw, 0, sizeof(bgw));
|
||||||
bgw.bgw_flags = BGWORKER_SHMEM_ACCESS;
|
bgw.bgw_flags = BGWORKER_SHMEM_ACCESS;
|
||||||
bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
|
bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
|
||||||
@@ -1292,9 +1336,7 @@ StartProposerReplication(WalProposer *wp, StartReplicationCmd *cmd)
|
|||||||
|
|
||||||
#if PG_VERSION_NUM < 150000
|
#if PG_VERSION_NUM < 150000
|
||||||
if (ThisTimeLineID == 0)
|
if (ThisTimeLineID == 0)
|
||||||
ereport(ERROR,
|
ThisTimeLineID = 1;
|
||||||
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
|
|
||||||
errmsg("IDENTIFY_SYSTEM has not been run before START_REPLICATION")));
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1508,7 +1550,7 @@ walprop_pg_wal_reader_allocate(Safekeeper *sk)
|
|||||||
|
|
||||||
snprintf(log_prefix, sizeof(log_prefix), WP_LOG_PREFIX "sk %s:%s nwr: ", sk->host, sk->port);
|
snprintf(log_prefix, sizeof(log_prefix), WP_LOG_PREFIX "sk %s:%s nwr: ", sk->host, sk->port);
|
||||||
Assert(!sk->xlogreader);
|
Assert(!sk->xlogreader);
|
||||||
sk->xlogreader = NeonWALReaderAllocate(wal_segment_size, sk->wp->propTermStartLsn, log_prefix);
|
sk->xlogreader = NeonWALReaderAllocate(wal_segment_size, sk->wp->propTermStartLsn, log_prefix, sk->wp->localTimeLineID);
|
||||||
if (sk->xlogreader == NULL)
|
if (sk->xlogreader == NULL)
|
||||||
wpg_log(FATAL, "failed to allocate xlog reader");
|
wpg_log(FATAL, "failed to allocate xlog reader");
|
||||||
}
|
}
|
||||||
@@ -1522,7 +1564,7 @@ walprop_pg_wal_read(Safekeeper *sk, char *buf, XLogRecPtr startptr, Size count,
|
|||||||
buf,
|
buf,
|
||||||
startptr,
|
startptr,
|
||||||
count,
|
count,
|
||||||
walprop_pg_get_timeline_id());
|
sk->wp->localTimeLineID);
|
||||||
|
|
||||||
if (res == NEON_WALREAD_SUCCESS)
|
if (res == NEON_WALREAD_SUCCESS)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ NeonWALPageRead(
|
|||||||
readBuf,
|
readBuf,
|
||||||
targetPagePtr,
|
targetPagePtr,
|
||||||
count,
|
count,
|
||||||
walprop_pg_get_timeline_id());
|
NeonWALReaderLocalActiveTimeLineID(wal_reader));
|
||||||
|
|
||||||
if (res == NEON_WALREAD_SUCCESS)
|
if (res == NEON_WALREAD_SUCCESS)
|
||||||
{
|
{
|
||||||
@@ -202,7 +202,7 @@ NeonOnDemandXLogReaderRoutines(XLogReaderRoutine *xlr)
|
|||||||
{
|
{
|
||||||
elog(ERROR, "unable to start walsender when basebackupLsn is 0");
|
elog(ERROR, "unable to start walsender when basebackupLsn is 0");
|
||||||
}
|
}
|
||||||
wal_reader = NeonWALReaderAllocate(wal_segment_size, basebackupLsn, "[walsender] ");
|
wal_reader = NeonWALReaderAllocate(wal_segment_size, basebackupLsn, "[walsender] ", 1);
|
||||||
}
|
}
|
||||||
xlr->page_read = NeonWALPageRead;
|
xlr->page_read = NeonWALPageRead;
|
||||||
xlr->segment_open = NeonWALReadSegmentOpen;
|
xlr->segment_open = NeonWALReadSegmentOpen;
|
||||||
|
|||||||
@@ -18,26 +18,17 @@ pub(super) async fn authenticate(
|
|||||||
secret: AuthSecret,
|
secret: AuthSecret,
|
||||||
) -> auth::Result<ComputeCredentials> {
|
) -> auth::Result<ComputeCredentials> {
|
||||||
let scram_keys = match secret {
|
let scram_keys = match secret {
|
||||||
#[cfg(any(test, feature = "testing"))]
|
|
||||||
AuthSecret::Md5(_) => {
|
|
||||||
debug!("auth endpoint chooses MD5");
|
|
||||||
return Err(auth::AuthError::MalformedPassword("MD5 not supported"));
|
|
||||||
}
|
|
||||||
AuthSecret::Scram(secret) => {
|
AuthSecret::Scram(secret) => {
|
||||||
debug!("auth endpoint chooses SCRAM");
|
debug!("auth endpoint chooses SCRAM");
|
||||||
let scram = auth::Scram(&secret, ctx);
|
|
||||||
|
|
||||||
let auth_outcome = tokio::time::timeout(config.scram_protocol_timeout, async {
|
let auth_outcome = tokio::time::timeout(
|
||||||
AuthFlow::new(client, scram)
|
config.scram_protocol_timeout,
|
||||||
.authenticate()
|
AuthFlow::new(client, auth::Scram(&secret, ctx)).authenticate(),
|
||||||
.await
|
)
|
||||||
.inspect_err(|error| {
|
|
||||||
warn!(?error, "error processing scram messages");
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
.inspect_err(|_| warn!("error processing scram messages error = authentication timed out, execution time exceeded {} seconds", config.scram_protocol_timeout.as_secs()))
|
.inspect_err(|_| warn!("error processing scram messages error = authentication timed out, execution time exceeded {} seconds", config.scram_protocol_timeout.as_secs()))
|
||||||
.map_err(auth::AuthError::user_timeout)??;
|
.map_err(auth::AuthError::user_timeout)?
|
||||||
|
.inspect_err(|error| warn!(?error, "error processing scram messages"))?;
|
||||||
|
|
||||||
let client_key = match auth_outcome {
|
let client_key = match auth_outcome {
|
||||||
sasl::Outcome::Success(key) => key,
|
sasl::Outcome::Success(key) => key,
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ use thiserror::Error;
|
|||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
use tracing::{info, info_span};
|
use tracing::{info, info_span};
|
||||||
|
|
||||||
use super::ComputeCredentialKeys;
|
|
||||||
use crate::auth::IpPattern;
|
|
||||||
use crate::auth::backend::ComputeUserInfo;
|
use crate::auth::backend::ComputeUserInfo;
|
||||||
use crate::cache::Cached;
|
use crate::cache::Cached;
|
||||||
|
use crate::compute::AuthInfo;
|
||||||
use crate::config::AuthenticationConfig;
|
use crate::config::AuthenticationConfig;
|
||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::control_plane::client::cplane_proxy_v1;
|
use crate::control_plane::client::cplane_proxy_v1;
|
||||||
@@ -17,7 +16,7 @@ use crate::control_plane::{self, CachedNodeInfo, NodeInfo};
|
|||||||
use crate::error::{ReportableError, UserFacingError};
|
use crate::error::{ReportableError, UserFacingError};
|
||||||
use crate::pqproto::BeMessage;
|
use crate::pqproto::BeMessage;
|
||||||
use crate::proxy::NeonOptions;
|
use crate::proxy::NeonOptions;
|
||||||
use crate::proxy::connect_compute::ComputeConnectBackend;
|
use crate::proxy::wake_compute::WakeComputeBackend;
|
||||||
use crate::stream::PqStream;
|
use crate::stream::PqStream;
|
||||||
use crate::types::RoleName;
|
use crate::types::RoleName;
|
||||||
use crate::{auth, compute, waiters};
|
use crate::{auth, compute, waiters};
|
||||||
@@ -98,15 +97,11 @@ impl ConsoleRedirectBackend {
|
|||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
auth_config: &'static AuthenticationConfig,
|
auth_config: &'static AuthenticationConfig,
|
||||||
client: &mut PqStream<impl AsyncRead + AsyncWrite + Unpin>,
|
client: &mut PqStream<impl AsyncRead + AsyncWrite + Unpin>,
|
||||||
) -> auth::Result<(
|
) -> auth::Result<(ConsoleRedirectNodeInfo, AuthInfo, ComputeUserInfo)> {
|
||||||
ConsoleRedirectNodeInfo,
|
|
||||||
ComputeUserInfo,
|
|
||||||
Option<Vec<IpPattern>>,
|
|
||||||
)> {
|
|
||||||
authenticate(ctx, auth_config, &self.console_uri, client)
|
authenticate(ctx, auth_config, &self.console_uri, client)
|
||||||
.await
|
.await
|
||||||
.map(|(node_info, user_info, ip_allowlist)| {
|
.map(|(node_info, auth_info, user_info)| {
|
||||||
(ConsoleRedirectNodeInfo(node_info), user_info, ip_allowlist)
|
(ConsoleRedirectNodeInfo(node_info), auth_info, user_info)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,17 +109,13 @@ impl ConsoleRedirectBackend {
|
|||||||
pub struct ConsoleRedirectNodeInfo(pub(super) NodeInfo);
|
pub struct ConsoleRedirectNodeInfo(pub(super) NodeInfo);
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ComputeConnectBackend for ConsoleRedirectNodeInfo {
|
impl WakeComputeBackend for ConsoleRedirectNodeInfo {
|
||||||
async fn wake_compute(
|
async fn wake_compute(
|
||||||
&self,
|
&self,
|
||||||
_ctx: &RequestContext,
|
_ctx: &RequestContext,
|
||||||
) -> Result<CachedNodeInfo, control_plane::errors::WakeComputeError> {
|
) -> Result<CachedNodeInfo, control_plane::errors::WakeComputeError> {
|
||||||
Ok(Cached::new_uncached(self.0.clone()))
|
Ok(Cached::new_uncached(self.0.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_keys(&self) -> &ComputeCredentialKeys {
|
|
||||||
&ComputeCredentialKeys::None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn authenticate(
|
async fn authenticate(
|
||||||
@@ -132,7 +123,7 @@ async fn authenticate(
|
|||||||
auth_config: &'static AuthenticationConfig,
|
auth_config: &'static AuthenticationConfig,
|
||||||
link_uri: &reqwest::Url,
|
link_uri: &reqwest::Url,
|
||||||
client: &mut PqStream<impl AsyncRead + AsyncWrite + Unpin>,
|
client: &mut PqStream<impl AsyncRead + AsyncWrite + Unpin>,
|
||||||
) -> auth::Result<(NodeInfo, ComputeUserInfo, Option<Vec<IpPattern>>)> {
|
) -> auth::Result<(NodeInfo, AuthInfo, ComputeUserInfo)> {
|
||||||
ctx.set_auth_method(crate::context::AuthMethod::ConsoleRedirect);
|
ctx.set_auth_method(crate::context::AuthMethod::ConsoleRedirect);
|
||||||
|
|
||||||
// registering waiter can fail if we get unlucky with rng.
|
// registering waiter can fail if we get unlucky with rng.
|
||||||
@@ -192,10 +183,24 @@ async fn authenticate(
|
|||||||
|
|
||||||
client.write_message(BeMessage::NoticeResponse("Connecting to database."));
|
client.write_message(BeMessage::NoticeResponse("Connecting to database."));
|
||||||
|
|
||||||
// This config should be self-contained, because we won't
|
// Backwards compatibility. pg_sni_proxy uses "--" in domain names
|
||||||
// take username or dbname from client's startup message.
|
// while direct connections do not. Once we migrate to pg_sni_proxy
|
||||||
let mut config = compute::ConnCfg::new(db_info.host.to_string(), db_info.port);
|
// everywhere, we can remove this.
|
||||||
config.dbname(&db_info.dbname).user(&db_info.user);
|
let ssl_mode = if db_info.host.contains("--") {
|
||||||
|
// we need TLS connection with SNI info to properly route it
|
||||||
|
SslMode::Require
|
||||||
|
} else {
|
||||||
|
SslMode::Disable
|
||||||
|
};
|
||||||
|
|
||||||
|
let conn_info = compute::ConnectInfo {
|
||||||
|
host: db_info.host.into(),
|
||||||
|
port: db_info.port,
|
||||||
|
ssl_mode,
|
||||||
|
host_addr: None,
|
||||||
|
};
|
||||||
|
let auth_info =
|
||||||
|
AuthInfo::for_console_redirect(&db_info.dbname, &db_info.user, db_info.password.as_deref());
|
||||||
|
|
||||||
let user: RoleName = db_info.user.into();
|
let user: RoleName = db_info.user.into();
|
||||||
let user_info = ComputeUserInfo {
|
let user_info = ComputeUserInfo {
|
||||||
@@ -209,26 +214,12 @@ async fn authenticate(
|
|||||||
ctx.set_project(db_info.aux.clone());
|
ctx.set_project(db_info.aux.clone());
|
||||||
info!("woken up a compute node");
|
info!("woken up a compute node");
|
||||||
|
|
||||||
// Backwards compatibility. pg_sni_proxy uses "--" in domain names
|
|
||||||
// while direct connections do not. Once we migrate to pg_sni_proxy
|
|
||||||
// everywhere, we can remove this.
|
|
||||||
if db_info.host.contains("--") {
|
|
||||||
// we need TLS connection with SNI info to properly route it
|
|
||||||
config.ssl_mode(SslMode::Require);
|
|
||||||
} else {
|
|
||||||
config.ssl_mode(SslMode::Disable);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(password) = db_info.password {
|
|
||||||
config.password(password.as_ref());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
NodeInfo {
|
NodeInfo {
|
||||||
config,
|
conn_info,
|
||||||
aux: db_info.aux,
|
aux: db_info.aux,
|
||||||
},
|
},
|
||||||
|
auth_info,
|
||||||
user_info,
|
user_info,
|
||||||
db_info.allowed_ips,
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
use arc_swap::ArcSwapOption;
|
use arc_swap::ArcSwapOption;
|
||||||
|
use postgres_client::config::SslMode;
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
|
|
||||||
use super::jwt::{AuthRule, FetchAuthRules};
|
use super::jwt::{AuthRule, FetchAuthRules};
|
||||||
use crate::auth::backend::jwt::FetchAuthRulesError;
|
use crate::auth::backend::jwt::FetchAuthRulesError;
|
||||||
use crate::compute::ConnCfg;
|
use crate::compute::ConnectInfo;
|
||||||
use crate::compute_ctl::ComputeCtlApi;
|
use crate::compute_ctl::ComputeCtlApi;
|
||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::control_plane::NodeInfo;
|
use crate::control_plane::NodeInfo;
|
||||||
@@ -29,7 +30,12 @@ impl LocalBackend {
|
|||||||
api: http::Endpoint::new(compute_ctl, http::new_client()),
|
api: http::Endpoint::new(compute_ctl, http::new_client()),
|
||||||
},
|
},
|
||||||
node_info: NodeInfo {
|
node_info: NodeInfo {
|
||||||
config: ConnCfg::new(postgres_addr.ip().to_string(), postgres_addr.port()),
|
conn_info: ConnectInfo {
|
||||||
|
host_addr: Some(postgres_addr.ip()),
|
||||||
|
host: postgres_addr.ip().to_string().into(),
|
||||||
|
port: postgres_addr.port(),
|
||||||
|
ssl_mode: SslMode::Disable,
|
||||||
|
},
|
||||||
// TODO(conrad): make this better reflect compute info rather than endpoint info.
|
// TODO(conrad): make this better reflect compute info rather than endpoint info.
|
||||||
aux: MetricsAuxInfo {
|
aux: MetricsAuxInfo {
|
||||||
endpoint_id: EndpointIdTag::get_interner().get_or_intern("local"),
|
endpoint_id: EndpointIdTag::get_interner().get_or_intern("local"),
|
||||||
|
|||||||
@@ -4,38 +4,31 @@ mod hacks;
|
|||||||
pub mod jwt;
|
pub mod jwt;
|
||||||
pub mod local;
|
pub mod local;
|
||||||
|
|
||||||
use std::net::IpAddr;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use console_redirect::ConsoleRedirectBackend;
|
pub use console_redirect::ConsoleRedirectBackend;
|
||||||
pub(crate) use console_redirect::ConsoleRedirectError;
|
pub(crate) use console_redirect::ConsoleRedirectError;
|
||||||
use ipnet::{Ipv4Net, Ipv6Net};
|
|
||||||
use local::LocalBackend;
|
use local::LocalBackend;
|
||||||
use postgres_client::config::AuthKeys;
|
use postgres_client::config::AuthKeys;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use crate::auth::credentials::check_peer_addr_is_in_list;
|
use crate::auth::{self, AuthError, ComputeUserInfoMaybeEndpoint, validate_password_and_exchange};
|
||||||
use crate::auth::{
|
|
||||||
self, AuthError, ComputeUserInfoMaybeEndpoint, IpPattern, validate_password_and_exchange,
|
|
||||||
};
|
|
||||||
use crate::cache::Cached;
|
use crate::cache::Cached;
|
||||||
use crate::config::AuthenticationConfig;
|
use crate::config::AuthenticationConfig;
|
||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::control_plane::client::ControlPlaneClient;
|
use crate::control_plane::client::ControlPlaneClient;
|
||||||
use crate::control_plane::errors::GetAuthInfoError;
|
use crate::control_plane::errors::GetAuthInfoError;
|
||||||
use crate::control_plane::{
|
use crate::control_plane::{
|
||||||
self, AccessBlockerFlags, AuthSecret, CachedAccessBlockerFlags, CachedAllowedIps,
|
self, AccessBlockerFlags, AuthSecret, CachedNodeInfo, ControlPlaneApi, EndpointAccessControl,
|
||||||
CachedAllowedVpcEndpointIds, CachedNodeInfo, CachedRoleSecret, ControlPlaneApi,
|
RoleAccessControl,
|
||||||
};
|
};
|
||||||
use crate::intern::EndpointIdInt;
|
use crate::intern::EndpointIdInt;
|
||||||
use crate::metrics::Metrics;
|
|
||||||
use crate::pqproto::BeMessage;
|
use crate::pqproto::BeMessage;
|
||||||
use crate::protocol2::ConnectionInfoExtra;
|
|
||||||
use crate::proxy::NeonOptions;
|
use crate::proxy::NeonOptions;
|
||||||
use crate::proxy::connect_compute::ComputeConnectBackend;
|
use crate::proxy::wake_compute::WakeComputeBackend;
|
||||||
use crate::rate_limiter::{BucketRateLimiter, EndpointRateLimiter};
|
use crate::rate_limiter::EndpointRateLimiter;
|
||||||
use crate::stream::Stream;
|
use crate::stream::Stream;
|
||||||
use crate::types::{EndpointCacheKey, EndpointId, RoleName};
|
use crate::types::{EndpointCacheKey, EndpointId, RoleName};
|
||||||
use crate::{scram, stream};
|
use crate::{scram, stream};
|
||||||
@@ -175,8 +168,6 @@ impl ComputeUserInfo {
|
|||||||
|
|
||||||
#[cfg_attr(test, derive(Debug))]
|
#[cfg_attr(test, derive(Debug))]
|
||||||
pub(crate) enum ComputeCredentialKeys {
|
pub(crate) enum ComputeCredentialKeys {
|
||||||
#[cfg(any(test, feature = "testing"))]
|
|
||||||
Password(Vec<u8>),
|
|
||||||
AuthKeys(AuthKeys),
|
AuthKeys(AuthKeys),
|
||||||
JwtPayload(Vec<u8>),
|
JwtPayload(Vec<u8>),
|
||||||
None,
|
None,
|
||||||
@@ -201,78 +192,6 @@ impl TryFrom<ComputeUserInfoMaybeEndpoint> for ComputeUserInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, PartialOrd, Hash, Eq, Ord, Debug, Copy, Clone)]
|
|
||||||
pub struct MaskedIp(IpAddr);
|
|
||||||
|
|
||||||
impl MaskedIp {
|
|
||||||
fn new(value: IpAddr, prefix: u8) -> Self {
|
|
||||||
match value {
|
|
||||||
IpAddr::V4(v4) => Self(IpAddr::V4(
|
|
||||||
Ipv4Net::new(v4, prefix).map_or(v4, |x| x.trunc().addr()),
|
|
||||||
)),
|
|
||||||
IpAddr::V6(v6) => Self(IpAddr::V6(
|
|
||||||
Ipv6Net::new(v6, prefix).map_or(v6, |x| x.trunc().addr()),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This can't be just per IP because that would limit some PaaS that share IP addresses
|
|
||||||
pub type AuthRateLimiter = BucketRateLimiter<(EndpointIdInt, MaskedIp)>;
|
|
||||||
|
|
||||||
impl AuthenticationConfig {
|
|
||||||
pub(crate) fn check_rate_limit(
|
|
||||||
&self,
|
|
||||||
ctx: &RequestContext,
|
|
||||||
secret: AuthSecret,
|
|
||||||
endpoint: &EndpointId,
|
|
||||||
is_cleartext: bool,
|
|
||||||
) -> auth::Result<AuthSecret> {
|
|
||||||
// we have validated the endpoint exists, so let's intern it.
|
|
||||||
let endpoint_int = EndpointIdInt::from(endpoint.normalize());
|
|
||||||
|
|
||||||
// only count the full hash count if password hack or websocket flow.
|
|
||||||
// in other words, if proxy needs to run the hashing
|
|
||||||
let password_weight = if is_cleartext {
|
|
||||||
match &secret {
|
|
||||||
#[cfg(any(test, feature = "testing"))]
|
|
||||||
AuthSecret::Md5(_) => 1,
|
|
||||||
AuthSecret::Scram(s) => s.iterations + 1,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// validating scram takes just 1 hmac_sha_256 operation.
|
|
||||||
1
|
|
||||||
};
|
|
||||||
|
|
||||||
let limit_not_exceeded = self.rate_limiter.check(
|
|
||||||
(
|
|
||||||
endpoint_int,
|
|
||||||
MaskedIp::new(ctx.peer_addr(), self.rate_limit_ip_subnet),
|
|
||||||
),
|
|
||||||
password_weight,
|
|
||||||
);
|
|
||||||
|
|
||||||
if !limit_not_exceeded {
|
|
||||||
warn!(
|
|
||||||
enabled = self.rate_limiter_enabled,
|
|
||||||
"rate limiting authentication"
|
|
||||||
);
|
|
||||||
Metrics::get().proxy.requests_auth_rate_limits_total.inc();
|
|
||||||
Metrics::get()
|
|
||||||
.proxy
|
|
||||||
.endpoints_auth_rate_limits
|
|
||||||
.get_metric()
|
|
||||||
.measure(endpoint);
|
|
||||||
|
|
||||||
if self.rate_limiter_enabled {
|
|
||||||
return Err(auth::AuthError::too_many_connections());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(secret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// True to its name, this function encapsulates our current auth trade-offs.
|
/// True to its name, this function encapsulates our current auth trade-offs.
|
||||||
/// Here, we choose the appropriate auth flow based on circumstances.
|
/// Here, we choose the appropriate auth flow based on circumstances.
|
||||||
///
|
///
|
||||||
@@ -285,7 +204,7 @@ async fn auth_quirks(
|
|||||||
allow_cleartext: bool,
|
allow_cleartext: bool,
|
||||||
config: &'static AuthenticationConfig,
|
config: &'static AuthenticationConfig,
|
||||||
endpoint_rate_limiter: Arc<EndpointRateLimiter>,
|
endpoint_rate_limiter: Arc<EndpointRateLimiter>,
|
||||||
) -> auth::Result<(ComputeCredentials, Option<Vec<IpPattern>>)> {
|
) -> auth::Result<ComputeCredentials> {
|
||||||
// If there's no project so far, that entails that client doesn't
|
// If there's no project so far, that entails that client doesn't
|
||||||
// support SNI or other means of passing the endpoint (project) name.
|
// support SNI or other means of passing the endpoint (project) name.
|
||||||
// We now expect to see a very specific payload in the place of password.
|
// We now expect to see a very specific payload in the place of password.
|
||||||
@@ -301,55 +220,27 @@ async fn auth_quirks(
|
|||||||
|
|
||||||
debug!("fetching authentication info and allowlists");
|
debug!("fetching authentication info and allowlists");
|
||||||
|
|
||||||
// check allowed list
|
let access_controls = api
|
||||||
let allowed_ips = if config.ip_allowlist_check_enabled {
|
.get_endpoint_access_control(ctx, &info.endpoint, &info.user)
|
||||||
let allowed_ips = api.get_allowed_ips(ctx, &info).await?;
|
.await?;
|
||||||
if !check_peer_addr_is_in_list(&ctx.peer_addr(), &allowed_ips) {
|
|
||||||
return Err(auth::AuthError::ip_address_not_allowed(ctx.peer_addr()));
|
|
||||||
}
|
|
||||||
allowed_ips
|
|
||||||
} else {
|
|
||||||
Cached::new_uncached(Arc::new(vec![]))
|
|
||||||
};
|
|
||||||
|
|
||||||
// check if a VPC endpoint ID is coming in and if yes, if it's allowed
|
access_controls.check(
|
||||||
let access_blocks = api.get_block_public_or_vpc_access(ctx, &info).await?;
|
ctx,
|
||||||
if config.is_vpc_acccess_proxy {
|
config.ip_allowlist_check_enabled,
|
||||||
if access_blocks.vpc_access_blocked {
|
config.is_vpc_acccess_proxy,
|
||||||
return Err(AuthError::NetworkNotAllowed);
|
)?;
|
||||||
}
|
|
||||||
|
|
||||||
let incoming_vpc_endpoint_id = match ctx.extra() {
|
let endpoint = EndpointIdInt::from(&info.endpoint);
|
||||||
None => return Err(AuthError::MissingEndpointName),
|
let rate_limit_config = None;
|
||||||
Some(ConnectionInfoExtra::Aws { vpce_id }) => vpce_id.to_string(),
|
if !endpoint_rate_limiter.check(endpoint, rate_limit_config, 1) {
|
||||||
Some(ConnectionInfoExtra::Azure { link_id }) => link_id.to_string(),
|
|
||||||
};
|
|
||||||
let allowed_vpc_endpoint_ids = api.get_allowed_vpc_endpoint_ids(ctx, &info).await?;
|
|
||||||
// TODO: For now an empty VPC endpoint ID list means all are allowed. We should replace that.
|
|
||||||
if !allowed_vpc_endpoint_ids.is_empty()
|
|
||||||
&& !allowed_vpc_endpoint_ids.contains(&incoming_vpc_endpoint_id)
|
|
||||||
{
|
|
||||||
return Err(AuthError::vpc_endpoint_id_not_allowed(
|
|
||||||
incoming_vpc_endpoint_id,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else if access_blocks.public_access_blocked {
|
|
||||||
return Err(AuthError::NetworkNotAllowed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !endpoint_rate_limiter.check(info.endpoint.clone().into(), 1) {
|
|
||||||
return Err(AuthError::too_many_connections());
|
return Err(AuthError::too_many_connections());
|
||||||
}
|
}
|
||||||
let cached_secret = api.get_role_secret(ctx, &info).await?;
|
let role_access = api
|
||||||
let (cached_entry, secret) = cached_secret.take_value();
|
.get_role_access_control(ctx, &info.endpoint, &info.user)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let secret = if let Some(secret) = secret {
|
let secret = if let Some(secret) = role_access.secret {
|
||||||
config.check_rate_limit(
|
secret
|
||||||
ctx,
|
|
||||||
secret,
|
|
||||||
&info.endpoint,
|
|
||||||
unauthenticated_password.is_some() || allow_cleartext,
|
|
||||||
)?
|
|
||||||
} else {
|
} else {
|
||||||
// If we don't have an authentication secret, we mock one to
|
// If we don't have an authentication secret, we mock one to
|
||||||
// prevent malicious probing (possible due to missing protocol steps).
|
// prevent malicious probing (possible due to missing protocol steps).
|
||||||
@@ -369,14 +260,8 @@ async fn auth_quirks(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(keys) => Ok((keys, Some(allowed_ips.as_ref().clone()))),
|
Ok(keys) => Ok(keys),
|
||||||
Err(e) => {
|
Err(e) => Err(e),
|
||||||
if e.is_password_failed() {
|
|
||||||
// The password could have been changed, so we invalidate the cache.
|
|
||||||
cached_entry.invalidate();
|
|
||||||
}
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,7 +324,7 @@ impl<'a> Backend<'a, ComputeUserInfoMaybeEndpoint> {
|
|||||||
allow_cleartext: bool,
|
allow_cleartext: bool,
|
||||||
config: &'static AuthenticationConfig,
|
config: &'static AuthenticationConfig,
|
||||||
endpoint_rate_limiter: Arc<EndpointRateLimiter>,
|
endpoint_rate_limiter: Arc<EndpointRateLimiter>,
|
||||||
) -> auth::Result<(Backend<'a, ComputeCredentials>, Option<Vec<IpPattern>>)> {
|
) -> auth::Result<Backend<'a, ComputeCredentials>> {
|
||||||
let res = match self {
|
let res = match self {
|
||||||
Self::ControlPlane(api, user_info) => {
|
Self::ControlPlane(api, user_info) => {
|
||||||
debug!(
|
debug!(
|
||||||
@@ -448,17 +333,35 @@ impl<'a> Backend<'a, ComputeUserInfoMaybeEndpoint> {
|
|||||||
"performing authentication using the console"
|
"performing authentication using the console"
|
||||||
);
|
);
|
||||||
|
|
||||||
let (credentials, ip_allowlist) = auth_quirks(
|
let auth_res = auth_quirks(
|
||||||
ctx,
|
ctx,
|
||||||
&*api,
|
&*api,
|
||||||
user_info,
|
user_info.clone(),
|
||||||
client,
|
client,
|
||||||
allow_cleartext,
|
allow_cleartext,
|
||||||
config,
|
config,
|
||||||
endpoint_rate_limiter,
|
endpoint_rate_limiter,
|
||||||
)
|
)
|
||||||
.await?;
|
.await;
|
||||||
Ok((Backend::ControlPlane(api, credentials), ip_allowlist))
|
match auth_res {
|
||||||
|
Ok(credentials) => Ok(Backend::ControlPlane(api, credentials)),
|
||||||
|
Err(e) => {
|
||||||
|
// The password could have been changed, so we invalidate the cache.
|
||||||
|
// We should only invalidate the cache if the TTL might have expired.
|
||||||
|
if e.is_password_failed() {
|
||||||
|
#[allow(irrefutable_let_patterns)]
|
||||||
|
if let ControlPlaneClient::ProxyV1(api) = &*api {
|
||||||
|
if let Some(ep) = &user_info.endpoint_id {
|
||||||
|
api.caches
|
||||||
|
.project_info
|
||||||
|
.maybe_invalidate_role_secret(ep, &user_info.user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Self::Local(_) => {
|
Self::Local(_) => {
|
||||||
return Err(auth::AuthError::bad_auth_method("invalid for local proxy"));
|
return Err(auth::AuthError::bad_auth_method("invalid for local proxy"));
|
||||||
@@ -475,75 +378,52 @@ impl Backend<'_, ComputeUserInfo> {
|
|||||||
pub(crate) async fn get_role_secret(
|
pub(crate) async fn get_role_secret(
|
||||||
&self,
|
&self,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<CachedRoleSecret, GetAuthInfoError> {
|
) -> Result<RoleAccessControl, GetAuthInfoError> {
|
||||||
match self {
|
|
||||||
Self::ControlPlane(api, user_info) => api.get_role_secret(ctx, user_info).await,
|
|
||||||
Self::Local(_) => Ok(Cached::new_uncached(None)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn get_allowed_ips(
|
|
||||||
&self,
|
|
||||||
ctx: &RequestContext,
|
|
||||||
) -> Result<CachedAllowedIps, GetAuthInfoError> {
|
|
||||||
match self {
|
|
||||||
Self::ControlPlane(api, user_info) => api.get_allowed_ips(ctx, user_info).await,
|
|
||||||
Self::Local(_) => Ok(Cached::new_uncached(Arc::new(vec![]))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn get_allowed_vpc_endpoint_ids(
|
|
||||||
&self,
|
|
||||||
ctx: &RequestContext,
|
|
||||||
) -> Result<CachedAllowedVpcEndpointIds, GetAuthInfoError> {
|
|
||||||
match self {
|
match self {
|
||||||
Self::ControlPlane(api, user_info) => {
|
Self::ControlPlane(api, user_info) => {
|
||||||
api.get_allowed_vpc_endpoint_ids(ctx, user_info).await
|
api.get_role_access_control(ctx, &user_info.endpoint, &user_info.user)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
Self::Local(_) => Ok(Cached::new_uncached(Arc::new(vec![]))),
|
Self::Local(_) => Ok(RoleAccessControl { secret: None }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn get_block_public_or_vpc_access(
|
pub(crate) async fn get_endpoint_access_control(
|
||||||
&self,
|
&self,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<CachedAccessBlockerFlags, GetAuthInfoError> {
|
) -> Result<EndpointAccessControl, GetAuthInfoError> {
|
||||||
match self {
|
match self {
|
||||||
Self::ControlPlane(api, user_info) => {
|
Self::ControlPlane(api, user_info) => {
|
||||||
api.get_block_public_or_vpc_access(ctx, user_info).await
|
api.get_endpoint_access_control(ctx, &user_info.endpoint, &user_info.user)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
Self::Local(_) => Ok(Cached::new_uncached(AccessBlockerFlags::default())),
|
Self::Local(_) => Ok(EndpointAccessControl {
|
||||||
|
allowed_ips: Arc::new(vec![]),
|
||||||
|
allowed_vpce: Arc::new(vec![]),
|
||||||
|
flags: AccessBlockerFlags::default(),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl ComputeConnectBackend for Backend<'_, ComputeCredentials> {
|
impl WakeComputeBackend for Backend<'_, ComputeUserInfo> {
|
||||||
async fn wake_compute(
|
async fn wake_compute(
|
||||||
&self,
|
&self,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<CachedNodeInfo, control_plane::errors::WakeComputeError> {
|
) -> Result<CachedNodeInfo, control_plane::errors::WakeComputeError> {
|
||||||
match self {
|
match self {
|
||||||
Self::ControlPlane(api, creds) => api.wake_compute(ctx, &creds.info).await,
|
Self::ControlPlane(api, info) => api.wake_compute(ctx, info).await,
|
||||||
Self::Local(local) => Ok(Cached::new_uncached(local.node_info.clone())),
|
Self::Local(local) => Ok(Cached::new_uncached(local.node_info.clone())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_keys(&self) -> &ComputeCredentialKeys {
|
|
||||||
match self {
|
|
||||||
Self::ControlPlane(_, creds) => &creds.keys,
|
|
||||||
Self::Local(_) => &ComputeCredentialKeys::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#![allow(clippy::unimplemented, clippy::unwrap_used)]
|
#![allow(clippy::unimplemented, clippy::unwrap_used)]
|
||||||
|
|
||||||
use std::net::IpAddr;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use control_plane::AuthSecret;
|
use control_plane::AuthSecret;
|
||||||
@@ -554,18 +434,16 @@ mod tests {
|
|||||||
use postgres_protocol::message::frontend;
|
use postgres_protocol::message::frontend;
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
|
use super::auth_quirks;
|
||||||
use super::jwt::JwkCache;
|
use super::jwt::JwkCache;
|
||||||
use super::{AuthRateLimiter, auth_quirks};
|
|
||||||
use crate::auth::backend::MaskedIp;
|
|
||||||
use crate::auth::{ComputeUserInfoMaybeEndpoint, IpPattern};
|
use crate::auth::{ComputeUserInfoMaybeEndpoint, IpPattern};
|
||||||
use crate::config::AuthenticationConfig;
|
use crate::config::AuthenticationConfig;
|
||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::control_plane::{
|
use crate::control_plane::{
|
||||||
self, AccessBlockerFlags, CachedAccessBlockerFlags, CachedAllowedIps,
|
self, AccessBlockerFlags, CachedNodeInfo, EndpointAccessControl, RoleAccessControl,
|
||||||
CachedAllowedVpcEndpointIds, CachedNodeInfo, CachedRoleSecret,
|
|
||||||
};
|
};
|
||||||
use crate::proxy::NeonOptions;
|
use crate::proxy::NeonOptions;
|
||||||
use crate::rate_limiter::{EndpointRateLimiter, RateBucketInfo};
|
use crate::rate_limiter::EndpointRateLimiter;
|
||||||
use crate::scram::ServerSecret;
|
use crate::scram::ServerSecret;
|
||||||
use crate::scram::threadpool::ThreadPool;
|
use crate::scram::threadpool::ThreadPool;
|
||||||
use crate::stream::{PqStream, Stream};
|
use crate::stream::{PqStream, Stream};
|
||||||
@@ -578,46 +456,34 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl control_plane::ControlPlaneApi for Auth {
|
impl control_plane::ControlPlaneApi for Auth {
|
||||||
async fn get_role_secret(
|
async fn get_role_access_control(
|
||||||
&self,
|
&self,
|
||||||
_ctx: &RequestContext,
|
_ctx: &RequestContext,
|
||||||
_user_info: &super::ComputeUserInfo,
|
_endpoint: &crate::types::EndpointId,
|
||||||
) -> Result<CachedRoleSecret, control_plane::errors::GetAuthInfoError> {
|
_role: &crate::types::RoleName,
|
||||||
Ok(CachedRoleSecret::new_uncached(Some(self.secret.clone())))
|
) -> Result<RoleAccessControl, control_plane::errors::GetAuthInfoError> {
|
||||||
|
Ok(RoleAccessControl {
|
||||||
|
secret: Some(self.secret.clone()),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_allowed_ips(
|
async fn get_endpoint_access_control(
|
||||||
&self,
|
&self,
|
||||||
_ctx: &RequestContext,
|
_ctx: &RequestContext,
|
||||||
_user_info: &super::ComputeUserInfo,
|
_endpoint: &crate::types::EndpointId,
|
||||||
) -> Result<CachedAllowedIps, control_plane::errors::GetAuthInfoError> {
|
_role: &crate::types::RoleName,
|
||||||
Ok(CachedAllowedIps::new_uncached(Arc::new(self.ips.clone())))
|
) -> Result<EndpointAccessControl, control_plane::errors::GetAuthInfoError> {
|
||||||
}
|
Ok(EndpointAccessControl {
|
||||||
|
allowed_ips: Arc::new(self.ips.clone()),
|
||||||
async fn get_allowed_vpc_endpoint_ids(
|
allowed_vpce: Arc::new(self.vpc_endpoint_ids.clone()),
|
||||||
&self,
|
flags: self.access_blocker_flags,
|
||||||
_ctx: &RequestContext,
|
})
|
||||||
_user_info: &super::ComputeUserInfo,
|
|
||||||
) -> Result<CachedAllowedVpcEndpointIds, control_plane::errors::GetAuthInfoError> {
|
|
||||||
Ok(CachedAllowedVpcEndpointIds::new_uncached(Arc::new(
|
|
||||||
self.vpc_endpoint_ids.clone(),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_block_public_or_vpc_access(
|
|
||||||
&self,
|
|
||||||
_ctx: &RequestContext,
|
|
||||||
_user_info: &super::ComputeUserInfo,
|
|
||||||
) -> Result<CachedAccessBlockerFlags, control_plane::errors::GetAuthInfoError> {
|
|
||||||
Ok(CachedAccessBlockerFlags::new_uncached(
|
|
||||||
self.access_blocker_flags.clone(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_endpoint_jwks(
|
async fn get_endpoint_jwks(
|
||||||
&self,
|
&self,
|
||||||
_ctx: &RequestContext,
|
_ctx: &RequestContext,
|
||||||
_endpoint: crate::types::EndpointId,
|
_endpoint: &crate::types::EndpointId,
|
||||||
) -> Result<Vec<super::jwt::AuthRule>, control_plane::errors::GetEndpointJwksError>
|
) -> Result<Vec<super::jwt::AuthRule>, control_plane::errors::GetEndpointJwksError>
|
||||||
{
|
{
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
@@ -636,9 +502,6 @@ mod tests {
|
|||||||
jwks_cache: JwkCache::default(),
|
jwks_cache: JwkCache::default(),
|
||||||
thread_pool: ThreadPool::new(1),
|
thread_pool: ThreadPool::new(1),
|
||||||
scram_protocol_timeout: std::time::Duration::from_secs(5),
|
scram_protocol_timeout: std::time::Duration::from_secs(5),
|
||||||
rate_limiter_enabled: true,
|
|
||||||
rate_limiter: AuthRateLimiter::new(&RateBucketInfo::DEFAULT_AUTH_SET),
|
|
||||||
rate_limit_ip_subnet: 64,
|
|
||||||
ip_allowlist_check_enabled: true,
|
ip_allowlist_check_enabled: true,
|
||||||
is_vpc_acccess_proxy: false,
|
is_vpc_acccess_proxy: false,
|
||||||
is_auth_broker: false,
|
is_auth_broker: false,
|
||||||
@@ -655,51 +518,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn masked_ip() {
|
|
||||||
let ip_a = IpAddr::V4([127, 0, 0, 1].into());
|
|
||||||
let ip_b = IpAddr::V4([127, 0, 0, 2].into());
|
|
||||||
let ip_c = IpAddr::V4([192, 168, 1, 101].into());
|
|
||||||
let ip_d = IpAddr::V4([192, 168, 1, 102].into());
|
|
||||||
let ip_e = IpAddr::V6("abcd:abcd:abcd:abcd:abcd:abcd:abcd:abcd".parse().unwrap());
|
|
||||||
let ip_f = IpAddr::V6("abcd:abcd:abcd:abcd:1234:abcd:abcd:abcd".parse().unwrap());
|
|
||||||
|
|
||||||
assert_ne!(MaskedIp::new(ip_a, 64), MaskedIp::new(ip_b, 64));
|
|
||||||
assert_ne!(MaskedIp::new(ip_a, 32), MaskedIp::new(ip_b, 32));
|
|
||||||
assert_eq!(MaskedIp::new(ip_a, 30), MaskedIp::new(ip_b, 30));
|
|
||||||
assert_eq!(MaskedIp::new(ip_c, 30), MaskedIp::new(ip_d, 30));
|
|
||||||
|
|
||||||
assert_ne!(MaskedIp::new(ip_e, 128), MaskedIp::new(ip_f, 128));
|
|
||||||
assert_eq!(MaskedIp::new(ip_e, 64), MaskedIp::new(ip_f, 64));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_default_auth_rate_limit_set() {
|
|
||||||
// these values used to exceed u32::MAX
|
|
||||||
assert_eq!(
|
|
||||||
RateBucketInfo::DEFAULT_AUTH_SET,
|
|
||||||
[
|
|
||||||
RateBucketInfo {
|
|
||||||
interval: Duration::from_secs(1),
|
|
||||||
max_rpi: 1000 * 4096,
|
|
||||||
},
|
|
||||||
RateBucketInfo {
|
|
||||||
interval: Duration::from_secs(60),
|
|
||||||
max_rpi: 600 * 4096 * 60,
|
|
||||||
},
|
|
||||||
RateBucketInfo {
|
|
||||||
interval: Duration::from_secs(600),
|
|
||||||
max_rpi: 300 * 4096 * 600,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
for x in RateBucketInfo::DEFAULT_AUTH_SET {
|
|
||||||
let y = x.to_string().parse().unwrap();
|
|
||||||
assert_eq!(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn auth_quirks_scram() {
|
async fn auth_quirks_scram() {
|
||||||
let (mut client, server) = tokio::io::duplex(1024);
|
let (mut client, server) = tokio::io::duplex(1024);
|
||||||
@@ -888,7 +706,7 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(creds.0.info.endpoint, "my-endpoint");
|
assert_eq!(creds.info.endpoint, "my-endpoint");
|
||||||
|
|
||||||
handle.await.unwrap();
|
handle.await.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,13 +169,6 @@ pub(crate) async fn validate_password_and_exchange(
|
|||||||
secret: AuthSecret,
|
secret: AuthSecret,
|
||||||
) -> super::Result<sasl::Outcome<ComputeCredentialKeys>> {
|
) -> super::Result<sasl::Outcome<ComputeCredentialKeys>> {
|
||||||
match secret {
|
match secret {
|
||||||
#[cfg(any(test, feature = "testing"))]
|
|
||||||
AuthSecret::Md5(_) => {
|
|
||||||
// test only
|
|
||||||
Ok(sasl::Outcome::Success(ComputeCredentialKeys::Password(
|
|
||||||
password.to_owned(),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
// perform scram authentication as both client and server to validate the keys
|
// perform scram authentication as both client and server to validate the keys
|
||||||
AuthSecret::Scram(scram_secret) => {
|
AuthSecret::Scram(scram_secret) => {
|
||||||
let outcome = crate::scram::exchange(pool, endpoint, &scram_secret, password).await?;
|
let outcome = crate::scram::exchange(pool, endpoint, &scram_secret, password).await?;
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ use crate::ext::TaskExt;
|
|||||||
use crate::http::health_server::AppMetrics;
|
use crate::http::health_server::AppMetrics;
|
||||||
use crate::intern::RoleNameInt;
|
use crate::intern::RoleNameInt;
|
||||||
use crate::metrics::{Metrics, ThreadPoolMetrics};
|
use crate::metrics::{Metrics, ThreadPoolMetrics};
|
||||||
use crate::rate_limiter::{
|
use crate::rate_limiter::{EndpointRateLimiter, LeakyBucketConfig, RateBucketInfo};
|
||||||
BucketRateLimiter, EndpointRateLimiter, LeakyBucketConfig, RateBucketInfo,
|
|
||||||
};
|
|
||||||
use crate::scram::threadpool::ThreadPool;
|
use crate::scram::threadpool::ThreadPool;
|
||||||
use crate::serverless::cancel_set::CancelSet;
|
use crate::serverless::cancel_set::CancelSet;
|
||||||
use crate::serverless::{self, GlobalConnPoolOptions};
|
use crate::serverless::{self, GlobalConnPoolOptions};
|
||||||
@@ -69,15 +67,6 @@ struct LocalProxyCliArgs {
|
|||||||
/// Can be given multiple times for different bucket sizes.
|
/// Can be given multiple times for different bucket sizes.
|
||||||
#[clap(long, default_values_t = RateBucketInfo::DEFAULT_ENDPOINT_SET)]
|
#[clap(long, default_values_t = RateBucketInfo::DEFAULT_ENDPOINT_SET)]
|
||||||
user_rps_limit: Vec<RateBucketInfo>,
|
user_rps_limit: Vec<RateBucketInfo>,
|
||||||
/// Whether the auth rate limiter actually takes effect (for testing)
|
|
||||||
#[clap(long, default_value_t = false, value_parser = clap::builder::BoolishValueParser::new(), action = clap::ArgAction::Set)]
|
|
||||||
auth_rate_limit_enabled: bool,
|
|
||||||
/// Authentication rate limiter max number of hashes per second.
|
|
||||||
#[clap(long, default_values_t = RateBucketInfo::DEFAULT_AUTH_SET)]
|
|
||||||
auth_rate_limit: Vec<RateBucketInfo>,
|
|
||||||
/// The IP subnet to use when considering whether two IP addresses are considered the same.
|
|
||||||
#[clap(long, default_value_t = 64)]
|
|
||||||
auth_rate_limit_ip_subnet: u8,
|
|
||||||
/// Whether to retry the connection to the compute node
|
/// Whether to retry the connection to the compute node
|
||||||
#[clap(long, default_value = config::RetryConfig::CONNECT_TO_COMPUTE_DEFAULT_VALUES)]
|
#[clap(long, default_value = config::RetryConfig::CONNECT_TO_COMPUTE_DEFAULT_VALUES)]
|
||||||
connect_to_compute_retry: String,
|
connect_to_compute_retry: String,
|
||||||
@@ -282,9 +271,6 @@ fn build_config(args: &LocalProxyCliArgs) -> anyhow::Result<&'static ProxyConfig
|
|||||||
jwks_cache: JwkCache::default(),
|
jwks_cache: JwkCache::default(),
|
||||||
thread_pool: ThreadPool::new(0),
|
thread_pool: ThreadPool::new(0),
|
||||||
scram_protocol_timeout: Duration::from_secs(10),
|
scram_protocol_timeout: Duration::from_secs(10),
|
||||||
rate_limiter_enabled: false,
|
|
||||||
rate_limiter: BucketRateLimiter::new(vec![]),
|
|
||||||
rate_limit_ip_subnet: 64,
|
|
||||||
ip_allowlist_check_enabled: true,
|
ip_allowlist_check_enabled: true,
|
||||||
is_vpc_acccess_proxy: false,
|
is_vpc_acccess_proxy: false,
|
||||||
is_auth_broker: false,
|
is_auth_broker: false,
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ use utils::sentry_init::init_sentry;
|
|||||||
|
|
||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::metrics::{Metrics, ThreadPoolMetrics};
|
use crate::metrics::{Metrics, ThreadPoolMetrics};
|
||||||
|
use crate::pglb::TlsRequired;
|
||||||
use crate::pqproto::FeStartupPacket;
|
use crate::pqproto::FeStartupPacket;
|
||||||
use crate::protocol2::ConnectionInfo;
|
use crate::protocol2::ConnectionInfo;
|
||||||
use crate::proxy::{
|
use crate::proxy::{ErrorSource, copy_bidirectional_client_compute};
|
||||||
ErrorSource, TlsRequired, copy_bidirectional_client_compute, run_until_cancelled,
|
|
||||||
};
|
|
||||||
use crate::stream::{PqStream, Stream};
|
use crate::stream::{PqStream, Stream};
|
||||||
|
use crate::util::run_until_cancelled;
|
||||||
|
|
||||||
project_git_version!(GIT_VERSION);
|
project_git_version!(GIT_VERSION);
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use utils::sentry_init::init_sentry;
|
|||||||
use utils::{project_build_tag, project_git_version};
|
use utils::{project_build_tag, project_git_version};
|
||||||
|
|
||||||
use crate::auth::backend::jwt::JwkCache;
|
use crate::auth::backend::jwt::JwkCache;
|
||||||
use crate::auth::backend::{AuthRateLimiter, ConsoleRedirectBackend, MaybeOwned};
|
use crate::auth::backend::{ConsoleRedirectBackend, MaybeOwned};
|
||||||
use crate::cancellation::{CancellationHandler, handle_cancel_messages};
|
use crate::cancellation::{CancellationHandler, handle_cancel_messages};
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
self, AuthenticationConfig, CacheOptions, ComputeConfig, HttpConfig, ProjectInfoCacheOptions,
|
self, AuthenticationConfig, CacheOptions, ComputeConfig, HttpConfig, ProjectInfoCacheOptions,
|
||||||
@@ -29,9 +29,7 @@ use crate::config::{
|
|||||||
use crate::context::parquet::ParquetUploadArgs;
|
use crate::context::parquet::ParquetUploadArgs;
|
||||||
use crate::http::health_server::AppMetrics;
|
use crate::http::health_server::AppMetrics;
|
||||||
use crate::metrics::Metrics;
|
use crate::metrics::Metrics;
|
||||||
use crate::rate_limiter::{
|
use crate::rate_limiter::{EndpointRateLimiter, RateBucketInfo, WakeComputeRateLimiter};
|
||||||
EndpointRateLimiter, LeakyBucketConfig, RateBucketInfo, WakeComputeRateLimiter,
|
|
||||||
};
|
|
||||||
use crate::redis::connection_with_credentials_provider::ConnectionWithCredentialsProvider;
|
use crate::redis::connection_with_credentials_provider::ConnectionWithCredentialsProvider;
|
||||||
use crate::redis::kv_ops::RedisKVClient;
|
use crate::redis::kv_ops::RedisKVClient;
|
||||||
use crate::redis::{elasticache, notifications};
|
use crate::redis::{elasticache, notifications};
|
||||||
@@ -154,15 +152,6 @@ struct ProxyCliArgs {
|
|||||||
/// Wake compute rate limiter max number of requests per second.
|
/// Wake compute rate limiter max number of requests per second.
|
||||||
#[clap(long, default_values_t = RateBucketInfo::DEFAULT_SET)]
|
#[clap(long, default_values_t = RateBucketInfo::DEFAULT_SET)]
|
||||||
wake_compute_limit: Vec<RateBucketInfo>,
|
wake_compute_limit: Vec<RateBucketInfo>,
|
||||||
/// Whether the auth rate limiter actually takes effect (for testing)
|
|
||||||
#[clap(long, default_value_t = false, value_parser = clap::builder::BoolishValueParser::new(), action = clap::ArgAction::Set)]
|
|
||||||
auth_rate_limit_enabled: bool,
|
|
||||||
/// Authentication rate limiter max number of hashes per second.
|
|
||||||
#[clap(long, default_values_t = RateBucketInfo::DEFAULT_AUTH_SET)]
|
|
||||||
auth_rate_limit: Vec<RateBucketInfo>,
|
|
||||||
/// The IP subnet to use when considering whether two IP addresses are considered the same.
|
|
||||||
#[clap(long, default_value_t = 64)]
|
|
||||||
auth_rate_limit_ip_subnet: u8,
|
|
||||||
/// Redis rate limiter max number of requests per second.
|
/// Redis rate limiter max number of requests per second.
|
||||||
#[clap(long, default_values_t = RateBucketInfo::DEFAULT_REDIS_SET)]
|
#[clap(long, default_values_t = RateBucketInfo::DEFAULT_REDIS_SET)]
|
||||||
redis_rps_limit: Vec<RateBucketInfo>,
|
redis_rps_limit: Vec<RateBucketInfo>,
|
||||||
@@ -232,8 +221,7 @@ struct ProxyCliArgs {
|
|||||||
is_private_access_proxy: bool,
|
is_private_access_proxy: bool,
|
||||||
|
|
||||||
/// Configure whether all incoming requests have a Proxy Protocol V2 packet.
|
/// Configure whether all incoming requests have a Proxy Protocol V2 packet.
|
||||||
// TODO(conradludgate): switch default to rejected or required once we've updated all deployments
|
#[clap(value_enum, long, default_value_t = ProxyProtocolV2::Rejected)]
|
||||||
#[clap(value_enum, long, default_value_t = ProxyProtocolV2::Supported)]
|
|
||||||
proxy_protocol_v2: ProxyProtocolV2,
|
proxy_protocol_v2: ProxyProtocolV2,
|
||||||
|
|
||||||
/// Time the proxy waits for the webauth session to be confirmed by the control plane.
|
/// Time the proxy waits for the webauth session to be confirmed by the control plane.
|
||||||
@@ -410,22 +398,9 @@ pub async fn run() -> anyhow::Result<()> {
|
|||||||
Some(tx_cancel),
|
Some(tx_cancel),
|
||||||
));
|
));
|
||||||
|
|
||||||
// bit of a hack - find the min rps and max rps supported and turn it into
|
|
||||||
// leaky bucket config instead
|
|
||||||
let max = args
|
|
||||||
.endpoint_rps_limit
|
|
||||||
.iter()
|
|
||||||
.map(|x| x.rps())
|
|
||||||
.max_by(f64::total_cmp)
|
|
||||||
.unwrap_or(EndpointRateLimiter::DEFAULT.max);
|
|
||||||
let rps = args
|
|
||||||
.endpoint_rps_limit
|
|
||||||
.iter()
|
|
||||||
.map(|x| x.rps())
|
|
||||||
.min_by(f64::total_cmp)
|
|
||||||
.unwrap_or(EndpointRateLimiter::DEFAULT.rps);
|
|
||||||
let endpoint_rate_limiter = Arc::new(EndpointRateLimiter::new_with_shards(
|
let endpoint_rate_limiter = Arc::new(EndpointRateLimiter::new_with_shards(
|
||||||
LeakyBucketConfig { rps, max },
|
RateBucketInfo::to_leaky_bucket(&args.endpoint_rps_limit)
|
||||||
|
.unwrap_or(EndpointRateLimiter::DEFAULT),
|
||||||
64,
|
64,
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -435,7 +410,7 @@ pub async fn run() -> anyhow::Result<()> {
|
|||||||
match auth_backend {
|
match auth_backend {
|
||||||
Either::Left(auth_backend) => {
|
Either::Left(auth_backend) => {
|
||||||
if let Some(proxy_listener) = proxy_listener {
|
if let Some(proxy_listener) = proxy_listener {
|
||||||
client_tasks.spawn(crate::proxy::task_main(
|
client_tasks.spawn(crate::pglb::task_main(
|
||||||
config,
|
config,
|
||||||
auth_backend,
|
auth_backend,
|
||||||
proxy_listener,
|
proxy_listener,
|
||||||
@@ -678,9 +653,6 @@ fn build_config(args: &ProxyCliArgs) -> anyhow::Result<&'static ProxyConfig> {
|
|||||||
jwks_cache: JwkCache::default(),
|
jwks_cache: JwkCache::default(),
|
||||||
thread_pool,
|
thread_pool,
|
||||||
scram_protocol_timeout: args.scram_protocol_timeout,
|
scram_protocol_timeout: args.scram_protocol_timeout,
|
||||||
rate_limiter_enabled: args.auth_rate_limit_enabled,
|
|
||||||
rate_limiter: AuthRateLimiter::new(args.auth_rate_limit.clone()),
|
|
||||||
rate_limit_ip_subnet: args.auth_rate_limit_ip_subnet,
|
|
||||||
ip_allowlist_check_enabled: !args.is_private_access_proxy,
|
ip_allowlist_check_enabled: !args.is_private_access_proxy,
|
||||||
is_vpc_acccess_proxy: args.is_private_access_proxy,
|
is_vpc_acccess_proxy: args.is_private_access_proxy,
|
||||||
is_auth_broker: args.is_auth_broker,
|
is_auth_broker: args.is_auth_broker,
|
||||||
|
|||||||
686
proxy/src/cache/project_info.rs
vendored
686
proxy/src/cache/project_info.rs
vendored
@@ -1,30 +1,26 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet, hash_map};
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::AtomicU64;
|
use std::sync::atomic::AtomicU64;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use clashmap::ClashMap;
|
use clashmap::ClashMap;
|
||||||
|
use clashmap::mapref::one::Ref;
|
||||||
use rand::{Rng, thread_rng};
|
use rand::{Rng, thread_rng};
|
||||||
use smol_str::SmolStr;
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use super::{Cache, Cached};
|
|
||||||
use crate::auth::IpPattern;
|
|
||||||
use crate::config::ProjectInfoCacheOptions;
|
use crate::config::ProjectInfoCacheOptions;
|
||||||
use crate::control_plane::{AccessBlockerFlags, AuthSecret};
|
use crate::control_plane::{EndpointAccessControl, RoleAccessControl};
|
||||||
use crate::intern::{AccountIdInt, EndpointIdInt, ProjectIdInt, RoleNameInt};
|
use crate::intern::{AccountIdInt, EndpointIdInt, ProjectIdInt, RoleNameInt};
|
||||||
use crate::types::{EndpointId, RoleName};
|
use crate::types::{EndpointId, RoleName};
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub(crate) trait ProjectInfoCache {
|
pub(crate) trait ProjectInfoCache {
|
||||||
fn invalidate_allowed_ips_for_project(&self, project_id: ProjectIdInt);
|
fn invalidate_endpoint_access(&self, endpoint_id: EndpointIdInt);
|
||||||
fn invalidate_allowed_vpc_endpoint_ids_for_projects(&self, project_ids: Vec<ProjectIdInt>);
|
fn invalidate_endpoint_access_for_project(&self, project_id: ProjectIdInt);
|
||||||
fn invalidate_allowed_vpc_endpoint_ids_for_org(&self, account_id: AccountIdInt);
|
fn invalidate_endpoint_access_for_org(&self, account_id: AccountIdInt);
|
||||||
fn invalidate_block_public_or_vpc_access_for_project(&self, project_id: ProjectIdInt);
|
|
||||||
fn invalidate_role_secret_for_project(&self, project_id: ProjectIdInt, role_name: RoleNameInt);
|
fn invalidate_role_secret_for_project(&self, project_id: ProjectIdInt, role_name: RoleNameInt);
|
||||||
async fn decrement_active_listeners(&self);
|
async fn decrement_active_listeners(&self);
|
||||||
async fn increment_active_listeners(&self);
|
async fn increment_active_listeners(&self);
|
||||||
@@ -42,6 +38,10 @@ impl<T> Entry<T> {
|
|||||||
value,
|
value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get(&self, valid_since: Instant) -> Option<&T> {
|
||||||
|
(valid_since < self.created_at).then_some(&self.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<T> for Entry<T> {
|
impl<T> From<T> for Entry<T> {
|
||||||
@@ -50,101 +50,32 @@ impl<T> From<T> for Entry<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct EndpointInfo {
|
struct EndpointInfo {
|
||||||
secret: std::collections::HashMap<RoleNameInt, Entry<Option<AuthSecret>>>,
|
role_controls: HashMap<RoleNameInt, Entry<RoleAccessControl>>,
|
||||||
allowed_ips: Option<Entry<Arc<Vec<IpPattern>>>>,
|
controls: Option<Entry<EndpointAccessControl>>,
|
||||||
block_public_or_vpc_access: Option<Entry<AccessBlockerFlags>>,
|
|
||||||
allowed_vpc_endpoint_ids: Option<Entry<Arc<Vec<String>>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EndpointInfo {
|
impl EndpointInfo {
|
||||||
fn check_ignore_cache(ignore_cache_since: Option<Instant>, created_at: Instant) -> bool {
|
|
||||||
match ignore_cache_since {
|
|
||||||
None => false,
|
|
||||||
Some(t) => t < created_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub(crate) fn get_role_secret(
|
pub(crate) fn get_role_secret(
|
||||||
&self,
|
&self,
|
||||||
role_name: RoleNameInt,
|
role_name: RoleNameInt,
|
||||||
valid_since: Instant,
|
valid_since: Instant,
|
||||||
ignore_cache_since: Option<Instant>,
|
) -> Option<RoleAccessControl> {
|
||||||
) -> Option<(Option<AuthSecret>, bool)> {
|
let controls = self.role_controls.get(&role_name)?;
|
||||||
if let Some(secret) = self.secret.get(&role_name) {
|
controls.get(valid_since).cloned()
|
||||||
if valid_since < secret.created_at {
|
|
||||||
return Some((
|
|
||||||
secret.value.clone(),
|
|
||||||
Self::check_ignore_cache(ignore_cache_since, secret.created_at),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_allowed_ips(
|
pub(crate) fn get_controls(&self, valid_since: Instant) -> Option<EndpointAccessControl> {
|
||||||
&self,
|
let controls = self.controls.as_ref()?;
|
||||||
valid_since: Instant,
|
controls.get(valid_since).cloned()
|
||||||
ignore_cache_since: Option<Instant>,
|
|
||||||
) -> Option<(Arc<Vec<IpPattern>>, bool)> {
|
|
||||||
if let Some(allowed_ips) = &self.allowed_ips {
|
|
||||||
if valid_since < allowed_ips.created_at {
|
|
||||||
return Some((
|
|
||||||
allowed_ips.value.clone(),
|
|
||||||
Self::check_ignore_cache(ignore_cache_since, allowed_ips.created_at),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
pub(crate) fn get_allowed_vpc_endpoint_ids(
|
|
||||||
&self,
|
|
||||||
valid_since: Instant,
|
|
||||||
ignore_cache_since: Option<Instant>,
|
|
||||||
) -> Option<(Arc<Vec<String>>, bool)> {
|
|
||||||
if let Some(allowed_vpc_endpoint_ids) = &self.allowed_vpc_endpoint_ids {
|
|
||||||
if valid_since < allowed_vpc_endpoint_ids.created_at {
|
|
||||||
return Some((
|
|
||||||
allowed_vpc_endpoint_ids.value.clone(),
|
|
||||||
Self::check_ignore_cache(
|
|
||||||
ignore_cache_since,
|
|
||||||
allowed_vpc_endpoint_ids.created_at,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
pub(crate) fn get_block_public_or_vpc_access(
|
|
||||||
&self,
|
|
||||||
valid_since: Instant,
|
|
||||||
ignore_cache_since: Option<Instant>,
|
|
||||||
) -> Option<(AccessBlockerFlags, bool)> {
|
|
||||||
if let Some(block_public_or_vpc_access) = &self.block_public_or_vpc_access {
|
|
||||||
if valid_since < block_public_or_vpc_access.created_at {
|
|
||||||
return Some((
|
|
||||||
block_public_or_vpc_access.value.clone(),
|
|
||||||
Self::check_ignore_cache(
|
|
||||||
ignore_cache_since,
|
|
||||||
block_public_or_vpc_access.created_at,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn invalidate_allowed_ips(&mut self) {
|
pub(crate) fn invalidate_endpoint(&mut self) {
|
||||||
self.allowed_ips = None;
|
self.controls = None;
|
||||||
}
|
|
||||||
pub(crate) fn invalidate_allowed_vpc_endpoint_ids(&mut self) {
|
|
||||||
self.allowed_vpc_endpoint_ids = None;
|
|
||||||
}
|
|
||||||
pub(crate) fn invalidate_block_public_or_vpc_access(&mut self) {
|
|
||||||
self.block_public_or_vpc_access = None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn invalidate_role_secret(&mut self, role_name: RoleNameInt) {
|
pub(crate) fn invalidate_role_secret(&mut self, role_name: RoleNameInt) {
|
||||||
self.secret.remove(&role_name);
|
self.role_controls.remove(&role_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,34 +101,29 @@ pub struct ProjectInfoCacheImpl {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ProjectInfoCache for ProjectInfoCacheImpl {
|
impl ProjectInfoCache for ProjectInfoCacheImpl {
|
||||||
fn invalidate_allowed_vpc_endpoint_ids_for_projects(&self, project_ids: Vec<ProjectIdInt>) {
|
fn invalidate_endpoint_access(&self, endpoint_id: EndpointIdInt) {
|
||||||
info!(
|
info!("invalidating endpoint access for `{endpoint_id}`");
|
||||||
"invalidating allowed vpc endpoint ids for projects `{}`",
|
if let Some(mut endpoint_info) = self.cache.get_mut(&endpoint_id) {
|
||||||
project_ids
|
endpoint_info.invalidate_endpoint();
|
||||||
.iter()
|
}
|
||||||
.map(|id| id.to_string())
|
}
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
fn invalidate_endpoint_access_for_project(&self, project_id: ProjectIdInt) {
|
||||||
);
|
info!("invalidating endpoint access for project `{project_id}`");
|
||||||
for project_id in project_ids {
|
let endpoints = self
|
||||||
let endpoints = self
|
.project2ep
|
||||||
.project2ep
|
.get(&project_id)
|
||||||
.get(&project_id)
|
.map(|kv| kv.value().clone())
|
||||||
.map(|kv| kv.value().clone())
|
.unwrap_or_default();
|
||||||
.unwrap_or_default();
|
for endpoint_id in endpoints {
|
||||||
for endpoint_id in endpoints {
|
if let Some(mut endpoint_info) = self.cache.get_mut(&endpoint_id) {
|
||||||
if let Some(mut endpoint_info) = self.cache.get_mut(&endpoint_id) {
|
endpoint_info.invalidate_endpoint();
|
||||||
endpoint_info.invalidate_allowed_vpc_endpoint_ids();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn invalidate_allowed_vpc_endpoint_ids_for_org(&self, account_id: AccountIdInt) {
|
fn invalidate_endpoint_access_for_org(&self, account_id: AccountIdInt) {
|
||||||
info!(
|
info!("invalidating endpoint access for org `{account_id}`");
|
||||||
"invalidating allowed vpc endpoint ids for org `{}`",
|
|
||||||
account_id
|
|
||||||
);
|
|
||||||
let endpoints = self
|
let endpoints = self
|
||||||
.account2ep
|
.account2ep
|
||||||
.get(&account_id)
|
.get(&account_id)
|
||||||
@@ -205,41 +131,11 @@ impl ProjectInfoCache for ProjectInfoCacheImpl {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
for endpoint_id in endpoints {
|
for endpoint_id in endpoints {
|
||||||
if let Some(mut endpoint_info) = self.cache.get_mut(&endpoint_id) {
|
if let Some(mut endpoint_info) = self.cache.get_mut(&endpoint_id) {
|
||||||
endpoint_info.invalidate_allowed_vpc_endpoint_ids();
|
endpoint_info.invalidate_endpoint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn invalidate_block_public_or_vpc_access_for_project(&self, project_id: ProjectIdInt) {
|
|
||||||
info!(
|
|
||||||
"invalidating block public or vpc access for project `{}`",
|
|
||||||
project_id
|
|
||||||
);
|
|
||||||
let endpoints = self
|
|
||||||
.project2ep
|
|
||||||
.get(&project_id)
|
|
||||||
.map(|kv| kv.value().clone())
|
|
||||||
.unwrap_or_default();
|
|
||||||
for endpoint_id in endpoints {
|
|
||||||
if let Some(mut endpoint_info) = self.cache.get_mut(&endpoint_id) {
|
|
||||||
endpoint_info.invalidate_block_public_or_vpc_access();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn invalidate_allowed_ips_for_project(&self, project_id: ProjectIdInt) {
|
|
||||||
info!("invalidating allowed ips for project `{}`", project_id);
|
|
||||||
let endpoints = self
|
|
||||||
.project2ep
|
|
||||||
.get(&project_id)
|
|
||||||
.map(|kv| kv.value().clone())
|
|
||||||
.unwrap_or_default();
|
|
||||||
for endpoint_id in endpoints {
|
|
||||||
if let Some(mut endpoint_info) = self.cache.get_mut(&endpoint_id) {
|
|
||||||
endpoint_info.invalidate_allowed_ips();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn invalidate_role_secret_for_project(&self, project_id: ProjectIdInt, role_name: RoleNameInt) {
|
fn invalidate_role_secret_for_project(&self, project_id: ProjectIdInt, role_name: RoleNameInt) {
|
||||||
info!(
|
info!(
|
||||||
"invalidating role secret for project_id `{}` and role_name `{}`",
|
"invalidating role secret for project_id `{}` and role_name `{}`",
|
||||||
@@ -256,6 +152,7 @@ impl ProjectInfoCache for ProjectInfoCacheImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn decrement_active_listeners(&self) {
|
async fn decrement_active_listeners(&self) {
|
||||||
let mut listeners_guard = self.active_listeners_lock.lock().await;
|
let mut listeners_guard = self.active_listeners_lock.lock().await;
|
||||||
if *listeners_guard == 0 {
|
if *listeners_guard == 0 {
|
||||||
@@ -293,155 +190,71 @@ impl ProjectInfoCacheImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_endpoint_cache(
|
||||||
|
&self,
|
||||||
|
endpoint_id: &EndpointId,
|
||||||
|
) -> Option<Ref<'_, EndpointIdInt, EndpointInfo>> {
|
||||||
|
let endpoint_id = EndpointIdInt::get(endpoint_id)?;
|
||||||
|
self.cache.get(&endpoint_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn get_role_secret(
|
pub(crate) fn get_role_secret(
|
||||||
&self,
|
&self,
|
||||||
endpoint_id: &EndpointId,
|
endpoint_id: &EndpointId,
|
||||||
role_name: &RoleName,
|
role_name: &RoleName,
|
||||||
) -> Option<Cached<&Self, Option<AuthSecret>>> {
|
) -> Option<RoleAccessControl> {
|
||||||
let endpoint_id = EndpointIdInt::get(endpoint_id)?;
|
let valid_since = self.get_cache_times();
|
||||||
let role_name = RoleNameInt::get(role_name)?;
|
let role_name = RoleNameInt::get(role_name)?;
|
||||||
let (valid_since, ignore_cache_since) = self.get_cache_times();
|
let endpoint_info = self.get_endpoint_cache(endpoint_id)?;
|
||||||
let endpoint_info = self.cache.get(&endpoint_id)?;
|
endpoint_info.get_role_secret(role_name, valid_since)
|
||||||
let (value, ignore_cache) =
|
|
||||||
endpoint_info.get_role_secret(role_name, valid_since, ignore_cache_since)?;
|
|
||||||
if !ignore_cache {
|
|
||||||
let cached = Cached {
|
|
||||||
token: Some((
|
|
||||||
self,
|
|
||||||
CachedLookupInfo::new_role_secret(endpoint_id, role_name),
|
|
||||||
)),
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
return Some(cached);
|
|
||||||
}
|
|
||||||
Some(Cached::new_uncached(value))
|
|
||||||
}
|
|
||||||
pub(crate) fn get_allowed_ips(
|
|
||||||
&self,
|
|
||||||
endpoint_id: &EndpointId,
|
|
||||||
) -> Option<Cached<&Self, Arc<Vec<IpPattern>>>> {
|
|
||||||
let endpoint_id = EndpointIdInt::get(endpoint_id)?;
|
|
||||||
let (valid_since, ignore_cache_since) = self.get_cache_times();
|
|
||||||
let endpoint_info = self.cache.get(&endpoint_id)?;
|
|
||||||
let value = endpoint_info.get_allowed_ips(valid_since, ignore_cache_since);
|
|
||||||
let (value, ignore_cache) = value?;
|
|
||||||
if !ignore_cache {
|
|
||||||
let cached = Cached {
|
|
||||||
token: Some((self, CachedLookupInfo::new_allowed_ips(endpoint_id))),
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
return Some(cached);
|
|
||||||
}
|
|
||||||
Some(Cached::new_uncached(value))
|
|
||||||
}
|
|
||||||
pub(crate) fn get_allowed_vpc_endpoint_ids(
|
|
||||||
&self,
|
|
||||||
endpoint_id: &EndpointId,
|
|
||||||
) -> Option<Cached<&Self, Arc<Vec<String>>>> {
|
|
||||||
let endpoint_id = EndpointIdInt::get(endpoint_id)?;
|
|
||||||
let (valid_since, ignore_cache_since) = self.get_cache_times();
|
|
||||||
let endpoint_info = self.cache.get(&endpoint_id)?;
|
|
||||||
let value = endpoint_info.get_allowed_vpc_endpoint_ids(valid_since, ignore_cache_since);
|
|
||||||
let (value, ignore_cache) = value?;
|
|
||||||
if !ignore_cache {
|
|
||||||
let cached = Cached {
|
|
||||||
token: Some((
|
|
||||||
self,
|
|
||||||
CachedLookupInfo::new_allowed_vpc_endpoint_ids(endpoint_id),
|
|
||||||
)),
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
return Some(cached);
|
|
||||||
}
|
|
||||||
Some(Cached::new_uncached(value))
|
|
||||||
}
|
|
||||||
pub(crate) fn get_block_public_or_vpc_access(
|
|
||||||
&self,
|
|
||||||
endpoint_id: &EndpointId,
|
|
||||||
) -> Option<Cached<&Self, AccessBlockerFlags>> {
|
|
||||||
let endpoint_id = EndpointIdInt::get(endpoint_id)?;
|
|
||||||
let (valid_since, ignore_cache_since) = self.get_cache_times();
|
|
||||||
let endpoint_info = self.cache.get(&endpoint_id)?;
|
|
||||||
let value = endpoint_info.get_block_public_or_vpc_access(valid_since, ignore_cache_since);
|
|
||||||
let (value, ignore_cache) = value?;
|
|
||||||
if !ignore_cache {
|
|
||||||
let cached = Cached {
|
|
||||||
token: Some((
|
|
||||||
self,
|
|
||||||
CachedLookupInfo::new_block_public_or_vpc_access(endpoint_id),
|
|
||||||
)),
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
return Some(cached);
|
|
||||||
}
|
|
||||||
Some(Cached::new_uncached(value))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn insert_role_secret(
|
pub(crate) fn get_endpoint_access(
|
||||||
&self,
|
&self,
|
||||||
project_id: ProjectIdInt,
|
endpoint_id: &EndpointId,
|
||||||
endpoint_id: EndpointIdInt,
|
) -> Option<EndpointAccessControl> {
|
||||||
role_name: RoleNameInt,
|
let valid_since = self.get_cache_times();
|
||||||
secret: Option<AuthSecret>,
|
let endpoint_info = self.get_endpoint_cache(endpoint_id)?;
|
||||||
) {
|
endpoint_info.get_controls(valid_since)
|
||||||
if self.cache.len() >= self.config.size {
|
|
||||||
// If there are too many entries, wait until the next gc cycle.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.insert_project2endpoint(project_id, endpoint_id);
|
|
||||||
let mut entry = self.cache.entry(endpoint_id).or_default();
|
|
||||||
if entry.secret.len() < self.config.max_roles {
|
|
||||||
entry.secret.insert(role_name, secret.into());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pub(crate) fn insert_allowed_ips(
|
|
||||||
&self,
|
pub(crate) fn insert_endpoint_access(
|
||||||
project_id: ProjectIdInt,
|
|
||||||
endpoint_id: EndpointIdInt,
|
|
||||||
allowed_ips: Arc<Vec<IpPattern>>,
|
|
||||||
) {
|
|
||||||
if self.cache.len() >= self.config.size {
|
|
||||||
// If there are too many entries, wait until the next gc cycle.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.insert_project2endpoint(project_id, endpoint_id);
|
|
||||||
self.cache.entry(endpoint_id).or_default().allowed_ips = Some(allowed_ips.into());
|
|
||||||
}
|
|
||||||
pub(crate) fn insert_allowed_vpc_endpoint_ids(
|
|
||||||
&self,
|
&self,
|
||||||
account_id: Option<AccountIdInt>,
|
account_id: Option<AccountIdInt>,
|
||||||
project_id: ProjectIdInt,
|
project_id: ProjectIdInt,
|
||||||
endpoint_id: EndpointIdInt,
|
endpoint_id: EndpointIdInt,
|
||||||
allowed_vpc_endpoint_ids: Arc<Vec<String>>,
|
role_name: RoleNameInt,
|
||||||
|
controls: EndpointAccessControl,
|
||||||
|
role_controls: RoleAccessControl,
|
||||||
) {
|
) {
|
||||||
if self.cache.len() >= self.config.size {
|
|
||||||
// If there are too many entries, wait until the next gc cycle.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Some(account_id) = account_id {
|
if let Some(account_id) = account_id {
|
||||||
self.insert_account2endpoint(account_id, endpoint_id);
|
self.insert_account2endpoint(account_id, endpoint_id);
|
||||||
}
|
}
|
||||||
self.insert_project2endpoint(project_id, endpoint_id);
|
self.insert_project2endpoint(project_id, endpoint_id);
|
||||||
self.cache
|
|
||||||
.entry(endpoint_id)
|
|
||||||
.or_default()
|
|
||||||
.allowed_vpc_endpoint_ids = Some(allowed_vpc_endpoint_ids.into());
|
|
||||||
}
|
|
||||||
pub(crate) fn insert_block_public_or_vpc_access(
|
|
||||||
&self,
|
|
||||||
project_id: ProjectIdInt,
|
|
||||||
endpoint_id: EndpointIdInt,
|
|
||||||
access_blockers: AccessBlockerFlags,
|
|
||||||
) {
|
|
||||||
if self.cache.len() >= self.config.size {
|
if self.cache.len() >= self.config.size {
|
||||||
// If there are too many entries, wait until the next gc cycle.
|
// If there are too many entries, wait until the next gc cycle.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.insert_project2endpoint(project_id, endpoint_id);
|
|
||||||
self.cache
|
let controls = Entry::from(controls);
|
||||||
.entry(endpoint_id)
|
let role_controls = Entry::from(role_controls);
|
||||||
.or_default()
|
|
||||||
.block_public_or_vpc_access = Some(access_blockers.into());
|
match self.cache.entry(endpoint_id) {
|
||||||
|
clashmap::Entry::Vacant(e) => {
|
||||||
|
e.insert(EndpointInfo {
|
||||||
|
role_controls: HashMap::from_iter([(role_name, role_controls)]),
|
||||||
|
controls: Some(controls),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
clashmap::Entry::Occupied(mut e) => {
|
||||||
|
let ep = e.get_mut();
|
||||||
|
ep.controls = Some(controls);
|
||||||
|
if ep.role_controls.len() < self.config.max_roles {
|
||||||
|
ep.role_controls.insert(role_name, role_controls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_project2endpoint(&self, project_id: ProjectIdInt, endpoint_id: EndpointIdInt) {
|
fn insert_project2endpoint(&self, project_id: ProjectIdInt, endpoint_id: EndpointIdInt) {
|
||||||
@@ -452,6 +265,7 @@ impl ProjectInfoCacheImpl {
|
|||||||
.insert(project_id, HashSet::from([endpoint_id]));
|
.insert(project_id, HashSet::from([endpoint_id]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_account2endpoint(&self, account_id: AccountIdInt, endpoint_id: EndpointIdInt) {
|
fn insert_account2endpoint(&self, account_id: AccountIdInt, endpoint_id: EndpointIdInt) {
|
||||||
if let Some(mut endpoints) = self.account2ep.get_mut(&account_id) {
|
if let Some(mut endpoints) = self.account2ep.get_mut(&account_id) {
|
||||||
endpoints.insert(endpoint_id);
|
endpoints.insert(endpoint_id);
|
||||||
@@ -460,21 +274,57 @@ impl ProjectInfoCacheImpl {
|
|||||||
.insert(account_id, HashSet::from([endpoint_id]));
|
.insert(account_id, HashSet::from([endpoint_id]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn get_cache_times(&self) -> (Instant, Option<Instant>) {
|
|
||||||
let mut valid_since = Instant::now() - self.config.ttl;
|
fn ignore_ttl_since(&self) -> Option<Instant> {
|
||||||
// Only ignore cache if ttl is disabled.
|
|
||||||
let ttl_disabled_since_us = self
|
let ttl_disabled_since_us = self
|
||||||
.ttl_disabled_since_us
|
.ttl_disabled_since_us
|
||||||
.load(std::sync::atomic::Ordering::Relaxed);
|
.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
let ignore_cache_since = if ttl_disabled_since_us == u64::MAX {
|
|
||||||
None
|
if ttl_disabled_since_us == u64::MAX {
|
||||||
} else {
|
return None;
|
||||||
let ignore_cache_since = self.start_time + Duration::from_micros(ttl_disabled_since_us);
|
}
|
||||||
|
|
||||||
|
Some(self.start_time + Duration::from_micros(ttl_disabled_since_us))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cache_times(&self) -> Instant {
|
||||||
|
let mut valid_since = Instant::now() - self.config.ttl;
|
||||||
|
if let Some(ignore_ttl_since) = self.ignore_ttl_since() {
|
||||||
// We are fine if entry is not older than ttl or was added before we are getting notifications.
|
// We are fine if entry is not older than ttl or was added before we are getting notifications.
|
||||||
valid_since = valid_since.min(ignore_cache_since);
|
valid_since = valid_since.min(ignore_ttl_since);
|
||||||
Some(ignore_cache_since)
|
}
|
||||||
|
valid_since
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn maybe_invalidate_role_secret(&self, endpoint_id: &EndpointId, role_name: &RoleName) {
|
||||||
|
let Some(endpoint_id) = EndpointIdInt::get(endpoint_id) else {
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
(valid_since, ignore_cache_since)
|
let Some(role_name) = RoleNameInt::get(role_name) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(mut endpoint_info) = self.cache.get_mut(&endpoint_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let entry = endpoint_info.role_controls.entry(role_name);
|
||||||
|
let hash_map::Entry::Occupied(role_controls) = entry else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let created_at = role_controls.get().created_at;
|
||||||
|
let expire = match self.ignore_ttl_since() {
|
||||||
|
// if ignoring TTL, we should still try and roll the password if it's old
|
||||||
|
// and we the client gave an incorrect password. There could be some lag on the redis channel.
|
||||||
|
Some(_) => created_at + self.config.ttl < Instant::now(),
|
||||||
|
// edge case: redis is down, let's be generous and invalidate the cache immediately.
|
||||||
|
None => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if expire {
|
||||||
|
role_controls.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn gc_worker(&self) -> anyhow::Result<Infallible> {
|
pub async fn gc_worker(&self) -> anyhow::Result<Infallible> {
|
||||||
@@ -509,84 +359,12 @@ impl ProjectInfoCacheImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lookup info for project info cache.
|
|
||||||
/// This is used to invalidate cache entries.
|
|
||||||
pub(crate) struct CachedLookupInfo {
|
|
||||||
/// Search by this key.
|
|
||||||
endpoint_id: EndpointIdInt,
|
|
||||||
lookup_type: LookupType,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CachedLookupInfo {
|
|
||||||
pub(self) fn new_role_secret(endpoint_id: EndpointIdInt, role_name: RoleNameInt) -> Self {
|
|
||||||
Self {
|
|
||||||
endpoint_id,
|
|
||||||
lookup_type: LookupType::RoleSecret(role_name),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub(self) fn new_allowed_ips(endpoint_id: EndpointIdInt) -> Self {
|
|
||||||
Self {
|
|
||||||
endpoint_id,
|
|
||||||
lookup_type: LookupType::AllowedIps,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub(self) fn new_allowed_vpc_endpoint_ids(endpoint_id: EndpointIdInt) -> Self {
|
|
||||||
Self {
|
|
||||||
endpoint_id,
|
|
||||||
lookup_type: LookupType::AllowedVpcEndpointIds,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub(self) fn new_block_public_or_vpc_access(endpoint_id: EndpointIdInt) -> Self {
|
|
||||||
Self {
|
|
||||||
endpoint_id,
|
|
||||||
lookup_type: LookupType::BlockPublicOrVpcAccess,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum LookupType {
|
|
||||||
RoleSecret(RoleNameInt),
|
|
||||||
AllowedIps,
|
|
||||||
AllowedVpcEndpointIds,
|
|
||||||
BlockPublicOrVpcAccess,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cache for ProjectInfoCacheImpl {
|
|
||||||
type Key = SmolStr;
|
|
||||||
// Value is not really used here, but we need to specify it.
|
|
||||||
type Value = SmolStr;
|
|
||||||
|
|
||||||
type LookupInfo<Key> = CachedLookupInfo;
|
|
||||||
|
|
||||||
fn invalidate(&self, key: &Self::LookupInfo<SmolStr>) {
|
|
||||||
match &key.lookup_type {
|
|
||||||
LookupType::RoleSecret(role_name) => {
|
|
||||||
if let Some(mut endpoint_info) = self.cache.get_mut(&key.endpoint_id) {
|
|
||||||
endpoint_info.invalidate_role_secret(*role_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LookupType::AllowedIps => {
|
|
||||||
if let Some(mut endpoint_info) = self.cache.get_mut(&key.endpoint_id) {
|
|
||||||
endpoint_info.invalidate_allowed_ips();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LookupType::AllowedVpcEndpointIds => {
|
|
||||||
if let Some(mut endpoint_info) = self.cache.get_mut(&key.endpoint_id) {
|
|
||||||
endpoint_info.invalidate_allowed_vpc_endpoint_ids();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LookupType::BlockPublicOrVpcAccess => {
|
|
||||||
if let Some(mut endpoint_info) = self.cache.get_mut(&key.endpoint_id) {
|
|
||||||
endpoint_info.invalidate_block_public_or_vpc_access();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::control_plane::{AccessBlockerFlags, AuthSecret};
|
||||||
use crate::scram::ServerSecret;
|
use crate::scram::ServerSecret;
|
||||||
use crate::types::ProjectId;
|
use crate::types::ProjectId;
|
||||||
|
|
||||||
@@ -601,6 +379,8 @@ mod tests {
|
|||||||
});
|
});
|
||||||
let project_id: ProjectId = "project".into();
|
let project_id: ProjectId = "project".into();
|
||||||
let endpoint_id: EndpointId = "endpoint".into();
|
let endpoint_id: EndpointId = "endpoint".into();
|
||||||
|
let account_id: Option<AccountIdInt> = None;
|
||||||
|
|
||||||
let user1: RoleName = "user1".into();
|
let user1: RoleName = "user1".into();
|
||||||
let user2: RoleName = "user2".into();
|
let user2: RoleName = "user2".into();
|
||||||
let secret1 = Some(AuthSecret::Scram(ServerSecret::mock([1; 32])));
|
let secret1 = Some(AuthSecret::Scram(ServerSecret::mock([1; 32])));
|
||||||
@@ -609,183 +389,73 @@ mod tests {
|
|||||||
"127.0.0.1".parse().unwrap(),
|
"127.0.0.1".parse().unwrap(),
|
||||||
"127.0.0.2".parse().unwrap(),
|
"127.0.0.2".parse().unwrap(),
|
||||||
]);
|
]);
|
||||||
cache.insert_role_secret(
|
|
||||||
|
cache.insert_endpoint_access(
|
||||||
|
account_id,
|
||||||
(&project_id).into(),
|
(&project_id).into(),
|
||||||
(&endpoint_id).into(),
|
(&endpoint_id).into(),
|
||||||
(&user1).into(),
|
(&user1).into(),
|
||||||
secret1.clone(),
|
EndpointAccessControl {
|
||||||
|
allowed_ips: allowed_ips.clone(),
|
||||||
|
allowed_vpce: Arc::new(vec![]),
|
||||||
|
flags: AccessBlockerFlags::default(),
|
||||||
|
},
|
||||||
|
RoleAccessControl {
|
||||||
|
secret: secret1.clone(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
cache.insert_role_secret(
|
|
||||||
|
cache.insert_endpoint_access(
|
||||||
|
account_id,
|
||||||
(&project_id).into(),
|
(&project_id).into(),
|
||||||
(&endpoint_id).into(),
|
(&endpoint_id).into(),
|
||||||
(&user2).into(),
|
(&user2).into(),
|
||||||
secret2.clone(),
|
EndpointAccessControl {
|
||||||
);
|
allowed_ips: allowed_ips.clone(),
|
||||||
cache.insert_allowed_ips(
|
allowed_vpce: Arc::new(vec![]),
|
||||||
(&project_id).into(),
|
flags: AccessBlockerFlags::default(),
|
||||||
(&endpoint_id).into(),
|
},
|
||||||
allowed_ips.clone(),
|
RoleAccessControl {
|
||||||
|
secret: secret2.clone(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let cached = cache.get_role_secret(&endpoint_id, &user1).unwrap();
|
let cached = cache.get_role_secret(&endpoint_id, &user1).unwrap();
|
||||||
assert!(cached.cached());
|
assert_eq!(cached.secret, secret1);
|
||||||
assert_eq!(cached.value, secret1);
|
|
||||||
let cached = cache.get_role_secret(&endpoint_id, &user2).unwrap();
|
let cached = cache.get_role_secret(&endpoint_id, &user2).unwrap();
|
||||||
assert!(cached.cached());
|
assert_eq!(cached.secret, secret2);
|
||||||
assert_eq!(cached.value, secret2);
|
|
||||||
|
|
||||||
// Shouldn't add more than 2 roles.
|
// Shouldn't add more than 2 roles.
|
||||||
let user3: RoleName = "user3".into();
|
let user3: RoleName = "user3".into();
|
||||||
let secret3 = Some(AuthSecret::Scram(ServerSecret::mock([3; 32])));
|
let secret3 = Some(AuthSecret::Scram(ServerSecret::mock([3; 32])));
|
||||||
cache.insert_role_secret(
|
|
||||||
|
cache.insert_endpoint_access(
|
||||||
|
account_id,
|
||||||
(&project_id).into(),
|
(&project_id).into(),
|
||||||
(&endpoint_id).into(),
|
(&endpoint_id).into(),
|
||||||
(&user3).into(),
|
(&user3).into(),
|
||||||
secret3.clone(),
|
EndpointAccessControl {
|
||||||
|
allowed_ips: allowed_ips.clone(),
|
||||||
|
allowed_vpce: Arc::new(vec![]),
|
||||||
|
flags: AccessBlockerFlags::default(),
|
||||||
|
},
|
||||||
|
RoleAccessControl {
|
||||||
|
secret: secret3.clone(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(cache.get_role_secret(&endpoint_id, &user3).is_none());
|
assert!(cache.get_role_secret(&endpoint_id, &user3).is_none());
|
||||||
|
|
||||||
let cached = cache.get_allowed_ips(&endpoint_id).unwrap();
|
let cached = cache.get_endpoint_access(&endpoint_id).unwrap();
|
||||||
assert!(cached.cached());
|
assert_eq!(cached.allowed_ips, allowed_ips);
|
||||||
assert_eq!(cached.value, allowed_ips);
|
|
||||||
|
|
||||||
tokio::time::advance(Duration::from_secs(2)).await;
|
tokio::time::advance(Duration::from_secs(2)).await;
|
||||||
let cached = cache.get_role_secret(&endpoint_id, &user1);
|
let cached = cache.get_role_secret(&endpoint_id, &user1);
|
||||||
assert!(cached.is_none());
|
assert!(cached.is_none());
|
||||||
let cached = cache.get_role_secret(&endpoint_id, &user2);
|
let cached = cache.get_role_secret(&endpoint_id, &user2);
|
||||||
assert!(cached.is_none());
|
assert!(cached.is_none());
|
||||||
let cached = cache.get_allowed_ips(&endpoint_id);
|
let cached = cache.get_endpoint_access(&endpoint_id);
|
||||||
assert!(cached.is_none());
|
assert!(cached.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_project_info_cache_invalidations() {
|
|
||||||
tokio::time::pause();
|
|
||||||
let cache = Arc::new(ProjectInfoCacheImpl::new(ProjectInfoCacheOptions {
|
|
||||||
size: 2,
|
|
||||||
max_roles: 2,
|
|
||||||
ttl: Duration::from_secs(1),
|
|
||||||
gc_interval: Duration::from_secs(600),
|
|
||||||
}));
|
|
||||||
cache.clone().increment_active_listeners().await;
|
|
||||||
tokio::time::advance(Duration::from_secs(2)).await;
|
|
||||||
|
|
||||||
let project_id: ProjectId = "project".into();
|
|
||||||
let endpoint_id: EndpointId = "endpoint".into();
|
|
||||||
let user1: RoleName = "user1".into();
|
|
||||||
let user2: RoleName = "user2".into();
|
|
||||||
let secret1 = Some(AuthSecret::Scram(ServerSecret::mock([1; 32])));
|
|
||||||
let secret2 = Some(AuthSecret::Scram(ServerSecret::mock([2; 32])));
|
|
||||||
let allowed_ips = Arc::new(vec![
|
|
||||||
"127.0.0.1".parse().unwrap(),
|
|
||||||
"127.0.0.2".parse().unwrap(),
|
|
||||||
]);
|
|
||||||
cache.insert_role_secret(
|
|
||||||
(&project_id).into(),
|
|
||||||
(&endpoint_id).into(),
|
|
||||||
(&user1).into(),
|
|
||||||
secret1.clone(),
|
|
||||||
);
|
|
||||||
cache.insert_role_secret(
|
|
||||||
(&project_id).into(),
|
|
||||||
(&endpoint_id).into(),
|
|
||||||
(&user2).into(),
|
|
||||||
secret2.clone(),
|
|
||||||
);
|
|
||||||
cache.insert_allowed_ips(
|
|
||||||
(&project_id).into(),
|
|
||||||
(&endpoint_id).into(),
|
|
||||||
allowed_ips.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
tokio::time::advance(Duration::from_secs(2)).await;
|
|
||||||
// Nothing should be invalidated.
|
|
||||||
|
|
||||||
let cached = cache.get_role_secret(&endpoint_id, &user1).unwrap();
|
|
||||||
// TTL is disabled, so it should be impossible to invalidate this value.
|
|
||||||
assert!(!cached.cached());
|
|
||||||
assert_eq!(cached.value, secret1);
|
|
||||||
|
|
||||||
cached.invalidate(); // Shouldn't do anything.
|
|
||||||
let cached = cache.get_role_secret(&endpoint_id, &user1).unwrap();
|
|
||||||
assert_eq!(cached.value, secret1);
|
|
||||||
|
|
||||||
let cached = cache.get_role_secret(&endpoint_id, &user2).unwrap();
|
|
||||||
assert!(!cached.cached());
|
|
||||||
assert_eq!(cached.value, secret2);
|
|
||||||
|
|
||||||
// The only way to invalidate this value is to invalidate via the api.
|
|
||||||
cache.invalidate_role_secret_for_project((&project_id).into(), (&user2).into());
|
|
||||||
assert!(cache.get_role_secret(&endpoint_id, &user2).is_none());
|
|
||||||
|
|
||||||
let cached = cache.get_allowed_ips(&endpoint_id).unwrap();
|
|
||||||
assert!(!cached.cached());
|
|
||||||
assert_eq!(cached.value, allowed_ips);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_increment_active_listeners_invalidate_added_before() {
|
|
||||||
tokio::time::pause();
|
|
||||||
let cache = Arc::new(ProjectInfoCacheImpl::new(ProjectInfoCacheOptions {
|
|
||||||
size: 2,
|
|
||||||
max_roles: 2,
|
|
||||||
ttl: Duration::from_secs(1),
|
|
||||||
gc_interval: Duration::from_secs(600),
|
|
||||||
}));
|
|
||||||
|
|
||||||
let project_id: ProjectId = "project".into();
|
|
||||||
let endpoint_id: EndpointId = "endpoint".into();
|
|
||||||
let user1: RoleName = "user1".into();
|
|
||||||
let user2: RoleName = "user2".into();
|
|
||||||
let secret1 = Some(AuthSecret::Scram(ServerSecret::mock([1; 32])));
|
|
||||||
let secret2 = Some(AuthSecret::Scram(ServerSecret::mock([2; 32])));
|
|
||||||
let allowed_ips = Arc::new(vec![
|
|
||||||
"127.0.0.1".parse().unwrap(),
|
|
||||||
"127.0.0.2".parse().unwrap(),
|
|
||||||
]);
|
|
||||||
cache.insert_role_secret(
|
|
||||||
(&project_id).into(),
|
|
||||||
(&endpoint_id).into(),
|
|
||||||
(&user1).into(),
|
|
||||||
secret1.clone(),
|
|
||||||
);
|
|
||||||
cache.clone().increment_active_listeners().await;
|
|
||||||
tokio::time::advance(Duration::from_millis(100)).await;
|
|
||||||
cache.insert_role_secret(
|
|
||||||
(&project_id).into(),
|
|
||||||
(&endpoint_id).into(),
|
|
||||||
(&user2).into(),
|
|
||||||
secret2.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Added before ttl was disabled + ttl should be still cached.
|
|
||||||
let cached = cache.get_role_secret(&endpoint_id, &user1).unwrap();
|
|
||||||
assert!(cached.cached());
|
|
||||||
let cached = cache.get_role_secret(&endpoint_id, &user2).unwrap();
|
|
||||||
assert!(cached.cached());
|
|
||||||
|
|
||||||
tokio::time::advance(Duration::from_secs(1)).await;
|
|
||||||
// Added before ttl was disabled + ttl should expire.
|
|
||||||
assert!(cache.get_role_secret(&endpoint_id, &user1).is_none());
|
|
||||||
assert!(cache.get_role_secret(&endpoint_id, &user2).is_none());
|
|
||||||
|
|
||||||
// Added after ttl was disabled + ttl should not be cached.
|
|
||||||
cache.insert_allowed_ips(
|
|
||||||
(&project_id).into(),
|
|
||||||
(&endpoint_id).into(),
|
|
||||||
allowed_ips.clone(),
|
|
||||||
);
|
|
||||||
let cached = cache.get_allowed_ips(&endpoint_id).unwrap();
|
|
||||||
assert!(!cached.cached());
|
|
||||||
|
|
||||||
tokio::time::advance(Duration::from_secs(1)).await;
|
|
||||||
// Added before ttl was disabled + ttl still should expire.
|
|
||||||
assert!(cache.get_role_secret(&endpoint_id, &user1).is_none());
|
|
||||||
assert!(cache.get_role_secret(&endpoint_id, &user2).is_none());
|
|
||||||
// Shouldn't be invalidated.
|
|
||||||
|
|
||||||
let cached = cache.get_allowed_ips(&endpoint_id).unwrap();
|
|
||||||
assert!(!cached.cached());
|
|
||||||
assert_eq!(cached.value, allowed_ips);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ use tokio::net::TcpStream;
|
|||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
|
use crate::auth::AuthError;
|
||||||
use crate::auth::backend::ComputeUserInfo;
|
use crate::auth::backend::ComputeUserInfo;
|
||||||
use crate::auth::{AuthError, check_peer_addr_is_in_list};
|
|
||||||
use crate::config::ComputeConfig;
|
use crate::config::ComputeConfig;
|
||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::control_plane::ControlPlaneApi;
|
use crate::control_plane::ControlPlaneApi;
|
||||||
@@ -21,11 +21,9 @@ use crate::error::ReportableError;
|
|||||||
use crate::ext::LockExt;
|
use crate::ext::LockExt;
|
||||||
use crate::metrics::{CancelChannelSizeGuard, CancellationRequest, Metrics, RedisMsgKind};
|
use crate::metrics::{CancelChannelSizeGuard, CancellationRequest, Metrics, RedisMsgKind};
|
||||||
use crate::pqproto::CancelKeyData;
|
use crate::pqproto::CancelKeyData;
|
||||||
use crate::protocol2::ConnectionInfoExtra;
|
|
||||||
use crate::rate_limiter::LeakyBucketRateLimiter;
|
use crate::rate_limiter::LeakyBucketRateLimiter;
|
||||||
use crate::redis::keys::KeyPrefix;
|
use crate::redis::keys::KeyPrefix;
|
||||||
use crate::redis::kv_ops::RedisKVClient;
|
use crate::redis::kv_ops::RedisKVClient;
|
||||||
use crate::tls::postgres_rustls::MakeRustlsConnect;
|
|
||||||
|
|
||||||
type IpSubnetKey = IpNet;
|
type IpSubnetKey = IpNet;
|
||||||
|
|
||||||
@@ -272,13 +270,7 @@ pub(crate) enum CancelError {
|
|||||||
#[error("rate limit exceeded")]
|
#[error("rate limit exceeded")]
|
||||||
RateLimit,
|
RateLimit,
|
||||||
|
|
||||||
#[error("IP is not allowed")]
|
#[error("Authentication error")]
|
||||||
IpNotAllowed,
|
|
||||||
|
|
||||||
#[error("VPC endpoint id is not allowed to connect")]
|
|
||||||
VpcEndpointIdNotAllowed,
|
|
||||||
|
|
||||||
#[error("Authentication backend error")]
|
|
||||||
AuthError(#[from] AuthError),
|
AuthError(#[from] AuthError),
|
||||||
|
|
||||||
#[error("key not found")]
|
#[error("key not found")]
|
||||||
@@ -297,10 +289,7 @@ impl ReportableError for CancelError {
|
|||||||
}
|
}
|
||||||
CancelError::Postgres(_) => crate::error::ErrorKind::Compute,
|
CancelError::Postgres(_) => crate::error::ErrorKind::Compute,
|
||||||
CancelError::RateLimit => crate::error::ErrorKind::RateLimit,
|
CancelError::RateLimit => crate::error::ErrorKind::RateLimit,
|
||||||
CancelError::IpNotAllowed
|
CancelError::NotFound | CancelError::AuthError(_) => crate::error::ErrorKind::User,
|
||||||
| CancelError::VpcEndpointIdNotAllowed
|
|
||||||
| CancelError::NotFound => crate::error::ErrorKind::User,
|
|
||||||
CancelError::AuthError(_) => crate::error::ErrorKind::ControlPlane,
|
|
||||||
CancelError::InternalError => crate::error::ErrorKind::Service,
|
CancelError::InternalError => crate::error::ErrorKind::Service,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,7 +411,13 @@ impl CancellationHandler {
|
|||||||
IpAddr::V4(ip) => IpNet::V4(Ipv4Net::new_assert(ip, 24).trunc()), // use defaut mask here
|
IpAddr::V4(ip) => IpNet::V4(Ipv4Net::new_assert(ip, 24).trunc()), // use defaut mask here
|
||||||
IpAddr::V6(ip) => IpNet::V6(Ipv6Net::new_assert(ip, 64).trunc()),
|
IpAddr::V6(ip) => IpNet::V6(Ipv6Net::new_assert(ip, 64).trunc()),
|
||||||
};
|
};
|
||||||
if !self.limiter.lock_propagate_poison().check(subnet_key, 1) {
|
|
||||||
|
let allowed = {
|
||||||
|
let rate_limit_config = None;
|
||||||
|
let limiter = self.limiter.lock_propagate_poison();
|
||||||
|
limiter.check(subnet_key, rate_limit_config, 1)
|
||||||
|
};
|
||||||
|
if !allowed {
|
||||||
// log only the subnet part of the IP address to know which subnet is rate limited
|
// log only the subnet part of the IP address to know which subnet is rate limited
|
||||||
tracing::warn!("Rate limit exceeded. Skipping cancellation message, {subnet_key}");
|
tracing::warn!("Rate limit exceeded. Skipping cancellation message, {subnet_key}");
|
||||||
Metrics::get()
|
Metrics::get()
|
||||||
@@ -450,52 +445,13 @@ impl CancellationHandler {
|
|||||||
return Err(CancelError::NotFound);
|
return Err(CancelError::NotFound);
|
||||||
};
|
};
|
||||||
|
|
||||||
if check_ip_allowed {
|
let info = &cancel_closure.user_info;
|
||||||
let ip_allowlist = auth_backend
|
let access_controls = auth_backend
|
||||||
.get_allowed_ips(&ctx, &cancel_closure.user_info)
|
.get_endpoint_access_control(&ctx, &info.endpoint, &info.user)
|
||||||
.await
|
|
||||||
.map_err(|e| CancelError::AuthError(e.into()))?;
|
|
||||||
|
|
||||||
if !check_peer_addr_is_in_list(&ctx.peer_addr(), &ip_allowlist) {
|
|
||||||
// log it here since cancel_session could be spawned in a task
|
|
||||||
tracing::warn!(
|
|
||||||
"IP is not allowed to cancel the query: {key}, address: {}",
|
|
||||||
ctx.peer_addr()
|
|
||||||
);
|
|
||||||
return Err(CancelError::IpNotAllowed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if a VPC endpoint ID is coming in and if yes, if it's allowed
|
|
||||||
let access_blocks = auth_backend
|
|
||||||
.get_block_public_or_vpc_access(&ctx, &cancel_closure.user_info)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| CancelError::AuthError(e.into()))?;
|
.map_err(|e| CancelError::AuthError(e.into()))?;
|
||||||
|
|
||||||
if check_vpc_allowed {
|
access_controls.check(&ctx, check_ip_allowed, check_vpc_allowed)?;
|
||||||
if access_blocks.vpc_access_blocked {
|
|
||||||
return Err(CancelError::AuthError(AuthError::NetworkNotAllowed));
|
|
||||||
}
|
|
||||||
|
|
||||||
let incoming_vpc_endpoint_id = match ctx.extra() {
|
|
||||||
None => return Err(CancelError::AuthError(AuthError::MissingVPCEndpointId)),
|
|
||||||
Some(ConnectionInfoExtra::Aws { vpce_id }) => vpce_id.to_string(),
|
|
||||||
Some(ConnectionInfoExtra::Azure { link_id }) => link_id.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let allowed_vpc_endpoint_ids = auth_backend
|
|
||||||
.get_allowed_vpc_endpoint_ids(&ctx, &cancel_closure.user_info)
|
|
||||||
.await
|
|
||||||
.map_err(|e| CancelError::AuthError(e.into()))?;
|
|
||||||
// TODO: For now an empty VPC endpoint ID list means all are allowed. We should replace that.
|
|
||||||
if !allowed_vpc_endpoint_ids.is_empty()
|
|
||||||
&& !allowed_vpc_endpoint_ids.contains(&incoming_vpc_endpoint_id)
|
|
||||||
{
|
|
||||||
return Err(CancelError::VpcEndpointIdNotAllowed);
|
|
||||||
}
|
|
||||||
} else if access_blocks.public_access_blocked {
|
|
||||||
return Err(CancelError::VpcEndpointIdNotAllowed);
|
|
||||||
}
|
|
||||||
|
|
||||||
Metrics::get()
|
Metrics::get()
|
||||||
.proxy
|
.proxy
|
||||||
@@ -540,10 +496,8 @@ impl CancelClosure {
|
|||||||
) -> Result<(), CancelError> {
|
) -> Result<(), CancelError> {
|
||||||
let socket = TcpStream::connect(self.socket_addr).await?;
|
let socket = TcpStream::connect(self.socket_addr).await?;
|
||||||
|
|
||||||
let mut mk_tls =
|
let tls = <_ as MakeTlsConnect<tokio::net::TcpStream>>::make_tls_connect(
|
||||||
crate::tls::postgres_rustls::MakeRustlsConnect::new(compute_config.tls.clone());
|
compute_config,
|
||||||
let tls = <MakeRustlsConnect as MakeTlsConnect<tokio::net::TcpStream>>::make_tls_connect(
|
|
||||||
&mut mk_tls,
|
|
||||||
&self.hostname,
|
&self.hostname,
|
||||||
)
|
)
|
||||||
.map_err(|e| CancelError::IO(std::io::Error::other(e.to_string())))?;
|
.map_err(|e| CancelError::IO(std::io::Error::other(e.to_string())))?;
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
|
mod tls;
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::net::SocketAddr;
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use futures::{FutureExt, TryFutureExt};
|
use futures::{FutureExt, TryFutureExt};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use postgres_client::config::{AuthKeys, SslMode};
|
||||||
|
use postgres_client::maybe_tls_stream::MaybeTlsStream;
|
||||||
use postgres_client::tls::MakeTlsConnect;
|
use postgres_client::tls::MakeTlsConnect;
|
||||||
use postgres_client::{CancelToken, RawConnection};
|
use postgres_client::{CancelToken, NoTls, RawConnection};
|
||||||
use postgres_protocol::message::backend::NoticeResponseBody;
|
use postgres_protocol::message::backend::NoticeResponseBody;
|
||||||
use rustls::pki_types::InvalidDnsNameError;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::net::{TcpStream, lookup_host};
|
use tokio::net::{TcpStream, lookup_host};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
use crate::auth::backend::ComputeUserInfo;
|
use crate::auth::backend::{ComputeCredentialKeys, ComputeUserInfo};
|
||||||
use crate::auth::parse_endpoint_param;
|
use crate::auth::parse_endpoint_param;
|
||||||
use crate::cancellation::CancelClosure;
|
use crate::cancellation::CancelClosure;
|
||||||
|
use crate::compute::tls::TlsError;
|
||||||
use crate::config::ComputeConfig;
|
use crate::config::ComputeConfig;
|
||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::control_plane::client::ApiLockError;
|
use crate::control_plane::client::ApiLockError;
|
||||||
@@ -25,7 +28,6 @@ use crate::error::{ReportableError, UserFacingError};
|
|||||||
use crate::metrics::{Metrics, NumDbConnectionsGuard};
|
use crate::metrics::{Metrics, NumDbConnectionsGuard};
|
||||||
use crate::pqproto::StartupMessageParams;
|
use crate::pqproto::StartupMessageParams;
|
||||||
use crate::proxy::neon_option;
|
use crate::proxy::neon_option;
|
||||||
use crate::tls::postgres_rustls::MakeRustlsConnect;
|
|
||||||
use crate::types::Host;
|
use crate::types::Host;
|
||||||
|
|
||||||
pub const COULD_NOT_CONNECT: &str = "Couldn't connect to compute node";
|
pub const COULD_NOT_CONNECT: &str = "Couldn't connect to compute node";
|
||||||
@@ -38,10 +40,7 @@ pub(crate) enum ConnectionError {
|
|||||||
Postgres(#[from] postgres_client::Error),
|
Postgres(#[from] postgres_client::Error),
|
||||||
|
|
||||||
#[error("{COULD_NOT_CONNECT}: {0}")]
|
#[error("{COULD_NOT_CONNECT}: {0}")]
|
||||||
CouldNotConnect(#[from] io::Error),
|
TlsError(#[from] TlsError),
|
||||||
|
|
||||||
#[error("{COULD_NOT_CONNECT}: {0}")]
|
|
||||||
TlsError(#[from] InvalidDnsNameError),
|
|
||||||
|
|
||||||
#[error("{COULD_NOT_CONNECT}: {0}")]
|
#[error("{COULD_NOT_CONNECT}: {0}")]
|
||||||
WakeComputeError(#[from] WakeComputeError),
|
WakeComputeError(#[from] WakeComputeError),
|
||||||
@@ -73,7 +72,7 @@ impl UserFacingError for ConnectionError {
|
|||||||
ConnectionError::TooManyConnectionAttempts(_) => {
|
ConnectionError::TooManyConnectionAttempts(_) => {
|
||||||
"Failed to acquire permit to connect to the database. Too many database connection attempts are currently ongoing.".to_owned()
|
"Failed to acquire permit to connect to the database. Too many database connection attempts are currently ongoing.".to_owned()
|
||||||
}
|
}
|
||||||
_ => COULD_NOT_CONNECT.to_owned(),
|
ConnectionError::TlsError(_) => COULD_NOT_CONNECT.to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +84,6 @@ impl ReportableError for ConnectionError {
|
|||||||
crate::error::ErrorKind::Postgres
|
crate::error::ErrorKind::Postgres
|
||||||
}
|
}
|
||||||
ConnectionError::Postgres(_) => crate::error::ErrorKind::Compute,
|
ConnectionError::Postgres(_) => crate::error::ErrorKind::Compute,
|
||||||
ConnectionError::CouldNotConnect(_) => crate::error::ErrorKind::Compute,
|
|
||||||
ConnectionError::TlsError(_) => crate::error::ErrorKind::Compute,
|
ConnectionError::TlsError(_) => crate::error::ErrorKind::Compute,
|
||||||
ConnectionError::WakeComputeError(e) => e.get_error_kind(),
|
ConnectionError::WakeComputeError(e) => e.get_error_kind(),
|
||||||
ConnectionError::TooManyConnectionAttempts(e) => e.get_error_kind(),
|
ConnectionError::TooManyConnectionAttempts(e) => e.get_error_kind(),
|
||||||
@@ -96,34 +94,87 @@ impl ReportableError for ConnectionError {
|
|||||||
/// A pair of `ClientKey` & `ServerKey` for `SCRAM-SHA-256`.
|
/// A pair of `ClientKey` & `ServerKey` for `SCRAM-SHA-256`.
|
||||||
pub(crate) type ScramKeys = postgres_client::config::ScramKeys<32>;
|
pub(crate) type ScramKeys = postgres_client::config::ScramKeys<32>;
|
||||||
|
|
||||||
/// A config for establishing a connection to compute node.
|
|
||||||
/// Eventually, `postgres_client` will be replaced with something better.
|
|
||||||
/// Newtype allows us to implement methods on top of it.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct ConnCfg(Box<postgres_client::Config>);
|
pub enum Auth {
|
||||||
|
/// Only used during console-redirect.
|
||||||
|
Password(Vec<u8>),
|
||||||
|
/// Used by sql-over-http, ws, tcp.
|
||||||
|
Scram(Box<ScramKeys>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A config for authenticating to the compute node.
|
||||||
|
// TODO: avoid Clone
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct AuthInfo {
|
||||||
|
/// None for local-proxy, as we use trust-based localhost auth.
|
||||||
|
/// Some for sql-over-http, ws, tcp, and in most cases for console-redirect.
|
||||||
|
/// Might be None for console-redirect, but that's only a consequence of testing environments ATM.
|
||||||
|
auth: Option<Auth>,
|
||||||
|
server_params: StartupMessageParams,
|
||||||
|
|
||||||
|
/// Console redirect sets user and database, we shouldn't re-use those from the params.
|
||||||
|
skip_db_user: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains only the data needed to establish a secure connection to compute.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ConnectInfo {
|
||||||
|
pub host_addr: Option<IpAddr>,
|
||||||
|
pub host: Host,
|
||||||
|
pub port: u16,
|
||||||
|
pub ssl_mode: SslMode,
|
||||||
|
}
|
||||||
|
|
||||||
/// Creation and initialization routines.
|
/// Creation and initialization routines.
|
||||||
impl ConnCfg {
|
impl AuthInfo {
|
||||||
pub(crate) fn new(host: String, port: u16) -> Self {
|
pub(crate) fn for_console_redirect(db: &str, user: &str, pw: Option<&str>) -> Self {
|
||||||
Self(Box::new(postgres_client::Config::new(host, port)))
|
let mut server_params = StartupMessageParams::default();
|
||||||
}
|
server_params.insert("database", db);
|
||||||
|
server_params.insert("user", user);
|
||||||
/// Reuse password or auth keys from the other config.
|
Self {
|
||||||
pub(crate) fn reuse_password(&mut self, other: Self) {
|
auth: pw.map(|pw| Auth::Password(pw.as_bytes().to_owned())),
|
||||||
if let Some(password) = other.get_password() {
|
server_params,
|
||||||
self.password(password);
|
skip_db_user: true,
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(keys) = other.get_auth_keys() {
|
|
||||||
self.auth_keys(keys);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_host(&self) -> Host {
|
pub(crate) fn with_auth_keys(keys: ComputeCredentialKeys) -> Self {
|
||||||
match self.0.get_host() {
|
Self {
|
||||||
postgres_client::config::Host::Tcp(s) => s.into(),
|
auth: match keys {
|
||||||
|
ComputeCredentialKeys::AuthKeys(AuthKeys::ScramSha256(auth_keys)) => {
|
||||||
|
Some(Auth::Scram(Box::new(auth_keys)))
|
||||||
|
}
|
||||||
|
ComputeCredentialKeys::JwtPayload(_) | ComputeCredentialKeys::None => None,
|
||||||
|
},
|
||||||
|
server_params: StartupMessageParams::default(),
|
||||||
|
skip_db_user: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectInfo {
|
||||||
|
pub fn to_postgres_client_config(&self) -> postgres_client::Config {
|
||||||
|
let mut config = postgres_client::Config::new(self.host.to_string(), self.port);
|
||||||
|
config.ssl_mode(self.ssl_mode);
|
||||||
|
if let Some(host_addr) = self.host_addr {
|
||||||
|
config.set_host_addr(host_addr);
|
||||||
|
}
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthInfo {
|
||||||
|
fn enrich(&self, mut config: postgres_client::Config) -> postgres_client::Config {
|
||||||
|
match &self.auth {
|
||||||
|
Some(Auth::Scram(keys)) => config.auth_keys(AuthKeys::ScramSha256(**keys)),
|
||||||
|
Some(Auth::Password(pw)) => config.password(pw),
|
||||||
|
None => &mut config,
|
||||||
|
};
|
||||||
|
for (k, v) in self.server_params.iter() {
|
||||||
|
config.set_param(k, v);
|
||||||
|
}
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply startup message params to the connection config.
|
/// Apply startup message params to the connection config.
|
||||||
pub(crate) fn set_startup_params(
|
pub(crate) fn set_startup_params(
|
||||||
@@ -132,27 +183,26 @@ impl ConnCfg {
|
|||||||
arbitrary_params: bool,
|
arbitrary_params: bool,
|
||||||
) {
|
) {
|
||||||
if !arbitrary_params {
|
if !arbitrary_params {
|
||||||
self.set_param("client_encoding", "UTF8");
|
self.server_params.insert("client_encoding", "UTF8");
|
||||||
}
|
}
|
||||||
for (k, v) in params.iter() {
|
for (k, v) in params.iter() {
|
||||||
match k {
|
match k {
|
||||||
// Only set `user` if it's not present in the config.
|
// Only set `user` if it's not present in the config.
|
||||||
// Console redirect auth flow takes username from the console's response.
|
// Console redirect auth flow takes username from the console's response.
|
||||||
"user" if self.user_is_set() => {}
|
"user" | "database" if self.skip_db_user => {}
|
||||||
"database" if self.db_is_set() => {}
|
|
||||||
"options" => {
|
"options" => {
|
||||||
if let Some(options) = filtered_options(v) {
|
if let Some(options) = filtered_options(v) {
|
||||||
self.set_param(k, &options);
|
self.server_params.insert(k, &options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"user" | "database" | "application_name" | "replication" => {
|
"user" | "database" | "application_name" | "replication" => {
|
||||||
self.set_param(k, v);
|
self.server_params.insert(k, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we allow arbitrary params, then we forward them through.
|
// if we allow arbitrary params, then we forward them through.
|
||||||
// this is a flag for a period of backwards compatibility
|
// this is a flag for a period of backwards compatibility
|
||||||
k if arbitrary_params => {
|
k if arbitrary_params => {
|
||||||
self.set_param(k, v);
|
self.server_params.insert(k, v);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -160,25 +210,13 @@ impl ConnCfg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for ConnCfg {
|
impl ConnectInfo {
|
||||||
type Target = postgres_client::Config;
|
/// Establish a raw TCP+TLS connection to the compute node.
|
||||||
|
async fn connect_raw(
|
||||||
fn deref(&self) -> &Self::Target {
|
&self,
|
||||||
&self.0
|
config: &ComputeConfig,
|
||||||
}
|
) -> Result<(SocketAddr, MaybeTlsStream<TcpStream, RustlsStream>), TlsError> {
|
||||||
}
|
let timeout = config.timeout;
|
||||||
|
|
||||||
/// For now, let's make it easier to setup the config.
|
|
||||||
impl std::ops::DerefMut for ConnCfg {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnCfg {
|
|
||||||
/// Establish a raw TCP connection to the compute node.
|
|
||||||
async fn connect_raw(&self, timeout: Duration) -> io::Result<(SocketAddr, TcpStream, &str)> {
|
|
||||||
use postgres_client::config::Host;
|
|
||||||
|
|
||||||
// wrap TcpStream::connect with timeout
|
// wrap TcpStream::connect with timeout
|
||||||
let connect_with_timeout = |addrs| {
|
let connect_with_timeout = |addrs| {
|
||||||
@@ -208,34 +246,32 @@ impl ConnCfg {
|
|||||||
// We can't reuse connection establishing logic from `postgres_client` here,
|
// We can't reuse connection establishing logic from `postgres_client` here,
|
||||||
// because it has no means for extracting the underlying socket which we
|
// because it has no means for extracting the underlying socket which we
|
||||||
// require for our business.
|
// require for our business.
|
||||||
let port = self.0.get_port();
|
let port = self.port;
|
||||||
let host = self.0.get_host();
|
let host = &*self.host;
|
||||||
|
|
||||||
let host = match host {
|
let addrs = match self.host_addr {
|
||||||
Host::Tcp(host) => host.as_str(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let addrs = match self.0.get_host_addr() {
|
|
||||||
Some(addr) => vec![SocketAddr::new(addr, port)],
|
Some(addr) => vec![SocketAddr::new(addr, port)],
|
||||||
None => lookup_host((host, port)).await?.collect(),
|
None => lookup_host((host, port)).await?.collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match connect_once(&*addrs).await {
|
match connect_once(&*addrs).await {
|
||||||
Ok((sockaddr, stream)) => Ok((sockaddr, stream, host)),
|
Ok((sockaddr, stream)) => Ok((
|
||||||
|
sockaddr,
|
||||||
|
tls::connect_tls(stream, self.ssl_mode, config, host).await?,
|
||||||
|
)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("couldn't connect to compute node at {host}:{port}: {err}");
|
warn!("couldn't connect to compute node at {host}:{port}: {err}");
|
||||||
Err(err)
|
Err(TlsError::Connection(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RustlsStream = <MakeRustlsConnect as MakeTlsConnect<tokio::net::TcpStream>>::Stream;
|
type RustlsStream = <ComputeConfig as MakeTlsConnect<tokio::net::TcpStream>>::Stream;
|
||||||
|
|
||||||
pub(crate) struct PostgresConnection {
|
pub(crate) struct PostgresConnection {
|
||||||
/// Socket connected to a compute node.
|
/// Socket connected to a compute node.
|
||||||
pub(crate) stream:
|
pub(crate) stream: MaybeTlsStream<tokio::net::TcpStream, RustlsStream>,
|
||||||
postgres_client::maybe_tls_stream::MaybeTlsStream<tokio::net::TcpStream, RustlsStream>,
|
|
||||||
/// PostgreSQL connection parameters.
|
/// PostgreSQL connection parameters.
|
||||||
pub(crate) params: std::collections::HashMap<String, String>,
|
pub(crate) params: std::collections::HashMap<String, String>,
|
||||||
/// Query cancellation token.
|
/// Query cancellation token.
|
||||||
@@ -248,28 +284,23 @@ pub(crate) struct PostgresConnection {
|
|||||||
_guage: NumDbConnectionsGuard<'static>,
|
_guage: NumDbConnectionsGuard<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnCfg {
|
impl ConnectInfo {
|
||||||
/// Connect to a corresponding compute node.
|
/// Connect to a corresponding compute node.
|
||||||
pub(crate) async fn connect(
|
pub(crate) async fn connect(
|
||||||
&self,
|
&self,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
aux: MetricsAuxInfo,
|
aux: MetricsAuxInfo,
|
||||||
|
auth: &AuthInfo,
|
||||||
config: &ComputeConfig,
|
config: &ComputeConfig,
|
||||||
user_info: ComputeUserInfo,
|
user_info: ComputeUserInfo,
|
||||||
) -> Result<PostgresConnection, ConnectionError> {
|
) -> Result<PostgresConnection, ConnectionError> {
|
||||||
let pause = ctx.latency_timer_pause(crate::metrics::Waiting::Compute);
|
let mut tmp_config = auth.enrich(self.to_postgres_client_config());
|
||||||
let (socket_addr, stream, host) = self.connect_raw(config.timeout).await?;
|
// we setup SSL early in `ConnectInfo::connect_raw`.
|
||||||
drop(pause);
|
tmp_config.ssl_mode(SslMode::Disable);
|
||||||
|
|
||||||
let mut mk_tls = crate::tls::postgres_rustls::MakeRustlsConnect::new(config.tls.clone());
|
|
||||||
let tls = <MakeRustlsConnect as MakeTlsConnect<tokio::net::TcpStream>>::make_tls_connect(
|
|
||||||
&mut mk_tls,
|
|
||||||
host,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// connect_raw() will not use TLS if sslmode is "disable"
|
|
||||||
let pause = ctx.latency_timer_pause(crate::metrics::Waiting::Compute);
|
let pause = ctx.latency_timer_pause(crate::metrics::Waiting::Compute);
|
||||||
let connection = self.0.connect_raw(stream, tls).await?;
|
let (socket_addr, stream) = self.connect_raw(config).await?;
|
||||||
|
let connection = tmp_config.connect_raw(stream, NoTls).await?;
|
||||||
drop(pause);
|
drop(pause);
|
||||||
|
|
||||||
let RawConnection {
|
let RawConnection {
|
||||||
@@ -282,13 +313,14 @@ impl ConnCfg {
|
|||||||
|
|
||||||
tracing::Span::current().record("pid", tracing::field::display(process_id));
|
tracing::Span::current().record("pid", tracing::field::display(process_id));
|
||||||
tracing::Span::current().record("compute_id", tracing::field::display(&aux.compute_id));
|
tracing::Span::current().record("compute_id", tracing::field::display(&aux.compute_id));
|
||||||
let stream = stream.into_inner();
|
let MaybeTlsStream::Raw(stream) = stream.into_inner();
|
||||||
|
|
||||||
// TODO: lots of useful info but maybe we can move it elsewhere (eg traces?)
|
// TODO: lots of useful info but maybe we can move it elsewhere (eg traces?)
|
||||||
info!(
|
info!(
|
||||||
cold_start_info = ctx.cold_start_info().as_str(),
|
cold_start_info = ctx.cold_start_info().as_str(),
|
||||||
"connected to compute node at {host} ({socket_addr}) sslmode={:?}, latency={}, query_id={}",
|
"connected to compute node at {} ({socket_addr}) sslmode={:?}, latency={}, query_id={}",
|
||||||
self.0.get_ssl_mode(),
|
self.host,
|
||||||
|
self.ssl_mode,
|
||||||
ctx.get_proxy_latency(),
|
ctx.get_proxy_latency(),
|
||||||
ctx.get_testodrome_id().unwrap_or_default(),
|
ctx.get_testodrome_id().unwrap_or_default(),
|
||||||
);
|
);
|
||||||
@@ -299,11 +331,11 @@ impl ConnCfg {
|
|||||||
socket_addr,
|
socket_addr,
|
||||||
CancelToken {
|
CancelToken {
|
||||||
socket_config: None,
|
socket_config: None,
|
||||||
ssl_mode: self.0.get_ssl_mode(),
|
ssl_mode: self.ssl_mode,
|
||||||
process_id,
|
process_id,
|
||||||
secret_key,
|
secret_key,
|
||||||
},
|
},
|
||||||
host.to_string(),
|
self.host.to_string(),
|
||||||
user_info,
|
user_info,
|
||||||
);
|
);
|
||||||
|
|
||||||
63
proxy/src/compute/tls.rs
Normal file
63
proxy/src/compute/tls.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
use futures::FutureExt;
|
||||||
|
use postgres_client::config::SslMode;
|
||||||
|
use postgres_client::maybe_tls_stream::MaybeTlsStream;
|
||||||
|
use postgres_client::tls::{MakeTlsConnect, TlsConnect};
|
||||||
|
use rustls::pki_types::InvalidDnsNameError;
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
|
|
||||||
|
use crate::pqproto::request_tls;
|
||||||
|
use crate::proxy::retry::CouldRetry;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum TlsError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Dns(#[from] InvalidDnsNameError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Connection(#[from] std::io::Error),
|
||||||
|
#[error("TLS required but not provided")]
|
||||||
|
Required,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CouldRetry for TlsError {
|
||||||
|
fn could_retry(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
TlsError::Dns(_) => false,
|
||||||
|
TlsError::Connection(err) => err.could_retry(),
|
||||||
|
// perhaps compute didn't realise it supports TLS?
|
||||||
|
TlsError::Required => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect_tls<S, T>(
|
||||||
|
mut stream: S,
|
||||||
|
mode: SslMode,
|
||||||
|
tls: &T,
|
||||||
|
host: &str,
|
||||||
|
) -> Result<MaybeTlsStream<S, T::Stream>, TlsError>
|
||||||
|
where
|
||||||
|
S: AsyncRead + AsyncWrite + Unpin + Send,
|
||||||
|
T: MakeTlsConnect<
|
||||||
|
S,
|
||||||
|
Error = InvalidDnsNameError,
|
||||||
|
TlsConnect: TlsConnect<S, Error = std::io::Error, Future: Send>,
|
||||||
|
>,
|
||||||
|
{
|
||||||
|
match mode {
|
||||||
|
SslMode::Disable => return Ok(MaybeTlsStream::Raw(stream)),
|
||||||
|
SslMode::Prefer | SslMode::Require => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !request_tls(&mut stream).await? {
|
||||||
|
if SslMode::Require == mode {
|
||||||
|
return Err(TlsError::Required);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(MaybeTlsStream::Raw(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(MaybeTlsStream::Tls(
|
||||||
|
tls.make_tls_connect(host)?.connect(stream).boxed().await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ use arc_swap::ArcSwapOption;
|
|||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use remote_storage::RemoteStorageConfig;
|
use remote_storage::RemoteStorageConfig;
|
||||||
|
|
||||||
use crate::auth::backend::AuthRateLimiter;
|
|
||||||
use crate::auth::backend::jwt::JwkCache;
|
use crate::auth::backend::jwt::JwkCache;
|
||||||
use crate::control_plane::locks::ApiLocks;
|
use crate::control_plane::locks::ApiLocks;
|
||||||
use crate::rate_limiter::{RateBucketInfo, RateLimitAlgorithm, RateLimiterConfig};
|
use crate::rate_limiter::{RateBucketInfo, RateLimitAlgorithm, RateLimiterConfig};
|
||||||
@@ -40,8 +39,6 @@ pub struct ComputeConfig {
|
|||||||
pub enum ProxyProtocolV2 {
|
pub enum ProxyProtocolV2 {
|
||||||
/// Connection will error if PROXY protocol v2 header is missing
|
/// Connection will error if PROXY protocol v2 header is missing
|
||||||
Required,
|
Required,
|
||||||
/// Connection will parse PROXY protocol v2 header, but accept the connection if it's missing.
|
|
||||||
Supported,
|
|
||||||
/// Connection will error if PROXY protocol v2 header is provided
|
/// Connection will error if PROXY protocol v2 header is provided
|
||||||
Rejected,
|
Rejected,
|
||||||
}
|
}
|
||||||
@@ -65,9 +62,6 @@ pub struct HttpConfig {
|
|||||||
pub struct AuthenticationConfig {
|
pub struct AuthenticationConfig {
|
||||||
pub thread_pool: Arc<ThreadPool>,
|
pub thread_pool: Arc<ThreadPool>,
|
||||||
pub scram_protocol_timeout: tokio::time::Duration,
|
pub scram_protocol_timeout: tokio::time::Duration,
|
||||||
pub rate_limiter_enabled: bool,
|
|
||||||
pub rate_limiter: AuthRateLimiter,
|
|
||||||
pub rate_limit_ip_subnet: u8,
|
|
||||||
pub ip_allowlist_check_enabled: bool,
|
pub ip_allowlist_check_enabled: bool,
|
||||||
pub is_vpc_acccess_proxy: bool,
|
pub is_vpc_acccess_proxy: bool,
|
||||||
pub jwks_cache: JwkCache,
|
pub jwks_cache: JwkCache,
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ use crate::config::{ProxyConfig, ProxyProtocolV2};
|
|||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::error::ReportableError;
|
use crate::error::ReportableError;
|
||||||
use crate::metrics::{Metrics, NumClientConnectionsGuard};
|
use crate::metrics::{Metrics, NumClientConnectionsGuard};
|
||||||
|
use crate::pglb::handshake::{HandshakeData, handshake};
|
||||||
|
use crate::pglb::passthrough::ProxyPassthrough;
|
||||||
|
use crate::pglb::{ClientRequestError, ErrorSource};
|
||||||
use crate::protocol2::{ConnectHeader, ConnectionInfo, read_proxy_protocol};
|
use crate::protocol2::{ConnectHeader, ConnectionInfo, read_proxy_protocol};
|
||||||
use crate::proxy::connect_compute::{TcpMechanism, connect_to_compute};
|
use crate::proxy::connect_compute::{TcpMechanism, connect_to_compute};
|
||||||
use crate::proxy::handshake::{HandshakeData, handshake};
|
use crate::proxy::prepare_client_connection;
|
||||||
use crate::proxy::passthrough::ProxyPassthrough;
|
use crate::util::run_until_cancelled;
|
||||||
use crate::proxy::{
|
|
||||||
ClientRequestError, ErrorSource, prepare_client_connection, run_until_cancelled,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn task_main(
|
pub async fn task_main(
|
||||||
config: &'static ProxyConfig,
|
config: &'static ProxyConfig,
|
||||||
@@ -54,30 +54,24 @@ pub async fn task_main(
|
|||||||
debug!(protocol = "tcp", %session_id, "accepted new TCP connection");
|
debug!(protocol = "tcp", %session_id, "accepted new TCP connection");
|
||||||
|
|
||||||
connections.spawn(async move {
|
connections.spawn(async move {
|
||||||
let (socket, peer_addr) = match read_proxy_protocol(socket).await {
|
let (socket, conn_info) = match config.proxy_protocol_v2 {
|
||||||
Err(e) => {
|
ProxyProtocolV2::Required => {
|
||||||
error!("per-client task finished with an error: {e:#}");
|
match read_proxy_protocol(socket).await {
|
||||||
return;
|
Err(e) => {
|
||||||
|
error!("per-client task finished with an error: {e:#}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// our load balancers will not send any more data. let's just exit immediately
|
||||||
|
Ok((_socket, ConnectHeader::Local)) => {
|
||||||
|
debug!("healthcheck received");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok((socket, ConnectHeader::Proxy(info))) => (socket, info),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// our load balancers will not send any more data. let's just exit immediately
|
// ignore the header - it cannot be confused for a postgres or http connection so will
|
||||||
Ok((_socket, ConnectHeader::Local)) => {
|
// error later.
|
||||||
debug!("healthcheck received");
|
ProxyProtocolV2::Rejected => (
|
||||||
return;
|
|
||||||
}
|
|
||||||
Ok((_socket, ConnectHeader::Missing))
|
|
||||||
if config.proxy_protocol_v2 == ProxyProtocolV2::Required =>
|
|
||||||
{
|
|
||||||
error!("missing required proxy protocol header");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Ok((_socket, ConnectHeader::Proxy(_)))
|
|
||||||
if config.proxy_protocol_v2 == ProxyProtocolV2::Rejected =>
|
|
||||||
{
|
|
||||||
error!("proxy protocol header not supported");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Ok((socket, ConnectHeader::Proxy(info))) => (socket, info),
|
|
||||||
Ok((socket, ConnectHeader::Missing)) => (
|
|
||||||
socket,
|
socket,
|
||||||
ConnectionInfo {
|
ConnectionInfo {
|
||||||
addr: peer_addr,
|
addr: peer_addr,
|
||||||
@@ -86,7 +80,7 @@ pub async fn task_main(
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
match socket.inner.set_nodelay(true) {
|
match socket.set_nodelay(true) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
@@ -98,7 +92,7 @@ pub async fn task_main(
|
|||||||
|
|
||||||
let ctx = RequestContext::new(
|
let ctx = RequestContext::new(
|
||||||
session_id,
|
session_id,
|
||||||
peer_addr,
|
conn_info,
|
||||||
crate::metrics::Protocol::Tcp,
|
crate::metrics::Protocol::Tcp,
|
||||||
&config.region,
|
&config.region,
|
||||||
);
|
);
|
||||||
@@ -159,7 +153,7 @@ pub async fn task_main(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) async fn handle_client<S: AsyncRead + AsyncWrite + Unpin>(
|
pub(crate) async fn handle_client<S: AsyncRead + AsyncWrite + Unpin + Send>(
|
||||||
config: &'static ProxyConfig,
|
config: &'static ProxyConfig,
|
||||||
backend: &'static ConsoleRedirectBackend,
|
backend: &'static ConsoleRedirectBackend,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
@@ -216,20 +210,20 @@ pub(crate) async fn handle_client<S: AsyncRead + AsyncWrite + Unpin>(
|
|||||||
|
|
||||||
ctx.set_db_options(params.clone());
|
ctx.set_db_options(params.clone());
|
||||||
|
|
||||||
let (node_info, user_info, _ip_allowlist) = match backend
|
let (node_info, mut auth_info, user_info) = match backend
|
||||||
.authenticate(ctx, &config.authentication_config, &mut stream)
|
.authenticate(ctx, &config.authentication_config, &mut stream)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(auth_result) => auth_result,
|
Ok(auth_result) => auth_result,
|
||||||
Err(e) => Err(stream.throw_error(e, Some(ctx)).await)?,
|
Err(e) => Err(stream.throw_error(e, Some(ctx)).await)?,
|
||||||
};
|
};
|
||||||
|
auth_info.set_startup_params(¶ms, true);
|
||||||
|
|
||||||
let node = connect_to_compute(
|
let node = connect_to_compute(
|
||||||
ctx,
|
ctx,
|
||||||
&TcpMechanism {
|
&TcpMechanism {
|
||||||
user_info,
|
user_info,
|
||||||
params_compat: true,
|
auth: auth_info,
|
||||||
params: ¶ms,
|
|
||||||
locks: &config.connect_compute_locks,
|
locks: &config.connect_compute_locks,
|
||||||
},
|
},
|
||||||
&node_info,
|
&node_info,
|
||||||
|
|||||||
@@ -370,6 +370,18 @@ impl RequestContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn latency_timer_pause_at(
|
||||||
|
&self,
|
||||||
|
at: tokio::time::Instant,
|
||||||
|
waiting_for: Waiting,
|
||||||
|
) -> LatencyTimerPause<'_> {
|
||||||
|
LatencyTimerPause {
|
||||||
|
ctx: self,
|
||||||
|
start: at,
|
||||||
|
waiting_for,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn get_proxy_latency(&self) -> LatencyAccumulated {
|
pub(crate) fn get_proxy_latency(&self) -> LatencyAccumulated {
|
||||||
self.0
|
self.0
|
||||||
.try_lock()
|
.try_lock()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user