Compare commits

..

2 Commits

Author SHA1 Message Date
Konstantin Knizhnik
d07101d317 Make clippy happy 2023-03-10 09:52:12 +02:00
Konstantin Knizhnik
89e4fc3c63 Copy block content in block cursor cache 2023-03-10 08:19:20 +02:00
103 changed files with 659 additions and 2379 deletions

View File

@@ -118,7 +118,7 @@
cmd: | cmd: |
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id) INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/pageservers/$INSTANCE_ID | jq '.version = {{ current_version }}' > /tmp/new_version curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/pageservers/$INSTANCE_ID | jq '.version = {{ current_version }}' > /tmp/new_version
curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" -H "Content-Type: application/json" -X POST -d@/tmp/new_version {{ console_mgmt_base_url }}/management/api/v2/pageservers curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" -X POST -d@/tmp/new_version {{ console_mgmt_base_url }}/management/api/v2/pageservers
tags: tags:
- pageserver - pageserver
@@ -188,6 +188,6 @@
cmd: | cmd: |
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id) INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/safekeepers/$INSTANCE_ID | jq '.version = {{ current_version }}' > /tmp/new_version curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/safekeepers/$INSTANCE_ID | jq '.version = {{ current_version }}' > /tmp/new_version
curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" -H "Content-Type: application/json" -X POST -d@/tmp/new_version {{ console_mgmt_base_url }}/management/api/v2/safekeepers curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" -X POST -d@/tmp/new_version {{ console_mgmt_base_url }}/management/api/v2/safekeepers
tags: tags:
- safekeeper - safekeeper

View File

@@ -26,7 +26,7 @@ EOF
if ! curl -sf -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/pageservers/${INSTANCE_ID} -o /dev/null; then if ! curl -sf -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/pageservers/${INSTANCE_ID} -o /dev/null; then
# not registered, so register it now # not registered, so register it now
ID=$(curl -sf -X POST -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" -H "Content-Type: application/json" {{ console_mgmt_base_url }}/management/api/v2/pageservers -d@/tmp/payload | jq -r '.id') ID=$(curl -sf -X POST -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/pageservers -d@/tmp/payload | jq -r '.id')
# init pageserver # init pageserver
sudo -u pageserver /usr/local/bin/pageserver -c "id=${ID}" -c "pg_distrib_dir='/usr/local'" --init -D /storage/pageserver/data sudo -u pageserver /usr/local/bin/pageserver -c "id=${ID}" -c "pg_distrib_dir='/usr/local'" --init -D /storage/pageserver/data

View File

@@ -25,7 +25,7 @@ EOF
if ! curl -sf -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/safekeepers/${INSTANCE_ID} -o /dev/null; then if ! curl -sf -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/safekeepers/${INSTANCE_ID} -o /dev/null; then
# not registered, so register it now # not registered, so register it now
ID=$(curl -sf -X POST -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" -H "Content-Type: application/json" {{ console_mgmt_base_url }}/management/api/v2/safekeepers -d@/tmp/payload | jq -r '.id') ID=$(curl -sf -X POST -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/safekeepers -d@/tmp/payload | jq -r '.id')
# init safekeeper # init safekeeper
sudo -u safekeeper /usr/local/bin/safekeeper --id ${ID} --init -D /storage/safekeeper/data sudo -u safekeeper /usr/local/bin/safekeeper --id ${ID} --init -D /storage/safekeeper/data
fi fi

View File

@@ -1,22 +1,6 @@
# Helm chart values for neon-proxy-scram. # Helm chart values for neon-proxy-scram.
# This is a YAML-formatted file. # This is a YAML-formatted file.
deploymentStrategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 100%
maxUnavailable: 50%
# Delay the kill signal by 7 days (7 * 24 * 60 * 60)
# The pod(s) will stay in Terminating, keeps the existing connections
# but doesn't receive new ones
containerLifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 604800"]
terminationGracePeriodSeconds: 604800
image: image:
repository: neondatabase/neon repository: neondatabase/neon

View File

@@ -74,12 +74,15 @@ jobs:
- name: Install Python deps - name: Install Python deps
run: ./scripts/pysync run: ./scripts/pysync
- name: Run ruff to ensure code format - name: Run isort to ensure code format
run: poetry run ruff . run: poetry run isort --diff --check .
- name: Run black to ensure code format - name: Run black to ensure code format
run: poetry run black --diff --check . run: poetry run black --diff --check .
- name: Run flake8 to ensure code format
run: poetry run flake8 .
- name: Run mypy to check types - name: Run mypy to check types
run: poetry run mypy . run: poetry run mypy .
@@ -548,48 +551,6 @@ jobs:
- name: Cleanup ECR folder - name: Cleanup ECR folder
run: rm -rf ~/.ecr run: rm -rf ~/.ecr
neon-image-depot:
# For testing this will run side-by-side for a few merges.
# This action is not really optimized yet, but gets the job done
runs-on: [ self-hosted, gen3, small ]
needs: [ tag ]
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/base:pinned
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: true
fetch-depth: 0
- name: Setup go
uses: actions/setup-go@v3
with:
go-version: '1.19'
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: Install Crane & ECR helper
run: go install github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login@69c85dc22db6511932bbf119e1a0cc5c90c69a7f # v0.6.0
- name: Configure ECR login
run: |
mkdir /github/home/.docker/
echo "{\"credsStore\":\"ecr-login\"}" > /github/home/.docker/config.json
- name: Build and push
uses: depot/build-push-action@v1
with:
# if no depot.json file is at the root of your repo, you must specify the project id
project: nrdv0s4kcs
push: true
tags: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:depot-${{needs.tag.outputs.build-tag}}
compute-tools-image: compute-tools-image:
runs-on: [ self-hosted, gen3, large ] runs-on: [ self-hosted, gen3, large ]
needs: [ tag ] needs: [ tag ]

View File

@@ -31,4 +31,3 @@ jobs:
head: releases/${{ steps.date.outputs.date }} head: releases/${{ steps.date.outputs.date }}
base: release base: release
title: Release ${{ steps.date.outputs.date }} title: Release ${{ steps.date.outputs.date }}
team_reviewers: release

3
Cargo.lock generated
View File

@@ -851,7 +851,6 @@ dependencies = [
"futures", "futures",
"hyper", "hyper",
"notify", "notify",
"num_cpus",
"opentelemetry", "opentelemetry",
"postgres", "postgres",
"regex", "regex",
@@ -3334,6 +3333,7 @@ dependencies = [
"humantime", "humantime",
"hyper", "hyper",
"metrics", "metrics",
"nix",
"once_cell", "once_cell",
"parking_lot", "parking_lot",
"postgres", "postgres",
@@ -4532,7 +4532,6 @@ dependencies = [
"metrics", "metrics",
"nix", "nix",
"once_cell", "once_cell",
"pin-project-lite",
"rand", "rand",
"routerify", "routerify",
"sentry", "sentry",

View File

@@ -64,7 +64,6 @@ md5 = "0.7.0"
memoffset = "0.8" memoffset = "0.8"
nix = "0.26" nix = "0.26"
notify = "5.0.0" notify = "5.0.0"
num_cpus = "1.15"
num-traits = "0.2.15" num-traits = "0.2.15"
once_cell = "1.13" once_cell = "1.13"
opentelemetry = "0.18.0" opentelemetry = "0.18.0"

View File

@@ -39,7 +39,7 @@ ARG CACHEPOT_BUCKET=neon-github-dev
COPY --from=pg-build /home/nonroot/pg_install/v14/include/postgresql/server pg_install/v14/include/postgresql/server COPY --from=pg-build /home/nonroot/pg_install/v14/include/postgresql/server pg_install/v14/include/postgresql/server
COPY --from=pg-build /home/nonroot/pg_install/v15/include/postgresql/server pg_install/v15/include/postgresql/server COPY --from=pg-build /home/nonroot/pg_install/v15/include/postgresql/server pg_install/v15/include/postgresql/server
COPY --chown=nonroot . . COPY . .
# Show build caching stats to check if it was used in the end. # Show build caching stats to check if it was used in the end.
# Has to be the part of the same RUN since cachepot daemon is killed in the end of this RUN, losing the compilation stats. # Has to be the part of the same RUN since cachepot daemon is killed in the end of this RUN, losing the compilation stats.

View File

@@ -323,7 +323,7 @@ RUN curl -sSO https://static.rust-lang.org/rustup/dist/$(uname -m)-unknown-linux
chmod +x rustup-init && \ chmod +x rustup-init && \
./rustup-init -y --no-modify-path --profile minimal --default-toolchain stable && \ ./rustup-init -y --no-modify-path --profile minimal --default-toolchain stable && \
rm rustup-init && \ rm rustup-init && \
cargo install --locked --version 0.7.3 cargo-pgx && \ cargo install --git https://github.com/vadim2404/pgx --branch neon_abi_v0.6.1 --locked cargo-pgx && \
/bin/bash -c 'cargo pgx init --pg${PG_VERSION:1}=/usr/local/pgsql/bin/pg_config' /bin/bash -c 'cargo pgx init --pg${PG_VERSION:1}=/usr/local/pgsql/bin/pg_config'
USER root USER root
@@ -337,11 +337,11 @@ USER root
FROM rust-extensions-build AS pg-jsonschema-pg-build FROM rust-extensions-build AS pg-jsonschema-pg-build
# there is no release tag yet, but we need it due to the superuser fix in the control file RUN git clone --depth=1 --single-branch --branch neon_abi_v0.1.4 https://github.com/vadim2404/pg_jsonschema/ && \
RUN wget https://github.com/supabase/pg_jsonschema/archive/caeab60d70b2fd3ae421ec66466a3abbb37b7ee6.tar.gz -O pg_jsonschema.tar.gz && \ cd pg_jsonschema && \
mkdir pg_jsonschema-src && cd pg_jsonschema-src && tar xvzf ../pg_jsonschema.tar.gz --strip-components=1 -C . && \
sed -i 's/pgx = "0.7.1"/pgx = { version = "0.7.3", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
cargo pgx install --release && \ cargo pgx install --release && \
# it's needed to enable extension because it uses untrusted C language
sed -i 's/superuser = false/superuser = true/g' /usr/local/pgsql/share/extension/pg_jsonschema.control && \
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_jsonschema.control echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_jsonschema.control
######################################################################################### #########################################################################################
@@ -353,32 +353,13 @@ RUN wget https://github.com/supabase/pg_jsonschema/archive/caeab60d70b2fd3ae421e
FROM rust-extensions-build AS pg-graphql-pg-build FROM rust-extensions-build AS pg-graphql-pg-build
# Currently pgx version bump to >= 0.7.2 causes "call to unsafe function" compliation errors in RUN git clone --depth=1 --single-branch --branch neon_abi_v1.1.0 https://github.com/vadim2404/pg_graphql && \
# pgx-contrib-spiext. There is a branch that removes that dependency, so use it. It is on the cd pg_graphql && \
# same 1.1 version we've used before.
RUN git clone -b remove-pgx-contrib-spiext --single-branch https://github.com/yrashk/pg_graphql && \
cd pg_graphql && \
sed -i 's/pgx = "~0.7.1"/pgx = { version = "0.7.3", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
sed -i 's/pgx-tests = "~0.7.1"/pgx-tests = "0.7.3"/g' Cargo.toml && \
cargo pgx install --release && \ cargo pgx install --release && \
# it's needed to enable extension because it uses untrusted C language # it's needed to enable extension because it uses untrusted C language
sed -i 's/superuser = false/superuser = true/g' /usr/local/pgsql/share/extension/pg_graphql.control && \ sed -i 's/superuser = false/superuser = true/g' /usr/local/pgsql/share/extension/pg_graphql.control && \
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_graphql.control echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_graphql.control
#########################################################################################
#
# Layer "pg-tiktoken-build"
# Compile "pg_tiktoken" extension
#
#########################################################################################
FROM rust-extensions-build AS pg-tiktoken-pg-build
RUN git clone --depth=1 --single-branch https://github.com/kelvich/pg_tiktoken && \
cd pg_tiktoken && \
cargo pgx install --release && \
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_tiktoken.control
######################################################################################### #########################################################################################
# #
# Layer "neon-pg-ext-build" # Layer "neon-pg-ext-build"
@@ -396,7 +377,6 @@ COPY --from=vector-pg-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=pgjwt-pg-build /usr/local/pgsql/ /usr/local/pgsql/ COPY --from=pgjwt-pg-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=pg-jsonschema-pg-build /usr/local/pgsql/ /usr/local/pgsql/ COPY --from=pg-jsonschema-pg-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=pg-graphql-pg-build /usr/local/pgsql/ /usr/local/pgsql/ COPY --from=pg-graphql-pg-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=pg-tiktoken-pg-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=hypopg-pg-build /usr/local/pgsql/ /usr/local/pgsql/ COPY --from=hypopg-pg-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=pg-hashids-pg-build /usr/local/pgsql/ /usr/local/pgsql/ COPY --from=pg-hashids-pg-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=rum-pg-build /usr/local/pgsql/ /usr/local/pgsql/ COPY --from=rum-pg-build /usr/local/pgsql/ /usr/local/pgsql/
@@ -409,10 +389,6 @@ COPY pgxn/ pgxn/
RUN make -j $(getconf _NPROCESSORS_ONLN) \ RUN make -j $(getconf _NPROCESSORS_ONLN) \
PG_CONFIG=/usr/local/pgsql/bin/pg_config \ PG_CONFIG=/usr/local/pgsql/bin/pg_config \
-C pgxn/neon \ -C pgxn/neon \
-s install && \
make -j $(getconf _NPROCESSORS_ONLN) \
PG_CONFIG=/usr/local/pgsql/bin/pg_config \
-C pgxn/neon_utils \
-s install -s install
######################################################################################### #########################################################################################

View File

@@ -1,7 +1,7 @@
# Note: this file *mostly* just builds on Dockerfile.compute-node # Note: this file *mostly* just builds on Dockerfile.compute-node
ARG SRC_IMAGE ARG SRC_IMAGE
ARG VM_INFORMANT_VERSION=v0.1.14 ARG VM_INFORMANT_VERSION=v0.1.6
# Pull VM informant and set up inittab # Pull VM informant and set up inittab
FROM neondatabase/vm-informant:$VM_INFORMANT_VERSION as informant FROM neondatabase/vm-informant:$VM_INFORMANT_VERSION as informant
@@ -11,9 +11,7 @@ RUN set -e \
&& touch /etc/inittab && touch /etc/inittab
RUN set -e \ RUN set -e \
&& CONNSTR="dbname=neondb user=cloud_admin sslmode=disable" \ && echo "::respawn:su vm-informant -c '/usr/local/bin/vm-informant --auto-restart'" >> /etc/inittab
&& ARGS="--auto-restart --pgconnstr=\"$CONNSTR\"" \
&& echo "::respawn:su vm-informant -c '/usr/local/bin/vm-informant $ARGS'" >> /etc/inittab
# Combine, starting from non-VM compute node image. # Combine, starting from non-VM compute node image.
FROM $SRC_IMAGE as base FROM $SRC_IMAGE as base

View File

@@ -133,11 +133,6 @@ neon-pg-ext-%: postgres-%
$(MAKE) PG_CONFIG=$(POSTGRES_INSTALL_DIR)/$*/bin/pg_config CFLAGS='$(PG_CFLAGS) $(COPT)' \ $(MAKE) PG_CONFIG=$(POSTGRES_INSTALL_DIR)/$*/bin/pg_config CFLAGS='$(PG_CFLAGS) $(COPT)' \
-C $(POSTGRES_INSTALL_DIR)/build/neon-test-utils-$* \ -C $(POSTGRES_INSTALL_DIR)/build/neon-test-utils-$* \
-f $(ROOT_PROJECT_DIR)/pgxn/neon_test_utils/Makefile install -f $(ROOT_PROJECT_DIR)/pgxn/neon_test_utils/Makefile install
+@echo "Compiling neon_utils $*"
mkdir -p $(POSTGRES_INSTALL_DIR)/build/neon-utils-$*
$(MAKE) PG_CONFIG=$(POSTGRES_INSTALL_DIR)/$*/bin/pg_config CFLAGS='$(PG_CFLAGS) $(COPT)' \
-C $(POSTGRES_INSTALL_DIR)/build/neon-utils-$* \
-f $(ROOT_PROJECT_DIR)/pgxn/neon_utils/Makefile install
.PHONY: neon-pg-ext-clean-% .PHONY: neon-pg-ext-clean-%
neon-pg-ext-clean-%: neon-pg-ext-clean-%:
@@ -150,9 +145,6 @@ neon-pg-ext-clean-%:
$(MAKE) PG_CONFIG=$(POSTGRES_INSTALL_DIR)/$*/bin/pg_config \ $(MAKE) PG_CONFIG=$(POSTGRES_INSTALL_DIR)/$*/bin/pg_config \
-C $(POSTGRES_INSTALL_DIR)/build/neon-test-utils-$* \ -C $(POSTGRES_INSTALL_DIR)/build/neon-test-utils-$* \
-f $(ROOT_PROJECT_DIR)/pgxn/neon_test_utils/Makefile clean -f $(ROOT_PROJECT_DIR)/pgxn/neon_test_utils/Makefile clean
$(MAKE) PG_CONFIG=$(POSTGRES_INSTALL_DIR)/$*/bin/pg_config \
-C $(POSTGRES_INSTALL_DIR)/build/neon-utils-$* \
-f $(ROOT_PROJECT_DIR)/pgxn/neon_utils/Makefile clean
.PHONY: neon-pg-ext .PHONY: neon-pg-ext
neon-pg-ext: \ neon-pg-ext: \

View File

@@ -11,7 +11,6 @@ clap.workspace = true
futures.workspace = true futures.workspace = true
hyper = { workspace = true, features = ["full"] } hyper = { workspace = true, features = ["full"] }
notify.workspace = true notify.workspace = true
num_cpus.workspace = true
opentelemetry.workspace = true opentelemetry.workspace = true
postgres.workspace = true postgres.workspace = true
regex.workspace = true regex.workspace = true

View File

@@ -7,7 +7,6 @@ use crate::compute::ComputeNode;
use anyhow::Result; use anyhow::Result;
use hyper::service::{make_service_fn, service_fn}; use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Method, Request, Response, Server, StatusCode}; use hyper::{Body, Method, Request, Response, Server, StatusCode};
use num_cpus;
use serde_json; use serde_json;
use tracing::{error, info}; use tracing::{error, info};
use tracing_utils::http::OtelName; use tracing_utils::http::OtelName;
@@ -50,17 +49,6 @@ async fn routes(req: Request<Body>, compute: &Arc<ComputeNode>) -> Response<Body
} }
} }
(&Method::GET, "/info") => {
let num_cpus = num_cpus::get_physical();
info!("serving /info GET request. num_cpus: {}", num_cpus);
Response::new(Body::from(
serde_json::json!({
"num_cpus": num_cpus,
})
.to_string(),
))
}
// Return the `404 Not Found` for any other routes. // Return the `404 Not Found` for any other routes.
_ => { _ => {
let mut not_found = Response::new(Body::from("404 Not Found")); let mut not_found = Response::new(Body::from("404 Not Found"));

View File

@@ -53,21 +53,6 @@ paths:
schema: schema:
$ref: "#/components/schemas/ComputeInsights" $ref: "#/components/schemas/ComputeInsights"
/info:
get:
tags:
- "info"
summary: Get info about the compute Pod/VM
description: ""
operationId: getInfo
responses:
"200":
description: Info
content:
application/json:
schema:
$ref: "#/components/schemas/Info"
/check_writability: /check_writability:
post: post:
tags: tags:
@@ -111,15 +96,6 @@ components:
total_startup_ms: total_startup_ms:
type: integer type: integer
Info:
type: object
description: Information about VM/Pod
required:
- num_cpus
properties:
num_cpus:
type: integer
ComputeState: ComputeState:
type: object type: object
required: required:

View File

@@ -47,23 +47,12 @@ pub struct GenericOption {
/// declare a `trait` on it. /// declare a `trait` on it.
pub type GenericOptions = Option<Vec<GenericOption>>; pub type GenericOptions = Option<Vec<GenericOption>>;
/// Escape a string for including it in a SQL literal
fn escape_literal(s: &str) -> String {
s.replace('\'', "''").replace('\\', "\\\\")
}
/// Escape a string so that it can be used in postgresql.conf.
/// Same as escape_literal, currently.
fn escape_conf_value(s: &str) -> String {
s.replace('\'', "''").replace('\\', "\\\\")
}
impl GenericOption { impl GenericOption {
/// Represent `GenericOption` as SQL statement parameter. /// Represent `GenericOption` as SQL statement parameter.
pub fn to_pg_option(&self) -> String { pub fn to_pg_option(&self) -> String {
if let Some(val) = &self.value { if let Some(val) = &self.value {
match self.vartype.as_ref() { match self.vartype.as_ref() {
"string" => format!("{} '{}'", self.name, escape_literal(val)), "string" => format!("{} '{}'", self.name, val),
_ => format!("{} {}", self.name, val), _ => format!("{} {}", self.name, val),
} }
} else { } else {
@@ -84,7 +73,7 @@ impl GenericOption {
}; };
match self.vartype.as_ref() { match self.vartype.as_ref() {
"string" => format!("{} = '{}'", name, escape_conf_value(val)), "string" => format!("{} = '{}'", name, val),
_ => format!("{} = {}", name, val), _ => format!("{} = {}", name, val),
} }
} else { } else {
@@ -120,7 +109,6 @@ impl PgOptionsSerialize for GenericOptions {
.map(|op| op.to_pg_setting()) .map(|op| op.to_pg_setting())
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n") .join("\n")
+ "\n" // newline after last setting
} else { } else {
"".to_string() "".to_string()
} }

View File

@@ -178,11 +178,6 @@
"name": "neon.pageserver_connstring", "name": "neon.pageserver_connstring",
"value": "host=127.0.0.1 port=6400", "value": "host=127.0.0.1 port=6400",
"vartype": "string" "vartype": "string"
},
{
"name": "test.escaping",
"value": "here's a backslash \\ and a quote ' and a double-quote \" hooray",
"vartype": "string"
} }
] ]
}, },

View File

@@ -28,30 +28,7 @@ mod pg_helpers_tests {
assert_eq!( assert_eq!(
spec.cluster.settings.as_pg_settings(), spec.cluster.settings.as_pg_settings(),
r#"fsync = off "fsync = off\nwal_level = replica\nhot_standby = on\nneon.safekeepers = '127.0.0.1:6502,127.0.0.1:6503,127.0.0.1:6501'\nwal_log_hints = on\nlog_connections = on\nshared_buffers = 32768\nport = 55432\nmax_connections = 100\nmax_wal_senders = 10\nlisten_addresses = '0.0.0.0'\nwal_sender_timeout = 0\npassword_encryption = md5\nmaintenance_work_mem = 65536\nmax_parallel_workers = 8\nmax_worker_processes = 8\nneon.tenant_id = 'b0554b632bd4d547a63b86c3630317e8'\nmax_replication_slots = 10\nneon.timeline_id = '2414a61ffc94e428f14b5758fe308e13'\nshared_preload_libraries = 'neon'\nsynchronous_standby_names = 'walproposer'\nneon.pageserver_connstring = 'host=127.0.0.1 port=6400'"
wal_level = replica
hot_standby = on
neon.safekeepers = '127.0.0.1:6502,127.0.0.1:6503,127.0.0.1:6501'
wal_log_hints = on
log_connections = on
shared_buffers = 32768
port = 55432
max_connections = 100
max_wal_senders = 10
listen_addresses = '0.0.0.0'
wal_sender_timeout = 0
password_encryption = md5
maintenance_work_mem = 65536
max_parallel_workers = 8
max_worker_processes = 8
neon.tenant_id = 'b0554b632bd4d547a63b86c3630317e8'
max_replication_slots = 10
neon.timeline_id = '2414a61ffc94e428f14b5758fe308e13'
shared_preload_libraries = 'neon'
synchronous_standby_names = 'walproposer'
neon.pageserver_connstring = 'host=127.0.0.1 port=6400'
test.escaping = 'here''s a backslash \\ and a quote '' and a double-quote " hooray'
"#
); );
} }

View File

@@ -29,41 +29,6 @@ These components should not have access to the private key and may only get toke
The key pair is generated once for an installation of compute/pageserver/safekeeper, e.g. by `neon_local init`. The key pair is generated once for an installation of compute/pageserver/safekeeper, e.g. by `neon_local init`.
There is currently no way to rotate the key without bringing down all components. There is currently no way to rotate the key without bringing down all components.
### Token format
The JWT tokens in Neon use RSA as the algorithm. Example:
Header:
```
{
"alg": "RS512", # RS256, RS384, or RS512
"typ": "JWT"
}
```
Payload:
```
{
"scope": "tenant", # "tenant", "pageserverapi", or "safekeeperdata"
"tenant_id": "5204921ff44f09de8094a1390a6a50f6",
}
```
Meanings of scope:
"tenant": Provides access to all data for a specific tenant
"pageserverapi": Provides blanket access to all tenants on the pageserver plus pageserver-wide APIs.
Should only be used e.g. for status check/tenant creation/list.
"safekeeperdata": Provides blanket access to all data on the safekeeper plus safekeeper-wide APIs.
Should only be used e.g. for status check.
Currently also used for connection from any pageserver to any safekeeper.
### CLI ### CLI
CLI generates a key pair during call to `neon_local init` with the following commands: CLI generates a key pair during call to `neon_local init` with the following commands:

View File

@@ -129,12 +129,13 @@ Run `poetry shell` to activate the virtual environment.
Alternatively, use `poetry run` to run a single command in the venv, e.g. `poetry run pytest`. Alternatively, use `poetry run` to run a single command in the venv, e.g. `poetry run pytest`.
### Obligatory checks ### Obligatory checks
We force code formatting via `black`, `ruff`, and type hints via `mypy`. We force code formatting via `black`, `isort` and type hints via `mypy`.
Run the following commands in the repository's root (next to `pyproject.toml`): Run the following commands in the repository's root (next to `pyproject.toml`):
```bash ```bash
poetry run isort . # Imports are reformatted
poetry run black . # All code is reformatted poetry run black . # All code is reformatted
poetry run ruff . # Python linter poetry run flake8 . # Python linter
poetry run mypy . # Ensure there are no typing errors poetry run mypy . # Ensure there are no typing errors
``` ```

View File

@@ -59,14 +59,14 @@ pub fn is_expected_io_error(e: &io::Error) -> bool {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait Handler<IO> { pub trait Handler {
/// Handle single query. /// Handle single query.
/// postgres_backend will issue ReadyForQuery after calling this (this /// postgres_backend will issue ReadyForQuery after calling this (this
/// might be not what we want after CopyData streaming, but currently we don't /// might be not what we want after CopyData streaming, but currently we don't
/// care). It will also flush out the output buffer. /// care). It will also flush out the output buffer.
async fn process_query( async fn process_query(
&mut self, &mut self,
pgb: &mut PostgresBackend<IO>, pgb: &mut PostgresBackend,
query_string: &str, query_string: &str,
) -> Result<(), QueryError>; ) -> Result<(), QueryError>;
@@ -77,7 +77,7 @@ pub trait Handler<IO> {
/// to override whole init logic in implementations. /// to override whole init logic in implementations.
fn startup( fn startup(
&mut self, &mut self,
_pgb: &mut PostgresBackend<IO>, _pgb: &mut PostgresBackend,
_sm: &FeStartupPacket, _sm: &FeStartupPacket,
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
Ok(()) Ok(())
@@ -86,7 +86,7 @@ pub trait Handler<IO> {
/// Check auth jwt /// Check auth jwt
fn check_auth_jwt( fn check_auth_jwt(
&mut self, &mut self,
_pgb: &mut PostgresBackend<IO>, _pgb: &mut PostgresBackend,
_jwt_response: &[u8], _jwt_response: &[u8],
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
Err(QueryError::Other(anyhow::anyhow!("JWT auth failed"))) Err(QueryError::Other(anyhow::anyhow!("JWT auth failed")))
@@ -115,12 +115,12 @@ pub enum ProcessMsgResult {
} }
/// Either plain TCP stream or encrypted one, implementing AsyncRead + AsyncWrite. /// Either plain TCP stream or encrypted one, implementing AsyncRead + AsyncWrite.
pub enum MaybeTlsStream<IO> { pub enum MaybeTlsStream {
Unencrypted(IO), Unencrypted(tokio::net::TcpStream),
Tls(Box<tokio_rustls::server::TlsStream<IO>>), Tls(Box<tokio_rustls::server::TlsStream<tokio::net::TcpStream>>),
} }
impl<IO: AsyncRead + AsyncWrite + Unpin> AsyncWrite for MaybeTlsStream<IO> { impl AsyncWrite for MaybeTlsStream {
fn poll_write( fn poll_write(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
@@ -147,7 +147,7 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> AsyncWrite for MaybeTlsStream<IO> {
} }
} }
} }
impl<IO: AsyncRead + AsyncWrite + Unpin> AsyncRead for MaybeTlsStream<IO> { impl AsyncRead for MaybeTlsStream {
fn poll_read( fn poll_read(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
@@ -192,13 +192,13 @@ impl fmt::Display for AuthType {
/// PostgresBackend after call to `split`. In principle we could always store a /// PostgresBackend after call to `split`. In principle we could always store a
/// pair of splitted handles, but that would force to to pay splitting price /// pair of splitted handles, but that would force to to pay splitting price
/// (Arc and kinda mutex inside polling) for all uses (e.g. pageserver). /// (Arc and kinda mutex inside polling) for all uses (e.g. pageserver).
enum MaybeWriteOnly<IO> { enum MaybeWriteOnly {
Full(Framed<MaybeTlsStream<IO>>), Full(Framed<MaybeTlsStream>),
WriteOnly(FramedWriter<MaybeTlsStream<IO>>), WriteOnly(FramedWriter<MaybeTlsStream>),
Broken, // temporary value palmed off during the split Broken, // temporary value palmed off during the split
} }
impl<IO: AsyncRead + AsyncWrite + Unpin> MaybeWriteOnly<IO> { impl MaybeWriteOnly {
async fn read_startup_message(&mut self) -> Result<Option<FeStartupPacket>, ConnectionError> { async fn read_startup_message(&mut self) -> Result<Option<FeStartupPacket>, ConnectionError> {
match self { match self {
MaybeWriteOnly::Full(framed) => framed.read_startup_message().await, MaybeWriteOnly::Full(framed) => framed.read_startup_message().await,
@@ -244,8 +244,8 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> MaybeWriteOnly<IO> {
} }
} }
pub struct PostgresBackend<IO> { pub struct PostgresBackend {
framed: MaybeWriteOnly<IO>, framed: MaybeWriteOnly,
pub state: ProtoState, pub state: ProtoState,
@@ -255,8 +255,6 @@ pub struct PostgresBackend<IO> {
pub tls_config: Option<Arc<rustls::ServerConfig>>, pub tls_config: Option<Arc<rustls::ServerConfig>>,
} }
pub type PostgresBackendTCP = PostgresBackend<tokio::net::TcpStream>;
pub fn query_from_cstring(query_string: Bytes) -> Vec<u8> { pub fn query_from_cstring(query_string: Bytes) -> Vec<u8> {
let mut query_string = query_string.to_vec(); let mut query_string = query_string.to_vec();
if let Some(ch) = query_string.last() { if let Some(ch) = query_string.last() {
@@ -273,7 +271,7 @@ fn cstr_to_str(bytes: &[u8]) -> anyhow::Result<&str> {
std::str::from_utf8(without_null).map_err(|e| e.into()) std::str::from_utf8(without_null).map_err(|e| e.into())
} }
impl PostgresBackend<tokio::net::TcpStream> { impl PostgresBackend {
pub fn new( pub fn new(
socket: tokio::net::TcpStream, socket: tokio::net::TcpStream,
auth_type: AuthType, auth_type: AuthType,
@@ -290,25 +288,6 @@ impl PostgresBackend<tokio::net::TcpStream> {
peer_addr, peer_addr,
}) })
} }
}
impl<IO: AsyncRead + AsyncWrite + Unpin> PostgresBackend<IO> {
pub fn new_from_io(
socket: IO,
peer_addr: SocketAddr,
auth_type: AuthType,
tls_config: Option<Arc<rustls::ServerConfig>>,
) -> io::Result<Self> {
let stream = MaybeTlsStream::Unencrypted(socket);
Ok(Self {
framed: MaybeWriteOnly::Full(Framed::new(stream)),
state: ProtoState::Initialization,
auth_type,
tls_config,
peer_addr,
})
}
pub fn get_peer_addr(&self) -> &SocketAddr { pub fn get_peer_addr(&self) -> &SocketAddr {
&self.peer_addr &self.peer_addr
@@ -367,14 +346,14 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> PostgresBackend<IO> {
/// to it in CopyData messages, and writes them to the connection /// to it in CopyData messages, and writes them to the connection
/// ///
/// The caller is responsible for sending CopyOutResponse and CopyDone messages. /// The caller is responsible for sending CopyOutResponse and CopyDone messages.
pub fn copyout_writer(&mut self) -> CopyDataWriter<IO> { pub fn copyout_writer(&mut self) -> CopyDataWriter {
CopyDataWriter { pgb: self } CopyDataWriter { pgb: self }
} }
/// Wrapper for run_message_loop() that shuts down socket when we are done /// Wrapper for run_message_loop() that shuts down socket when we are done
pub async fn run<F, S>( pub async fn run<F, S>(
mut self, mut self,
handler: &mut impl Handler<IO>, handler: &mut impl Handler,
shutdown_watcher: F, shutdown_watcher: F,
) -> Result<(), QueryError> ) -> Result<(), QueryError>
where where
@@ -390,7 +369,7 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> PostgresBackend<IO> {
async fn run_message_loop<F, S>( async fn run_message_loop<F, S>(
&mut self, &mut self,
handler: &mut impl Handler<IO>, handler: &mut impl Handler,
shutdown_watcher: F, shutdown_watcher: F,
) -> Result<(), QueryError> ) -> Result<(), QueryError>
where where
@@ -447,9 +426,9 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> PostgresBackend<IO> {
/// Try to upgrade MaybeTlsStream into actual TLS one, performing handshake. /// Try to upgrade MaybeTlsStream into actual TLS one, performing handshake.
async fn tls_upgrade( async fn tls_upgrade(
src: MaybeTlsStream<IO>, src: MaybeTlsStream,
tls_config: Arc<rustls::ServerConfig>, tls_config: Arc<rustls::ServerConfig>,
) -> anyhow::Result<MaybeTlsStream<IO>> { ) -> anyhow::Result<MaybeTlsStream> {
match src { match src {
MaybeTlsStream::Unencrypted(s) => { MaybeTlsStream::Unencrypted(s) => {
let acceptor = TlsAcceptor::from(tls_config); let acceptor = TlsAcceptor::from(tls_config);
@@ -487,7 +466,7 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> PostgresBackend<IO> {
/// Split off owned read part from which messages can be read in different /// Split off owned read part from which messages can be read in different
/// task/thread. /// task/thread.
pub fn split(&mut self) -> anyhow::Result<PostgresBackendReader<IO>> { pub fn split(&mut self) -> anyhow::Result<PostgresBackendReader> {
// temporary replace stream with fake to cook split one, Indiana Jones style // temporary replace stream with fake to cook split one, Indiana Jones style
match std::mem::replace(&mut self.framed, MaybeWriteOnly::Broken) { match std::mem::replace(&mut self.framed, MaybeWriteOnly::Broken) {
MaybeWriteOnly::Full(framed) => { MaybeWriteOnly::Full(framed) => {
@@ -503,7 +482,7 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> PostgresBackend<IO> {
} }
/// Join read part back. /// Join read part back.
pub fn unsplit(&mut self, reader: PostgresBackendReader<IO>) -> anyhow::Result<()> { pub fn unsplit(&mut self, reader: PostgresBackendReader) -> anyhow::Result<()> {
// temporary replace stream with fake to cook joined one, Indiana Jones style // temporary replace stream with fake to cook joined one, Indiana Jones style
match std::mem::replace(&mut self.framed, MaybeWriteOnly::Broken) { match std::mem::replace(&mut self.framed, MaybeWriteOnly::Broken) {
MaybeWriteOnly::Full(_) => { MaybeWriteOnly::Full(_) => {
@@ -520,7 +499,7 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> PostgresBackend<IO> {
/// Perform handshake with the client, transitioning to Established. /// Perform handshake with the client, transitioning to Established.
/// In case of EOF during handshake logs this, sets state to Closed and returns Ok(()). /// In case of EOF during handshake logs this, sets state to Closed and returns Ok(()).
async fn handshake(&mut self, handler: &mut impl Handler<IO>) -> Result<(), QueryError> { async fn handshake(&mut self, handler: &mut impl Handler) -> Result<(), QueryError> {
while self.state < ProtoState::Authentication { while self.state < ProtoState::Authentication {
match self.framed.read_startup_message().await? { match self.framed.read_startup_message().await? {
Some(msg) => { Some(msg) => {
@@ -586,7 +565,7 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> PostgresBackend<IO> {
/// actual startup packet. /// actual startup packet.
async fn process_startup_message( async fn process_startup_message(
&mut self, &mut self,
handler: &mut impl Handler<IO>, handler: &mut impl Handler,
msg: FeStartupPacket, msg: FeStartupPacket,
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
assert!(self.state < ProtoState::Authentication); assert!(self.state < ProtoState::Authentication);
@@ -650,7 +629,7 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> PostgresBackend<IO> {
async fn process_message( async fn process_message(
&mut self, &mut self,
handler: &mut impl Handler<IO>, handler: &mut impl Handler,
msg: FeMessage, msg: FeMessage,
unnamed_query_string: &mut Bytes, unnamed_query_string: &mut Bytes,
) -> Result<ProcessMsgResult, QueryError> { ) -> Result<ProcessMsgResult, QueryError> {
@@ -797,9 +776,9 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> PostgresBackend<IO> {
} }
} }
pub struct PostgresBackendReader<IO>(FramedReader<MaybeTlsStream<IO>>); pub struct PostgresBackendReader(FramedReader<MaybeTlsStream>);
impl<IO: AsyncRead + AsyncWrite + Unpin> PostgresBackendReader<IO> { impl PostgresBackendReader {
/// Read full message or return None if connection is cleanly closed with no /// Read full message or return None if connection is cleanly closed with no
/// unprocessed data. /// unprocessed data.
pub async fn read_message(&mut self) -> Result<Option<FeMessage>, ConnectionError> { pub async fn read_message(&mut self) -> Result<Option<FeMessage>, ConnectionError> {
@@ -833,11 +812,11 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> PostgresBackendReader<IO> {
/// messages. /// messages.
/// ///
pub struct CopyDataWriter<'a, IO> { pub struct CopyDataWriter<'a> {
pgb: &'a mut PostgresBackend<IO>, pgb: &'a mut PostgresBackend,
} }
impl<'a, IO: AsyncRead + AsyncWrite + Unpin> AsyncWrite for CopyDataWriter<'a, IO> { impl<'a> AsyncWrite for CopyDataWriter<'a> {
fn poll_write( fn poll_write(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,

View File

@@ -4,7 +4,6 @@ use postgres_backend::{AuthType, Handler, PostgresBackend, QueryError};
use pq_proto::{BeMessage, RowDescriptor}; use pq_proto::{BeMessage, RowDescriptor};
use std::io::Cursor; use std::io::Cursor;
use std::{future, sync::Arc}; use std::{future, sync::Arc};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio_postgres::config::SslMode; use tokio_postgres::config::SslMode;
use tokio_postgres::tls::MakeTlsConnect; use tokio_postgres::tls::MakeTlsConnect;
@@ -23,11 +22,11 @@ async fn make_tcp_pair() -> (TcpStream, TcpStream) {
struct TestHandler {} struct TestHandler {}
#[async_trait::async_trait] #[async_trait::async_trait]
impl<IO: AsyncRead + AsyncWrite + Unpin + Send> Handler<IO> for TestHandler { impl Handler for TestHandler {
// return single col 'hey' for any query // return single col 'hey' for any query
async fn process_query( async fn process_query(
&mut self, &mut self,
pgb: &mut PostgresBackend<IO>, pgb: &mut PostgresBackend,
_query_string: &str, _query_string: &str,
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
pgb.write_message_noflush(&BeMessage::RowDescription(&[RowDescriptor::text_col( pgb.write_message_noflush(&BeMessage::RowDescription(&[RowDescriptor::text_col(

View File

@@ -63,7 +63,10 @@ fn main() -> anyhow::Result<()> {
pg_install_dir_versioned = cwd.join("..").join("..").join(pg_install_dir_versioned); pg_install_dir_versioned = cwd.join("..").join("..").join(pg_install_dir_versioned);
} }
let pg_config_bin = pg_install_dir_versioned.join("bin").join("pg_config"); let pg_config_bin = pg_install_dir_versioned
.join(pg_version)
.join("bin")
.join("pg_config");
let inc_server_path: String = if pg_config_bin.exists() { let inc_server_path: String = if pg_config_bin.exists() {
let output = Command::new(pg_config_bin) let output = Command::new(pg_config_bin)
.arg("--includedir-server") .arg("--includedir-server")

View File

@@ -265,9 +265,11 @@ impl FeMessage {
b'c' => Ok(Some(FeMessage::CopyDone)), b'c' => Ok(Some(FeMessage::CopyDone)),
b'f' => Ok(Some(FeMessage::CopyFail)), b'f' => Ok(Some(FeMessage::CopyFail)),
b'p' => Ok(Some(FeMessage::PasswordMessage(msg))), b'p' => Ok(Some(FeMessage::PasswordMessage(msg))),
tag => Err(ProtocolError::Protocol(format!( tag => {
"unknown message tag: {tag},'{msg:?}'" return Err(ProtocolError::Protocol(format!(
))), "unknown message tag: {tag},'{msg:?}'"
)))
}
} }
} }
} }

View File

@@ -18,7 +18,6 @@ futures = { workspace = true}
jsonwebtoken.workspace = true jsonwebtoken.workspace = true
nix.workspace = true nix.workspace = true
once_cell.workspace = true once_cell.workspace = true
pin-project-lite.workspace = true
routerify.workspace = true routerify.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true

View File

@@ -9,28 +9,16 @@ use std::path::Path;
use anyhow::Result; use anyhow::Result;
use jsonwebtoken::{ use jsonwebtoken::{
decode, encode, Algorithm, Algorithm::*, DecodingKey, EncodingKey, Header, TokenData, decode, encode, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation,
Validation,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr}; use serde_with::{serde_as, DisplayFromStr};
use crate::id::TenantId; use crate::id::TenantId;
/// Algorithms accepted during validation. const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
///
/// Accept all RSA-based algorithms. We pass this list to jsonwebtoken::decode,
/// which checks that the algorithm in the token is one of these.
///
/// XXX: It also fails the validation if there are any algorithms in this list that belong
/// to different family than the token's algorithm. In other words, we can *not* list any
/// non-RSA algorithms here, or the validation always fails with InvalidAlgorithm error.
const ACCEPTED_ALGORITHMS: &[Algorithm] = &[RS256, RS384, RS512];
/// Algorithm to use when generating a new token in [`encode_from_key_file`] #[derive(Debug, Serialize, Deserialize, Clone)]
const ENCODE_ALGORITHM: Algorithm = Algorithm::RS256;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Scope { pub enum Scope {
// Provides access to all data for a specific tenant (specified in `struct Claims` below) // Provides access to all data for a specific tenant (specified in `struct Claims` below)
@@ -45,9 +33,8 @@ pub enum Scope {
SafekeeperData, SafekeeperData,
} }
/// JWT payload. See docs/authentication.md for the format
#[serde_as] #[serde_as]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims { pub struct Claims {
#[serde(default)] #[serde(default)]
#[serde_as(as = "Option<DisplayFromStr>")] #[serde_as(as = "Option<DisplayFromStr>")]
@@ -68,8 +55,7 @@ pub struct JwtAuth {
impl JwtAuth { impl JwtAuth {
pub fn new(decoding_key: DecodingKey) -> Self { pub fn new(decoding_key: DecodingKey) -> Self {
let mut validation = Validation::default(); let mut validation = Validation::new(JWT_ALGORITHM);
validation.algorithms = ACCEPTED_ALGORITHMS.into();
// The default 'required_spec_claims' is 'exp'. But we don't want to require // The default 'required_spec_claims' is 'exp'. But we don't want to require
// expiration. // expiration.
validation.required_spec_claims = [].into(); validation.required_spec_claims = [].into();
@@ -100,113 +86,5 @@ impl std::fmt::Debug for JwtAuth {
// this function is used only for testing purposes in CLI e g generate tokens during init // this function is used only for testing purposes in CLI e g generate tokens during init
pub fn encode_from_key_file(claims: &Claims, key_data: &[u8]) -> Result<String> { pub fn encode_from_key_file(claims: &Claims, key_data: &[u8]) -> Result<String> {
let key = EncodingKey::from_rsa_pem(key_data)?; let key = EncodingKey::from_rsa_pem(key_data)?;
Ok(encode(&Header::new(ENCODE_ALGORITHM), claims, &key)?) Ok(encode(&Header::new(JWT_ALGORITHM), claims, &key)?)
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
// generated with:
//
// openssl genpkey -algorithm rsa -out storage-auth-priv.pem
// openssl pkey -in storage-auth-priv.pem -pubout -out storage-auth-pub.pem
const TEST_PUB_KEY_RSA: &[u8] = br#"
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy6OZ+/kQXcueVJA/KTzO
v4ljxylc/Kcb0sXWuXg1GB8k3nDA1gK66LFYToH0aTnqrnqG32Vu6wrhwuvqsZA7
jQvP0ZePAbWhpEqho7EpNunDPcxZ/XDy5TQlB1P58F9I3lkJXDC+DsHYLuuzwhAv
vo2MtWRdYlVHblCVLyZtANHhUMp2HUhgjHnJh5UrLIKOl4doCBxkM3rK0wjKsNCt
M92PCR6S9rvYzldfeAYFNppBkEQrXt2CgUqZ4KaS4LXtjTRUJxljijA4HWffhxsr
euRu3ufq8kVqie7fum0rdZZSkONmce0V0LesQ4aE2jB+2Sn48h6jb4dLXGWdq8TV
wQIDAQAB
-----END PUBLIC KEY-----
"#;
const TEST_PRIV_KEY_RSA: &[u8] = br#"
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLo5n7+RBdy55U
kD8pPM6/iWPHKVz8pxvSxda5eDUYHyTecMDWArrosVhOgfRpOequeobfZW7rCuHC
6+qxkDuNC8/Rl48BtaGkSqGjsSk26cM9zFn9cPLlNCUHU/nwX0jeWQlcML4Owdgu
67PCEC++jYy1ZF1iVUduUJUvJm0A0eFQynYdSGCMecmHlSssgo6Xh2gIHGQzesrT
CMqw0K0z3Y8JHpL2u9jOV194BgU2mkGQRCte3YKBSpngppLgte2NNFQnGWOKMDgd
Z9+HGyt65G7e5+ryRWqJ7t+6bSt1llKQ42Zx7RXQt6xDhoTaMH7ZKfjyHqNvh0tc
ZZ2rxNXBAgMBAAECggEAVz3u4Wlx3o02dsoZlSQs+xf0PEX3RXKeU+1YMbtTG9Nz
6yxpIQaoZrpbt76rJE2gwkFR+PEu1NmjoOuLb6j4KlQuI4AHz1auOoGSwFtM6e66
K4aZ4x95oEJ3vqz2fkmEIWYJwYpMUmwvnuJx76kZm0xvROMLsu4QHS2+zCVtO5Tr
hvS05IMVuZ2TdQBZw0+JaFdwXbgDjQnQGY5n9MoTWSx1a4s/FF4Eby65BbDutcpn
Vt3jQAOmO1X2kbPeWSGuPJRzyUs7Kg8qfeglBIR3ppGP3vPYAdWX+ho00bmsVkSp
Q8vjul6C3WiM+kjwDxotHSDgbl/xldAl7OqPh0bfAQKBgQDnycXuq14Vg8nZvyn9
rTnvucO8RBz5P6G+FZ+44cAS2x79+85onARmMnm+9MKYLSMo8fOvsK034NDI68XM
04QQ/vlfouvFklMTGJIurgEImTZbGCmlMYCvFyIxaEWixon8OpeI4rFe4Hmbiijh
PxhxWg221AwvBS2sco8J/ylEkQKBgQDg6Rh2QYb/j0Wou1rJPbuy3NhHofd5Rq35
4YV3f2lfVYcPrgRhwe3T9SVII7Dx8LfwzsX5TAlf48ESlI3Dzv40uOCDM+xdtBRI
r96SfSm+jup6gsXU3AsdNkrRK3HoOG9Z/TkrUp213QAIlVnvIx65l4ckFMlpnPJ0
lo1LDXZWMQKBgFArzjZ7N5OhfdO+9zszC3MLgdRAivT7OWqR+CjujIz5FYMr8Xzl
WfAvTUTrS9Nu6VZkObFvHrrRG+YjBsuN7YQjbQXTSFGSBwH34bgbn2fl9pMTjHQC
50uoaL9GHa/rlBaV/YvvPQJgCi/uXa1rMX0jdNLkDULGO8IF7cu7Yf7BAoGBAIUU
J29BkpmAst0GDs/ogTlyR18LTR0rXyHt+UUd1MGeH859TwZw80JpWWf4BmkB4DTS
hH3gKePdJY7S65ci0XNsuRupC4DeXuorde0DtkGU2tUmr9wlX0Ynq9lcdYfMbMa4
eK1TsxG69JwfkxlWlIWITWRiEFM3lJa7xlrUWmLhAoGAFpKWF/hn4zYg3seU9gai
EYHKSbhxA4mRb+F0/9IlCBPMCqFrL5yftUsYIh2XFKn8+QhO97Nmk8wJSK6TzQ5t
ZaSRmgySrUUhx4nZ/MgqWCFv8VUbLM5MBzwxPKhXkSTfR4z2vLYLJwVY7Tb4kZtp
8ismApXVGHpOCstzikV9W7k=
-----END PRIVATE KEY-----
"#;
#[test]
fn test_decode() -> Result<(), anyhow::Error> {
let expected_claims = Claims {
tenant_id: Some(TenantId::from_str("3d1f7595b468230304e0b73cecbcb081")?),
scope: Scope::Tenant,
};
// Here are tokens containing the following payload, signed using TEST_PRIV_KEY_RSA
// using RS512, RS384 and RS256 algorithms:
//
// ```
// {
// "scope": "tenant",
// "tenant_id": "3d1f7595b468230304e0b73cecbcb081",
// "iss": "neon.controlplane",
// "exp": 1709200879,
// "iat": 1678442479
// }
// ```
//
// These were encoded with the online debugger at https://jwt.io
//
let encoded_rs512 = "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InRlbmFudCIsInRlbmFudF9pZCI6IjNkMWY3NTk1YjQ2ODIzMDMwNGUwYjczY2VjYmNiMDgxIiwiaXNzIjoibmVvbi5jb250cm9scGxhbmUiLCJleHAiOjE3MDkyMDA4NzksImlhdCI6MTY3ODQ0MjQ3OX0.QmqfteDQmDGoxQ5EFkasbt35Lx0W0Nh63muQnYZvFq93DSh4ZbOG9Mc4yaiXZoiS5HgeKtFKv3mbWkDqjz3En06aY17hWwguBtAsGASX48lYeCPADYGlGAuaWnOnVRwe3iiOC7tvPFvwX_45S84X73sNUXyUiXv6nLdcDqVXudtNrGST_DnZDnjuUJX11w7sebtKqQQ8l9-iGHiXOl5yevpMCoB1OcTWcT6DfDtffoNuMHDC3fyhmEGG5oKAt1qBybqAIiyC9-UBAowRZXhdfxrzUl-I9jzKWvk85c5ulhVRwbPeP6TTTlPKwFzBNHg1i2U-1GONew5osQ3aoptwsA";
let encoded_rs384 = "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InRlbmFudCIsInRlbmFudF9pZCI6IjNkMWY3NTk1YjQ2ODIzMDMwNGUwYjczY2VjYmNiMDgxIiwiaXNzIjoibmVvbi5jb250cm9scGxhbmUiLCJleHAiOjE3MDkyMDA4NzksImlhdCI6MTY3ODQ0MjQ3OX0.qqk4nkxKzOJP38c_g57_w_SfdQVmCsDT_bsLmdFj_N6LIB22gr6U6_P_5mvk3pIAsp0VCTDwPrCU908TxqjibEkwvQoJwbogHamSGHpD7eJBxGblSnA-Nr3MlEMxpFtec8QokSm6C5mH7DoBYjB2xzeOlxAmpR2GAzInKiMkU4kZ_OcqqrmVcMXY_6VnbxZWMekuw56zE1-PP_qNF1HvYOH-P08ONP8qdo5UPtBG7QBEFlCqZXJZCFihQaI4Vzil9rDuZGCm3I7xQJ8-yh1PX3BTbGo8EzqLdRyBeTpr08UTuRbp_MJDWevHpP3afvJetAItqZXIoZQrbJjcByHqKw";
let encoded_rs256 = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InRlbmFudCIsInRlbmFudF9pZCI6IjNkMWY3NTk1YjQ2ODIzMDMwNGUwYjczY2VjYmNiMDgxIiwiaXNzIjoibmVvbi5jb250cm9scGxhbmUiLCJleHAiOjE3MDkyMDA4NzksImlhdCI6MTY3ODQ0MjQ3OX0.dF2N9KXG8ftFKHYbd5jQtXMQqv0Ej8FISGp1b_dmqOCotXj5S1y2AWjwyB_EXHM77JXfbEoJPAPrFFBNfd8cWtkCSTvpxWoHaecGzegDFGv5ZSc5AECFV1Daahc3PI3jii9wEiGkFOiwiBNfZ5INomOAsV--XXxlqIwKbTcgSYI7lrOTfecXAbAHiMKQlQYiIBSGnytRCgafhRkyGzPAL8ismthFJ9RHfeejyskht-9GbVHURw02bUyijuHEulpf9eEY3ZiB28de6jnCdU7ftIYaUMaYWt0nZQGkzxKPSfSLZNy14DTOYLDS04DVstWQPqnCUW_ojg0wJETOOfo9Zw";
// Check that RS512, RS384 and RS256 tokens can all be validated
let auth = JwtAuth::new(DecodingKey::from_rsa_pem(TEST_PUB_KEY_RSA)?);
for encoded in [encoded_rs512, encoded_rs384, encoded_rs256] {
let claims_from_token = auth.decode(encoded)?.claims;
assert_eq!(claims_from_token, expected_claims);
}
Ok(())
}
#[test]
fn test_encode() -> Result<(), anyhow::Error> {
let claims = Claims {
tenant_id: Some(TenantId::from_str("3d1f7595b468230304e0b73cecbcb081")?),
scope: Scope::Tenant,
};
let encoded = encode_from_key_file(&claims, TEST_PRIV_KEY_RSA)?;
// decode it back
let auth = JwtAuth::new(DecodingKey::from_rsa_pem(TEST_PUB_KEY_RSA)?);
let decoded = auth.decode(&encoded)?;
assert_eq!(decoded.claims, claims);
Ok(())
}
} }

View File

@@ -11,7 +11,7 @@ where
P: AsRef<Path>, P: AsRef<Path>,
{ {
fn is_empty_dir(&self) -> io::Result<bool> { fn is_empty_dir(&self) -> io::Result<bool> {
Ok(fs::read_dir(self)?.next().is_none()) Ok(fs::read_dir(self)?.into_iter().next().is_none())
} }
} }

View File

@@ -49,8 +49,6 @@ pub mod fs_ext;
pub mod history_buffer; pub mod history_buffer;
pub mod measured_stream;
/// use with fail::cfg("$name", "return(2000)") /// use with fail::cfg("$name", "return(2000)")
#[macro_export] #[macro_export]
macro_rules! failpoint_sleep_millis_async { macro_rules! failpoint_sleep_millis_async {

View File

@@ -1,77 +0,0 @@
use pin_project_lite::pin_project;
use std::pin::Pin;
use std::{io, task};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
pin_project! {
/// This stream tracks all writes and calls user provided
/// callback when the underlying stream is flushed.
pub struct MeasuredStream<S, R, W> {
#[pin]
stream: S,
write_count: usize,
inc_read_count: R,
inc_write_count: W,
}
}
impl<S, R, W> MeasuredStream<S, R, W> {
pub fn new(stream: S, inc_read_count: R, inc_write_count: W) -> Self {
Self {
stream,
write_count: 0,
inc_read_count,
inc_write_count,
}
}
}
impl<S: AsyncRead + Unpin, R: FnMut(usize), W> AsyncRead for MeasuredStream<S, R, W> {
fn poll_read(
self: Pin<&mut Self>,
context: &mut task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> task::Poll<io::Result<()>> {
let this = self.project();
let filled = buf.filled().len();
this.stream.poll_read(context, buf).map_ok(|()| {
let cnt = buf.filled().len() - filled;
// Increment the read count.
(this.inc_read_count)(cnt);
})
}
}
impl<S: AsyncWrite + Unpin, R, W: FnMut(usize)> AsyncWrite for MeasuredStream<S, R, W> {
fn poll_write(
self: Pin<&mut Self>,
context: &mut task::Context<'_>,
buf: &[u8],
) -> task::Poll<io::Result<usize>> {
let this = self.project();
this.stream.poll_write(context, buf).map_ok(|cnt| {
// Increment the write count.
*this.write_count += cnt;
cnt
})
}
fn poll_flush(
self: Pin<&mut Self>,
context: &mut task::Context<'_>,
) -> task::Poll<io::Result<()>> {
let this = self.project();
this.stream.poll_flush(context).map_ok(|()| {
// Call the user provided callback and reset the write count.
(this.inc_write_count)(*this.write_count);
*this.write_count = 0;
})
}
fn poll_shutdown(
self: Pin<&mut Self>,
context: &mut task::Context<'_>,
) -> task::Poll<io::Result<()>> {
self.project().stream.poll_shutdown(context)
}
}

View File

@@ -123,22 +123,6 @@ static REMOTE_PHYSICAL_SIZE: Lazy<UIntGaugeVec> = Lazy::new(|| {
.expect("failed to define a metric") .expect("failed to define a metric")
}); });
pub static REMOTE_ONDEMAND_DOWNLOADED_LAYERS: Lazy<IntCounter> = Lazy::new(|| {
register_int_counter!(
"pageserver_remote_ondemand_downloaded_layers_total",
"Total on-demand downloaded layers"
)
.unwrap()
});
pub static REMOTE_ONDEMAND_DOWNLOADED_BYTES: Lazy<IntCounter> = Lazy::new(|| {
register_int_counter!(
"pageserver_remote_ondemand_downloaded_bytes_total",
"Total bytes of layers on-demand downloaded",
)
.unwrap()
});
static CURRENT_LOGICAL_SIZE: Lazy<UIntGaugeVec> = Lazy::new(|| { static CURRENT_LOGICAL_SIZE: Lazy<UIntGaugeVec> = Lazy::new(|| {
register_uint_gauge_vec!( register_uint_gauge_vec!(
"pageserver_current_logical_size", "pageserver_current_logical_size",

View File

@@ -12,7 +12,7 @@
use anyhow::Context; use anyhow::Context;
use bytes::Buf; use bytes::Buf;
use bytes::Bytes; use bytes::Bytes;
use futures::Stream; use futures::{Stream, StreamExt};
use pageserver_api::models::TenantState; use pageserver_api::models::TenantState;
use pageserver_api::models::{ use pageserver_api::models::{
PagestreamBeMessage, PagestreamDbSizeRequest, PagestreamDbSizeResponse, PagestreamBeMessage, PagestreamDbSizeRequest, PagestreamDbSizeResponse,
@@ -20,7 +20,6 @@ use pageserver_api::models::{
PagestreamFeMessage, PagestreamGetPageRequest, PagestreamGetPageResponse, PagestreamFeMessage, PagestreamGetPageRequest, PagestreamGetPageResponse,
PagestreamNblocksRequest, PagestreamNblocksResponse, PagestreamNblocksRequest, PagestreamNblocksResponse,
}; };
use postgres_backend::PostgresBackendTCP;
use postgres_backend::{self, is_expected_io_error, AuthType, PostgresBackend, QueryError}; use postgres_backend::{self, is_expected_io_error, AuthType, PostgresBackend, QueryError};
use pq_proto::framed::ConnectionError; use pq_proto::framed::ConnectionError;
use pq_proto::FeStartupPacket; use pq_proto::FeStartupPacket;
@@ -31,7 +30,6 @@ use std::str;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio_util::io::StreamReader;
use tracing::*; use tracing::*;
use utils::id::ConnectionId; use utils::id::ConnectionId;
use utils::{ use utils::{
@@ -56,7 +54,7 @@ use crate::trace::Tracer;
use postgres_ffi::pg_constants::DEFAULTTABLESPACE_OID; use postgres_ffi::pg_constants::DEFAULTTABLESPACE_OID;
use postgres_ffi::BLCKSZ; use postgres_ffi::BLCKSZ;
fn copyin_stream(pgb: &mut PostgresBackendTCP) -> impl Stream<Item = io::Result<Bytes>> + '_ { fn copyin_stream(pgb: &mut PostgresBackend) -> impl Stream<Item = io::Result<Bytes>> + '_ {
async_stream::try_stream! { async_stream::try_stream! {
loop { loop {
let msg = tokio::select! { let msg = tokio::select! {
@@ -116,49 +114,6 @@ fn copyin_stream(pgb: &mut PostgresBackendTCP) -> impl Stream<Item = io::Result<
} }
} }
/// Read the end of a tar archive.
///
/// A tar archive normally ends with two consecutive blocks of zeros, 512 bytes each.
/// `tokio_tar` already read the first such block. Read the second all-zeros block,
/// and check that there is no more data after the EOF marker.
///
/// XXX: Currently, any trailing data after the EOF marker prints a warning.
/// Perhaps it should be a hard error?
async fn read_tar_eof(mut reader: (impl tokio::io::AsyncRead + Unpin)) -> anyhow::Result<()> {
use tokio::io::AsyncReadExt;
let mut buf = [0u8; 512];
// Read the all-zeros block, and verify it
let mut total_bytes = 0;
while total_bytes < 512 {
let nbytes = reader.read(&mut buf[total_bytes..]).await?;
total_bytes += nbytes;
if nbytes == 0 {
break;
}
}
if total_bytes < 512 {
anyhow::bail!("incomplete or invalid tar EOF marker");
}
if !buf.iter().all(|&x| x == 0) {
anyhow::bail!("invalid tar EOF marker");
}
// Drain any data after the EOF marker
let mut trailing_bytes = 0;
loop {
let nbytes = reader.read(&mut buf).await?;
trailing_bytes += nbytes;
if nbytes == 0 {
break;
}
}
if trailing_bytes > 0 {
warn!("ignored {trailing_bytes} unexpected bytes after the tar archive");
}
Ok(())
}
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
/// ///
@@ -333,7 +288,7 @@ impl PageServerHandler {
#[instrument(skip(self, pgb, ctx))] #[instrument(skip(self, pgb, ctx))]
async fn handle_pagerequests( async fn handle_pagerequests(
&self, &self,
pgb: &mut PostgresBackendTCP, pgb: &mut PostgresBackend,
tenant_id: TenantId, tenant_id: TenantId,
timeline_id: TimelineId, timeline_id: TimelineId,
ctx: RequestContext, ctx: RequestContext,
@@ -437,7 +392,7 @@ impl PageServerHandler {
#[instrument(skip(self, pgb, ctx))] #[instrument(skip(self, pgb, ctx))]
async fn handle_import_basebackup( async fn handle_import_basebackup(
&self, &self,
pgb: &mut PostgresBackendTCP, pgb: &mut PostgresBackend,
tenant_id: TenantId, tenant_id: TenantId,
timeline_id: TimelineId, timeline_id: TimelineId,
base_lsn: Lsn, base_lsn: Lsn,
@@ -466,14 +421,19 @@ impl PageServerHandler {
pgb.write_message_noflush(&BeMessage::CopyInResponse)?; pgb.write_message_noflush(&BeMessage::CopyInResponse)?;
pgb.flush().await?; pgb.flush().await?;
let copyin_reader = StreamReader::new(copyin_stream(pgb)); let mut copyin_stream = Box::pin(copyin_stream(pgb));
tokio::pin!(copyin_reader);
timeline timeline
.import_basebackup_from_tar(&mut copyin_reader, base_lsn, &ctx) .import_basebackup_from_tar(&mut copyin_stream, base_lsn, &ctx)
.await?; .await?;
// Read the end of the tar archive. // Drain the rest of the Copy data
read_tar_eof(copyin_reader).await?; let mut bytes_after_tar = 0;
while let Some(bytes) = copyin_stream.next().await {
bytes_after_tar += bytes?.len();
}
if bytes_after_tar > 0 {
warn!("ignored {bytes_after_tar} unexpected bytes after the tar archive");
}
// TODO check checksum // TODO check checksum
// Meanwhile you can verify client-side by taking fullbackup // Meanwhile you can verify client-side by taking fullbackup
@@ -488,7 +448,7 @@ impl PageServerHandler {
#[instrument(skip(self, pgb, ctx))] #[instrument(skip(self, pgb, ctx))]
async fn handle_import_wal( async fn handle_import_wal(
&self, &self,
pgb: &mut PostgresBackendTCP, pgb: &mut PostgresBackend,
tenant_id: TenantId, tenant_id: TenantId,
timeline_id: TimelineId, timeline_id: TimelineId,
start_lsn: Lsn, start_lsn: Lsn,
@@ -512,13 +472,19 @@ impl PageServerHandler {
info!("importing wal"); info!("importing wal");
pgb.write_message_noflush(&BeMessage::CopyInResponse)?; pgb.write_message_noflush(&BeMessage::CopyInResponse)?;
pgb.flush().await?; pgb.flush().await?;
let copyin_reader = StreamReader::new(copyin_stream(pgb)); let mut copyin_stream = Box::pin(copyin_stream(pgb));
tokio::pin!(copyin_reader); let mut reader = tokio_util::io::StreamReader::new(&mut copyin_stream);
import_wal_from_tar(&timeline, &mut copyin_reader, start_lsn, end_lsn, &ctx).await?; import_wal_from_tar(&timeline, &mut reader, start_lsn, end_lsn, &ctx).await?;
info!("wal import complete"); info!("wal import complete");
// Read the end of the tar archive. // Drain the rest of the Copy data
read_tar_eof(copyin_reader).await?; let mut bytes_after_tar = 0;
while let Some(bytes) = copyin_stream.next().await {
bytes_after_tar += bytes?.len();
}
if bytes_after_tar > 0 {
warn!("ignored {bytes_after_tar} unexpected bytes after the tar archive");
}
// TODO Does it make sense to overshoot? // TODO Does it make sense to overshoot?
if timeline.get_last_record_lsn() < end_lsn { if timeline.get_last_record_lsn() < end_lsn {
@@ -693,7 +659,7 @@ impl PageServerHandler {
#[instrument(skip(self, pgb, ctx))] #[instrument(skip(self, pgb, ctx))]
async fn handle_basebackup_request( async fn handle_basebackup_request(
&mut self, &mut self,
pgb: &mut PostgresBackendTCP, pgb: &mut PostgresBackend,
tenant_id: TenantId, tenant_id: TenantId,
timeline_id: TimelineId, timeline_id: TimelineId,
lsn: Option<Lsn>, lsn: Option<Lsn>,
@@ -757,10 +723,10 @@ impl PageServerHandler {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl postgres_backend::Handler<tokio::net::TcpStream> for PageServerHandler { impl postgres_backend::Handler for PageServerHandler {
fn check_auth_jwt( fn check_auth_jwt(
&mut self, &mut self,
_pgb: &mut PostgresBackendTCP, _pgb: &mut PostgresBackend,
jwt_response: &[u8], jwt_response: &[u8],
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
// this unwrap is never triggered, because check_auth_jwt only called when auth_type is NeonJWT // this unwrap is never triggered, because check_auth_jwt only called when auth_type is NeonJWT
@@ -788,7 +754,7 @@ impl postgres_backend::Handler<tokio::net::TcpStream> for PageServerHandler {
fn startup( fn startup(
&mut self, &mut self,
_pgb: &mut PostgresBackendTCP, _pgb: &mut PostgresBackend,
_sm: &FeStartupPacket, _sm: &FeStartupPacket,
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
Ok(()) Ok(())
@@ -796,7 +762,7 @@ impl postgres_backend::Handler<tokio::net::TcpStream> for PageServerHandler {
async fn process_query( async fn process_query(
&mut self, &mut self,
pgb: &mut PostgresBackendTCP, pgb: &mut PostgresBackend,
query_string: &str, query_string: &str,
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
let ctx = self.connection_ctx.attached_child(); let ctx = self.connection_ctx.attached_child();

View File

@@ -12,7 +12,9 @@
//! //!
use anyhow::{bail, Context}; use anyhow::{bail, Context};
use bytes::Bytes;
use futures::FutureExt; use futures::FutureExt;
use futures::Stream;
use pageserver_api::models::TimelineState; use pageserver_api::models::TimelineState;
use remote_storage::DownloadError; use remote_storage::DownloadError;
use remote_storage::GenericRemoteStorage; use remote_storage::GenericRemoteStorage;
@@ -237,13 +239,14 @@ impl UninitializedTimeline<'_> {
/// Prepares timeline data by loading it from the basebackup archive. /// Prepares timeline data by loading it from the basebackup archive.
pub async fn import_basebackup_from_tar( pub async fn import_basebackup_from_tar(
self, self,
copyin_read: &mut (impl tokio::io::AsyncRead + Send + Sync + Unpin), copyin_stream: &mut (impl Stream<Item = io::Result<Bytes>> + Sync + Send + Unpin),
base_lsn: Lsn, base_lsn: Lsn,
ctx: &RequestContext, ctx: &RequestContext,
) -> anyhow::Result<Arc<Timeline>> { ) -> anyhow::Result<Arc<Timeline>> {
let raw_timeline = self.raw_timeline()?; let raw_timeline = self.raw_timeline()?;
import_datadir::import_basebackup_from_tar(raw_timeline, copyin_read, base_lsn, ctx) let mut reader = tokio_util::io::StreamReader::new(copyin_stream);
import_datadir::import_basebackup_from_tar(raw_timeline, &mut reader, base_lsn, ctx)
.await .await
.context("Failed to import basebackup")?; .context("Failed to import basebackup")?;
@@ -1240,8 +1243,11 @@ impl Tenant {
"Cannot run GC iteration on inactive tenant" "Cannot run GC iteration on inactive tenant"
); );
self.gc_iteration_internal(target_timeline_id, horizon, pitr, ctx) let gc_result = self
.await .gc_iteration_internal(target_timeline_id, horizon, pitr, ctx)
.await;
gc_result
} }
/// Perform one compaction iteration. /// Perform one compaction iteration.
@@ -3170,44 +3176,6 @@ mod tests {
} }
*/ */
#[tokio::test]
async fn test_get_branchpoints_from_an_inactive_timeline() -> anyhow::Result<()> {
let (tenant, ctx) =
TenantHarness::create("test_get_branchpoints_from_an_inactive_timeline")?
.load()
.await;
let tline = tenant
.create_empty_timeline(TIMELINE_ID, Lsn(0), DEFAULT_PG_VERSION, &ctx)?
.initialize(&ctx)?;
make_some_layers(tline.as_ref(), Lsn(0x20)).await?;
tenant
.branch_timeline(&tline, NEW_TIMELINE_ID, Some(Lsn(0x40)), &ctx)
.await?;
let newtline = tenant
.get_timeline(NEW_TIMELINE_ID, true)
.expect("Should have a local timeline");
make_some_layers(newtline.as_ref(), Lsn(0x60)).await?;
tline.set_state(TimelineState::Broken);
tenant
.gc_iteration(Some(TIMELINE_ID), 0x10, Duration::ZERO, &ctx)
.await?;
assert_eq!(
newtline.get(*TEST_KEY, Lsn(0x50), &ctx).await?,
TEST_IMG(&format!("foo at {}", Lsn(0x40)))
);
let branchpoints = &tline.gc_info.read().unwrap().retain_lsns;
assert_eq!(branchpoints.len(), 1);
assert_eq!(branchpoints[0], Lsn(0x40));
Ok(())
}
#[tokio::test] #[tokio::test]
async fn test_retain_data_in_parent_which_is_needed_for_child() -> anyhow::Result<()> { async fn test_retain_data_in_parent_which_is_needed_for_child() -> anyhow::Result<()> {
let (tenant, ctx) = let (tenant, ctx) =

View File

@@ -51,6 +51,9 @@ where
/// ///
/// A "cursor" for efficiently reading multiple pages from a BlockReader /// A "cursor" for efficiently reading multiple pages from a BlockReader
/// ///
/// A cursor caches the last accessed page, allowing for faster access if the
/// same block is accessed repeatedly.
///
/// You can access the last page with `*cursor`. 'read_blk' returns 'self', so /// You can access the last page with `*cursor`. 'read_blk' returns 'self', so
/// that in many cases you can use a BlockCursor as a drop-in replacement for /// that in many cases you can use a BlockCursor as a drop-in replacement for
/// the underlying BlockReader. For example: /// the underlying BlockReader. For example:
@@ -70,6 +73,8 @@ where
R: BlockReader, R: BlockReader,
{ {
reader: R, reader: R,
/// last accessed page
cache: (u32, [u8; PAGE_SZ]),
} }
impl<R> BlockCursor<R> impl<R> BlockCursor<R>
@@ -77,13 +82,38 @@ where
R: BlockReader, R: BlockReader,
{ {
pub fn new(reader: R) -> Self { pub fn new(reader: R) -> Self {
BlockCursor { reader } BlockCursor {
reader,
cache: (u32::MAX, [0u8; PAGE_SZ]),
}
} }
pub fn read_blk(&mut self, blknum: u32) -> Result<R::BlockLease, std::io::Error> { pub fn read_blk(&mut self, blknum: u32) -> Result<&Self, std::io::Error> {
self.reader.read_blk(blknum) // Fast return if this is the same block as before
if self.cache.0 == blknum {
return Ok(self);
}
// Read the block from the underlying reader, and cache it
let buf = self.reader.read_blk(blknum)?;
self.cache.0 = blknum;
self.cache.1[..].copy_from_slice(&buf[..]);
Ok(self)
} }
} }
impl<R> Deref for BlockCursor<R>
where
R: BlockReader,
{
type Target = [u8; PAGE_SZ];
fn deref(&self) -> &<Self as Deref>::Target {
&self.cache.1
}
}
static NEXT_ID: AtomicU64 = AtomicU64::new(1); static NEXT_ID: AtomicU64 = AtomicU64::new(1);
/// An adapter for reading a (virtual) file using the page cache. /// An adapter for reading a (virtual) file using the page cache.

View File

@@ -2,7 +2,9 @@
//! used to keep in-memory layers spilled on disk. //! used to keep in-memory layers spilled on disk.
use crate::config::PageServerConf; use crate::config::PageServerConf;
use crate::page_cache::{self, ReadBufResult, WriteBufResult, PAGE_SZ}; use crate::page_cache;
use crate::page_cache::PAGE_SZ;
use crate::page_cache::{ReadBufResult, WriteBufResult};
use crate::tenant::blob_io::BlobWriter; use crate::tenant::blob_io::BlobWriter;
use crate::tenant::block_io::BlockReader; use crate::tenant::block_io::BlockReader;
use crate::virtual_file::VirtualFile; use crate::virtual_file::VirtualFile;

View File

@@ -154,7 +154,11 @@ where
expected: &Arc<L>, expected: &Arc<L>,
new: Arc<L>, new: Arc<L>,
) -> anyhow::Result<Replacement<Arc<L>>> { ) -> anyhow::Result<Replacement<Arc<L>>> {
fail::fail_point!("layermap-replace-notfound", |_| Ok(Replacement::NotFound)); fail::fail_point!("layermap-replace-notfound", |_| Ok(
// this is not what happens if an L0 layer was not found a anyhow error but perhaps
// that should be changed. this is good enough to show a replacement failure.
Replacement::NotFound
));
self.layer_map.replace_historic_noflush(expected, new) self.layer_map.replace_historic_noflush(expected, new)
} }
@@ -336,15 +340,12 @@ where
let l0_index = if expected_l0 { let l0_index = if expected_l0 {
// find the index in case replace worked, we need to replace that as well // find the index in case replace worked, we need to replace that as well
let pos = self Some(
.l0_delta_layers self.l0_delta_layers
.iter() .iter()
.position(|slot| Self::compare_arced_layers(slot, expected)); .position(|slot| Self::compare_arced_layers(slot, expected))
.ok_or_else(|| anyhow::anyhow!("existing l0 delta layer was not found"))?,
if pos.is_none() { )
return Ok(Replacement::NotFound);
}
pos
} else { } else {
None None
}; };
@@ -803,26 +804,6 @@ mod tests {
) )
} }
#[test]
fn replacing_missing_l0_is_notfound() {
// original impl had an oversight, and L0 was an anyhow::Error. anyhow::Error should
// however only happen for precondition failures.
let layer = "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000053423C21-0000000053424D69";
let layer = LayerFileName::from_str(layer).unwrap();
let layer = LayerDescriptor::from(layer);
// same skeletan construction; see scenario below
let not_found: Arc<dyn Layer> = Arc::new(layer.clone());
let new_version: Arc<dyn Layer> = Arc::new(layer);
let mut map = LayerMap::default();
let res = map.batch_update().replace_historic(&not_found, new_version);
assert!(matches!(res, Ok(Replacement::NotFound)), "{res:?}");
}
fn l0_delta_layers_updated_scenario(layer_name: &str, expected_l0: bool) { fn l0_delta_layers_updated_scenario(layer_name: &str, expected_l0: bool) {
let name = LayerFileName::from_str(layer_name).unwrap(); let name = LayerFileName::from_str(layer_name).unwrap();
let skeleton = LayerDescriptor::from(name); let skeleton = LayerDescriptor::from(name);
@@ -832,8 +813,7 @@ mod tests {
let mut map = LayerMap::default(); let mut map = LayerMap::default();
// two disjoint Arcs in different lifecycle phases. even if it seems they must be the // two disjoint Arcs in different lifecycle phases.
// same layer, we use LayerMap::compare_arced_layers as the identity of layers.
assert!(!LayerMap::compare_arced_layers(&remote, &downloaded)); assert!(!LayerMap::compare_arced_layers(&remote, &downloaded));
let expected_in_counts = (1, usize::from(expected_l0)); let expected_in_counts = (1, usize::from(expected_l0));

View File

@@ -218,10 +218,9 @@ use tracing::{debug, info, warn};
use tracing::{info_span, Instrument}; use tracing::{info_span, Instrument};
use utils::lsn::Lsn; use utils::lsn::Lsn;
use crate::metrics::{ use crate::metrics::RemoteOpFileKind;
MeasureRemoteOp, RemoteOpFileKind, RemoteOpKind, RemoteTimelineClientMetrics, use crate::metrics::RemoteOpKind;
REMOTE_ONDEMAND_DOWNLOADED_BYTES, REMOTE_ONDEMAND_DOWNLOADED_LAYERS, use crate::metrics::{MeasureRemoteOp, RemoteTimelineClientMetrics};
};
use crate::tenant::remote_timeline_client::index::LayerFileMetadata; use crate::tenant::remote_timeline_client::index::LayerFileMetadata;
use crate::{ use crate::{
config::PageServerConf, config::PageServerConf,
@@ -447,10 +446,6 @@ impl RemoteTimelineClient {
); );
} }
} }
REMOTE_ONDEMAND_DOWNLOADED_LAYERS.inc();
REMOTE_ONDEMAND_DOWNLOADED_BYTES.inc_by(downloaded_size);
Ok(downloaded_size) Ok(downloaded_size)
} }

View File

@@ -2715,22 +2715,10 @@ impl Timeline {
) -> Result<HashMap<LayerFileName, LayerFileMetadata>, PageReconstructError> { ) -> Result<HashMap<LayerFileName, LayerFileMetadata>, PageReconstructError> {
let timer = self.metrics.create_images_time_histo.start_timer(); let timer = self.metrics.create_images_time_histo.start_timer();
let mut image_layers: Vec<ImageLayer> = Vec::new(); let mut image_layers: Vec<ImageLayer> = Vec::new();
// We need to avoid holes between generated image layers.
// Otherwise LayerMap::image_layer_exists will return false if key range of some layer is covered by more than one
// image layer with hole between them. In this case such layer can not be utilized by GC.
//
// How such hole between partitions can appear?
// if we have relation with relid=1 and size 100 and relation with relid=2 with size 200 then result of
// KeySpace::partition may contain partitions <100000000..100000099> and <200000000..200000199>.
// If there is delta layer <100000000..300000000> then it never be garbage collected because
// image layers <100000000..100000099> and <200000000..200000199> are not completely covering it.
let mut start = Key::MIN;
for partition in partitioning.parts.iter() { for partition in partitioning.parts.iter() {
let img_range = start..partition.ranges.last().unwrap().end;
start = img_range.end;
if force || self.time_for_new_image_layer(partition, lsn)? { if force || self.time_for_new_image_layer(partition, lsn)? {
let img_range =
partition.ranges.first().unwrap().start..partition.ranges.last().unwrap().end;
let mut image_layer_writer = ImageLayerWriter::new( let mut image_layer_writer = ImageLayerWriter::new(
self.conf, self.conf,
self.timeline_id, self.timeline_id,
@@ -2744,6 +2732,7 @@ impl Timeline {
"failpoint image-layer-writer-fail-before-finish" "failpoint image-layer-writer-fail-before-finish"
))) )))
}); });
for range in &partition.ranges { for range in &partition.ranges {
let mut key = range.start; let mut key = range.start;
while key < range.end { while key < range.end {
@@ -3158,7 +3147,9 @@ impl Timeline {
} }
fail_point!("delta-layer-writer-fail-before-finish", |_| { fail_point!("delta-layer-writer-fail-before-finish", |_| {
Err(anyhow::anyhow!("failpoint delta-layer-writer-fail-before-finish").into()) return Err(
anyhow::anyhow!("failpoint delta-layer-writer-fail-before-finish").into(),
);
}); });
writer.as_mut().unwrap().put_value(key, lsn, value)?; writer.as_mut().unwrap().put_value(key, lsn, value)?;

View File

@@ -1,15 +0,0 @@
# pgxs/neon_utils/Makefile
MODULE_big = neon_utils
OBJS = \
$(WIN32RES) \
neon_utils.o
EXTENSION = neon_utils
DATA = neon_utils--1.0.sql
PGFILEDESC = "neon_utils - small useful functions"
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

View File

@@ -1,6 +0,0 @@
CREATE FUNCTION num_cpus()
RETURNS int
AS 'MODULE_PATHNAME', 'num_cpus'
LANGUAGE C STRICT
PARALLEL UNSAFE
VOLATILE;

View File

@@ -1,35 +0,0 @@
/*-------------------------------------------------------------------------
*
* neon_utils.c
* neon_utils - small useful functions
*
* IDENTIFICATION
* contrib/neon_utils/neon_utils.c
*
*-------------------------------------------------------------------------
*/
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
#include "postgres.h"
#include "fmgr.h"
PG_MODULE_MAGIC;
PG_FUNCTION_INFO_V1(num_cpus);
Datum
num_cpus(PG_FUNCTION_ARGS)
{
#ifdef _WIN32
SYSTEM_INFO sysinfo;
GetSystemInfo(&sysinfo);
uint32 num_cpus = (uint32) sysinfo.dwNumberOfProcessors;
#else
uint32 num_cpus = (uint32) sysconf(_SC_NPROCESSORS_ONLN);
#endif
PG_RETURN_UINT32(num_cpus);
}

View File

@@ -1,6 +0,0 @@
# neon_utils extension
comment = 'neon_utils - small useful functions'
default_version = '1.0'
module_pathname = '$libdir/neon_utils'
relocatable = true
trusted = true

264
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. # This file is automatically @generated by Poetry and should not be changed by hand.
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
@@ -253,46 +253,43 @@ files = [
[[package]] [[package]]
name = "black" name = "black"
version = "23.1.0" version = "22.6.0"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.6.2"
files = [ files = [
{file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"},
{file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"},
{file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"},
{file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"},
{file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"},
{file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"},
{file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"},
{file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"},
{file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"},
{file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"},
{file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"},
{file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"},
{file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"},
{file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"},
{file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"},
{file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"},
{file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"},
{file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"},
{file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"},
{file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"},
{file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"},
{file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"},
{file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"},
{file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"},
{file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"},
] ]
[package.dependencies] [package.dependencies]
click = ">=8.0.0" click = ">=8.0.0"
mypy-extensions = ">=0.4.3" mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0" pathspec = ">=0.9.0"
platformdirs = ">=2" platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
[package.extras] [package.extras]
@@ -887,8 +884,6 @@ files = [
{file = "cryptography-39.0.1-cp36-abi3-win32.whl", hash = "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8"}, {file = "cryptography-39.0.1-cp36-abi3-win32.whl", hash = "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8"},
{file = "cryptography-39.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac"}, {file = "cryptography-39.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac"},
{file = "cryptography-39.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad"}, {file = "cryptography-39.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad"},
{file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef"},
{file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885"},
{file = "cryptography-39.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388"}, {file = "cryptography-39.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388"},
{file = "cryptography-39.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336"}, {file = "cryptography-39.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336"},
{file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2"}, {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2"},
@@ -968,6 +963,23 @@ files = [
[package.extras] [package.extras]
testing = ["pre-commit"] testing = ["pre-commit"]
[[package]]
name = "flake8"
version = "5.0.4"
description = "the modular source code checker: pep8 pyflakes and co"
category = "dev"
optional = false
python-versions = ">=3.6.1"
files = [
{file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
{file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
]
[package.dependencies]
mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.9.0,<2.10.0"
pyflakes = ">=2.5.0,<2.6.0"
[[package]] [[package]]
name = "flask" name = "flask"
version = "2.1.3" version = "2.1.3"
@@ -1063,6 +1075,24 @@ files = [
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
] ]
[[package]]
name = "isort"
version = "5.10.1"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
python-versions = ">=3.6.1,<4.0"
files = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
[package.extras]
colors = ["colorama (>=0.4.3,<0.5.0)"]
pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]] [[package]]
name = "itsdangerous" name = "itsdangerous"
version = "2.1.2" version = "2.1.2"
@@ -1208,7 +1238,6 @@ category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "junit-xml-1.9.tar.gz", hash = "sha256:de16a051990d4e25a3982b2dd9e89d671067548718866416faec14d9de56db9f"},
{file = "junit_xml-1.9-py2.py3-none-any.whl", hash = "sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732"}, {file = "junit_xml-1.9-py2.py3-none-any.whl", hash = "sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732"},
] ]
@@ -1265,6 +1294,18 @@ files = [
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
] ]
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]] [[package]]
name = "moto" name = "moto"
version = "4.1.2" version = "4.1.2"
@@ -1412,42 +1453,46 @@ files = [
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "1.1.1" version = "0.991"
description = "Optional static typing for Python" description = "Optional static typing for Python"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "mypy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39c7119335be05630611ee798cc982623b9e8f0cff04a0b48dfc26100e0b97af"}, {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"},
{file = "mypy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61bf08362e93b6b12fad3eab68c4ea903a077b87c90ac06c11e3d7a09b56b9c1"}, {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"},
{file = "mypy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbb19c9f662e41e474e0cff502b7064a7edc6764f5262b6cd91d698163196799"}, {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"},
{file = "mypy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:315ac73cc1cce4771c27d426b7ea558fb4e2836f89cb0296cbe056894e3a1f78"}, {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"},
{file = "mypy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5cb14ff9919b7df3538590fc4d4c49a0f84392237cbf5f7a816b4161c061829e"}, {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"},
{file = "mypy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26cdd6a22b9b40b2fd71881a8a4f34b4d7914c679f154f43385ca878a8297389"}, {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"},
{file = "mypy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b5f81b40d94c785f288948c16e1f2da37203c6006546c5d947aab6f90aefef2"}, {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"},
{file = "mypy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b437be1c02712a605591e1ed1d858aba681757a1e55fe678a15c2244cd68a5"}, {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"},
{file = "mypy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d809f88734f44a0d44959d795b1e6f64b2bbe0ea4d9cc4776aa588bb4229fc1c"}, {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"},
{file = "mypy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:a380c041db500e1410bb5b16b3c1c35e61e773a5c3517926b81dfdab7582be54"}, {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"},
{file = "mypy-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b7c7b708fe9a871a96626d61912e3f4ddd365bf7f39128362bc50cbd74a634d5"}, {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"},
{file = "mypy-1.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c10fa12df1232c936830839e2e935d090fc9ee315744ac33b8a32216b93707"}, {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"},
{file = "mypy-1.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0a28a76785bf57655a8ea5eb0540a15b0e781c807b5aa798bd463779988fa1d5"}, {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"},
{file = "mypy-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ef6a01e563ec6a4940784c574d33f6ac1943864634517984471642908b30b6f7"}, {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"},
{file = "mypy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d64c28e03ce40d5303450f547e07418c64c241669ab20610f273c9e6290b4b0b"}, {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"},
{file = "mypy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64cc3afb3e9e71a79d06e3ed24bb508a6d66f782aff7e56f628bf35ba2e0ba51"}, {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"},
{file = "mypy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce61663faf7a8e5ec6f456857bfbcec2901fbdb3ad958b778403f63b9e606a1b"}, {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"},
{file = "mypy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b0c373d071593deefbcdd87ec8db91ea13bd8f1328d44947e88beae21e8d5e9"}, {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"},
{file = "mypy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:2888ce4fe5aae5a673386fa232473014056967f3904f5abfcf6367b5af1f612a"}, {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"},
{file = "mypy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:19ba15f9627a5723e522d007fe708007bae52b93faab00f95d72f03e1afa9598"}, {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"},
{file = "mypy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:59bbd71e5c58eed2e992ce6523180e03c221dcd92b52f0e792f291d67b15a71c"}, {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"},
{file = "mypy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9401e33814cec6aec8c03a9548e9385e0e228fc1b8b0a37b9ea21038e64cdd8a"}, {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"},
{file = "mypy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b398d8b1f4fba0e3c6463e02f8ad3346f71956b92287af22c9b12c3ec965a9f"}, {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"},
{file = "mypy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:69b35d1dcb5707382810765ed34da9db47e7f95b3528334a3c999b0c90fe523f"}, {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"},
{file = "mypy-1.1.1-py3-none-any.whl", hash = "sha256:4e4e8b362cdf99ba00c2b218036002bdcdf1e0de085cdb296a49df03fb31dfc4"}, {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"},
{file = "mypy-1.1.1.tar.gz", hash = "sha256:ae9ceae0f5b9059f33dbc62dea087e942c0ccab4b7a003719cb70f9b8abfa32f"}, {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"},
{file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"},
{file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"},
{file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"},
{file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"},
] ]
[package.dependencies] [package.dependencies]
mypy-extensions = ">=1.0.0" mypy-extensions = ">=0.4.3"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=3.10" typing-extensions = ">=3.10"
@@ -1474,14 +1519,14 @@ typing-extensions = ">=4.1.0"
[[package]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "1.0.0" version = "0.4.3"
description = "Type system extensions for programs checked with the mypy type checker." description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = "*"
files = [ files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
] ]
[[package]] [[package]]
@@ -1546,16 +1591,19 @@ requests = ["requests"]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "23.0" version = "21.3"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.6"
files = [ files = [
{file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
] ]
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]] [[package]]
name = "pathspec" name = "pathspec"
version = "0.9.0" version = "0.9.0"
@@ -1664,7 +1712,6 @@ python-versions = ">=3.6"
files = [ files = [
{file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"}, {file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"}, {file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f2534ab7dc7e776a263b463a16e189eb30e85ec9bbe1bff9e78dae802608932"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"}, {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"}, {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"}, {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"},
@@ -1698,7 +1745,6 @@ files = [
{file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"}, {file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"},
{file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"}, {file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"}, {file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e6aa71ae45f952a2205377773e76f4e3f27951df38e69a4c95440c779e013560"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"}, {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"}, {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"}, {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"},
@@ -1710,7 +1756,6 @@ files = [
{file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"}, {file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"}, {file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"}, {file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b3a24a1982ae56461cc24f6680604fffa2c1b818e9dc55680da038792e004d18"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"}, {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"}, {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"}, {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"},
@@ -1743,10 +1788,33 @@ category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
] ]
[[package]]
name = "pycodestyle"
version = "2.9.1"
description = "Python style guide checker"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
{file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
]
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.21" version = "2.21"
@@ -1759,6 +1827,18 @@ files = [
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
] ]
[[package]]
name = "pyflakes"
version = "2.5.0"
description = "passive checker of Python programs"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
{file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
]
[[package]] [[package]]
name = "pyjwt" name = "pyjwt"
version = "2.4.0" version = "2.4.0"
@@ -1928,8 +2008,8 @@ files = [
[package.dependencies] [package.dependencies]
pytest = [ pytest = [
{version = ">=5.0", markers = "python_version < \"3.10\""},
{version = ">=6.2.4", markers = "python_version >= \"3.10\""}, {version = ">=6.2.4", markers = "python_version >= \"3.10\""},
{version = ">=5.0", markers = "python_version < \"3.10\""},
] ]
[[package]] [[package]]
@@ -2041,13 +2121,6 @@ files = [
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
{file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
{file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
{file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
{file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
@@ -2132,33 +2205,6 @@ files = [
[package.dependencies] [package.dependencies]
pyasn1 = ">=0.1.3" pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.0.255"
description = "An extremely fast Python linter, written in Rust."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.0.255-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:b2d71fb6a7e50501a2473864acffc85dee6b750c25db198f7e71fe1dbbff1aad"},
{file = "ruff-0.0.255-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6c97d746861a6010f941179e84bba9feb8a871815667471d9ed6beb98d45c252"},
{file = "ruff-0.0.255-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a7fa60085079b91a298b963361be9b1b1c724582af6c84be954cbabdbd9309a"},
{file = "ruff-0.0.255-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c089f7141496334ab5a127b54ce55e41f0d6714e68a4453a1e09d2204cdea8c3"},
{file = "ruff-0.0.255-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0423908caa7d437a416b853214565b9c33bbd1106c4f88147982216dddcbbd96"},
{file = "ruff-0.0.255-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:981493e92547cacbb8e0874904ec049fe744507ee890dc8736caf89a8864f9a7"},
{file = "ruff-0.0.255-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d5193d2aedb35db180824462b374dbcfc306b2e76076245088afa6e5837df2"},
{file = "ruff-0.0.255-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd5e00733c9d160c8a34a22e62b390da9d1e9f326676402421cb8c1236beefc3"},
{file = "ruff-0.0.255-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:694418cf41838bd19c6229e4e1b2d04505b1e6b86fe3ab81165484fc96d36f01"},
{file = "ruff-0.0.255-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5d0408985c9777369daebb5d3340a99e9f7294bdd7120642239261508185cf89"},
{file = "ruff-0.0.255-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abd6376ef9d12f370d95a8c7c98682fbb9bfedfba59f40e84a816fef8ddcb8de"},
{file = "ruff-0.0.255-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9b1a5df0bc09193cbef58a6f78e4a9a0b058a4f9733c0442866d078006d1bb9"},
{file = "ruff-0.0.255-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6a25c5f4ff087445b2e1bbcb9963f2ae7c868d65e4a8d5f84c36c12f71571179"},
{file = "ruff-0.0.255-py3-none-win32.whl", hash = "sha256:1ff87a8310354f9f1a099625e54a27fdd6756d9cd2a40b45922f2e943daf982d"},
{file = "ruff-0.0.255-py3-none-win_amd64.whl", hash = "sha256:f3d8416be618f023f93ec4fd6ee3048585ef85dba9563b2a7e38fc7e5131d5b1"},
{file = "ruff-0.0.255-py3-none-win_arm64.whl", hash = "sha256:8ba124819624145d7b6b53add40c367c44318893215ffc1bfe3d72e0225a1c9c"},
{file = "ruff-0.0.255.tar.gz", hash = "sha256:f9eb1d3b2eecbeedae419fa494c4e2a5e4484baf93a1ce0f81eddb005e1919c5"},
]
[[package]] [[package]]
name = "s3transfer" name = "s3transfer"
version = "0.6.0" version = "0.6.0"
@@ -2597,4 +2643,4 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "2515a9320c2960076012fbc036fb33c4f6a23515c8d143785931dc18c6722d91" content-hash = "3038940781ef59d1ed28cedf46120ad6623e21e602c38ad3c359428d79fa1efd"

View File

@@ -43,13 +43,17 @@ def black(fix_inplace: bool) -> str:
return cmd return cmd
def ruff(fix_inplace: bool) -> str: def isort(fix_inplace: bool) -> str:
cmd = "poetry run ruff" cmd = "poetry run isort"
if fix_inplace: if not fix_inplace:
cmd += " --fix" cmd += " --diff --check"
return cmd return cmd
def flake8() -> str:
return "poetry run flake8"
def mypy() -> str: def mypy() -> str:
return "poetry run mypy" return "poetry run mypy"
@@ -108,6 +112,13 @@ if __name__ == "__main__":
changed_files=files, changed_files=files,
no_color=args.no_color, no_color=args.no_color,
) )
check(
name="isort",
suffix=".py",
cmd=isort(fix_inplace=args.fix_inplace),
changed_files=files,
no_color=args.no_color,
)
check( check(
name="black", name="black",
suffix=".py", suffix=".py",
@@ -116,9 +127,9 @@ if __name__ == "__main__":
no_color=args.no_color, no_color=args.no_color,
) )
check( check(
name="ruff", name="flake8",
suffix=".py", suffix=".py",
cmd=ruff(fix_inplace=args.fix_inplace), cmd=flake8(),
changed_files=files, changed_files=files,
no_color=args.no_color, no_color=args.no_color,
) )

View File

@@ -6,9 +6,9 @@ use std::{io, net::SocketAddr};
use thiserror::Error; use thiserror::Error;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio_postgres::NoTls; use tokio_postgres::NoTls;
use tracing::{error, info, warn}; use tracing::{error, info};
const COULD_NOT_CONNECT: &str = "Couldn't connect to compute node"; const COULD_NOT_CONNECT: &str = "Could not connect to compute node";
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ConnectionError { pub enum ConnectionError {
@@ -131,7 +131,7 @@ impl ConnCfg {
use tokio_postgres::config::Host; use tokio_postgres::config::Host;
let connect_once = |host, port| { let connect_once = |host, port| {
info!("trying to connect to compute node at {host}:{port}"); info!("trying to connect to a compute node at {host}:{port}");
TcpStream::connect((host, port)).and_then(|socket| async { TcpStream::connect((host, port)).and_then(|socket| async {
let socket_addr = socket.peer_addr()?; let socket_addr = socket.peer_addr()?;
// This prevents load balancer from severing the connection. // This prevents load balancer from severing the connection.
@@ -151,7 +151,7 @@ impl ConnCfg {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::Other, io::ErrorKind::Other,
format!( format!(
"bad compute config, \ "couldn't connect: bad compute config, \
ports and hosts entries' count does not match: {:?}", ports and hosts entries' count does not match: {:?}",
self.0 self.0
), ),
@@ -170,7 +170,7 @@ impl ConnCfg {
Ok(socket) => return Ok(socket), Ok(socket) => return Ok(socket),
Err(err) => { Err(err) => {
// We can't throw an error here, as there might be more hosts to try. // We can't throw an error here, as there might be more hosts to try.
warn!("couldn't connect to compute node at {host}:{port}: {err}"); error!("failed to connect to a compute node at {host}:{port}: {err}");
connection_error = Some(err); connection_error = Some(err);
} }
} }
@@ -179,7 +179,7 @@ impl ConnCfg {
Err(connection_error.unwrap_or_else(|| { Err(connection_error.unwrap_or_else(|| {
io::Error::new( io::Error::new(
io::ErrorKind::Other, io::ErrorKind::Other,
format!("bad compute config: {:?}", self.0), format!("couldn't connect: bad compute config: {:?}", self.0),
) )
})) }))
} }
@@ -195,11 +195,12 @@ pub struct PostgresConnection {
} }
impl ConnCfg { impl ConnCfg {
async fn do_connect(&self) -> Result<PostgresConnection, ConnectionError> { /// Connect to a corresponding compute node.
pub async fn connect(&self) -> Result<PostgresConnection, ConnectionError> {
// TODO: establish a secure connection to the DB. // TODO: establish a secure connection to the DB.
let (socket_addr, mut stream) = self.connect_raw().await?; let (socket_addr, mut stream) = self.connect_raw().await?;
let (client, connection) = self.0.connect_raw(&mut stream, NoTls).await?; let (client, connection) = self.0.connect_raw(&mut stream, NoTls).await?;
info!("connected to compute node at {socket_addr}"); info!("connected to user's compute node at {socket_addr}");
// This is very ugly but as of now there's no better way to // This is very ugly but as of now there's no better way to
// extract the connection parameters from tokio-postgres' connection. // extract the connection parameters from tokio-postgres' connection.
@@ -218,16 +219,6 @@ impl ConnCfg {
Ok(connection) Ok(connection)
} }
/// Connect to a corresponding compute node.
pub async fn connect(&self) -> Result<PostgresConnection, ConnectionError> {
self.do_connect()
.inspect_err(|err| {
// Immediately log the error we have at our disposal.
error!("couldn't connect to compute node: {err}");
})
.await
}
} }
/// Retrieve `options` from a startup message, dropping all proxy-secific flags. /// Retrieve `options` from a startup message, dropping all proxy-secific flags.

View File

@@ -4,7 +4,7 @@ use crate::{
}; };
use anyhow::Context; use anyhow::Context;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use postgres_backend::{self, AuthType, PostgresBackend, PostgresBackendTCP, QueryError}; use postgres_backend::{self, AuthType, PostgresBackend, QueryError};
use pq_proto::{BeMessage, SINGLE_COL_ROWDESC}; use pq_proto::{BeMessage, SINGLE_COL_ROWDESC};
use std::future; use std::future;
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
@@ -29,15 +29,8 @@ pub fn notify(psql_session_id: &str, msg: ComputeReady) -> Result<(), waiters::N
CPLANE_WAITERS.notify(psql_session_id, msg) CPLANE_WAITERS.notify(psql_session_id, msg)
} }
/// Listener task for mgmt connections using the libpq protocol, for /// Console management API listener task.
/// the purposes of "kick callback" from proxy, for link authentication. /// It spawns console response handlers needed for the link auth.
///
/// The protocol is the libpq protocol, but the only "query" we accept is a
/// JSON document. The JSON document is a KickSession serialized to JSON.
///
/// This is considered legacy now. The preferred way to deliver "kick
/// callbacks" is now via the HTTP API. See `server.rs`. Once the control
/// plane has switched to using the HTTP API, this can be removed.
pub async fn task_main(listener: TcpListener) -> anyhow::Result<()> { pub async fn task_main(listener: TcpListener) -> anyhow::Result<()> {
scopeguard::defer! { scopeguard::defer! {
info!("mgmt has shut down"); info!("mgmt has shut down");
@@ -51,12 +44,11 @@ pub async fn task_main(listener: TcpListener) -> anyhow::Result<()> {
.set_nodelay(true) .set_nodelay(true)
.context("failed to set client socket option")?; .context("failed to set client socket option")?;
// spawn a task to handle this connection
tokio::task::spawn(async move { tokio::task::spawn(async move {
let span = info_span!("mgmt", peer = %peer_addr); let span = info_span!("mgmt", peer = %peer_addr);
let _enter = span.enter(); let _enter = span.enter();
info!("started a new console management API task"); info!("started a new console management API thread");
scopeguard::defer! { scopeguard::defer! {
info!("console management API thread is about to finish"); info!("console management API thread is about to finish");
} }
@@ -76,12 +68,13 @@ async fn handle_connection(socket: TcpStream) -> Result<(), QueryError> {
/// A message received by `mgmt` when a compute node is ready. /// A message received by `mgmt` when a compute node is ready.
pub type ComputeReady = Result<DatabaseInfo, String>; pub type ComputeReady = Result<DatabaseInfo, String>;
// TODO: replace with an http-based protocol.
struct MgmtHandler; struct MgmtHandler;
#[async_trait::async_trait] #[async_trait::async_trait]
impl postgres_backend::Handler<tokio::net::TcpStream> for MgmtHandler { impl postgres_backend::Handler for MgmtHandler {
async fn process_query( async fn process_query(
&mut self, &mut self,
pgb: &mut PostgresBackendTCP, pgb: &mut PostgresBackend,
query: &str, query: &str,
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
try_process_query(pgb, query).await.map_err(|e| { try_process_query(pgb, query).await.map_err(|e| {
@@ -91,7 +84,7 @@ impl postgres_backend::Handler<tokio::net::TcpStream> for MgmtHandler {
} }
} }
async fn try_process_query(pgb: &mut PostgresBackendTCP, query: &str) -> Result<(), QueryError> { async fn try_process_query(pgb: &mut PostgresBackend, query: &str) -> Result<(), QueryError> {
let resp: KickSession = serde_json::from_str(query).context("Failed to parse query as json")?; let resp: KickSession = serde_json::from_str(query).context("Failed to parse query as json")?;
let span = info_span!("event", session_id = resp.session_id); let span = info_span!("event", session_id = resp.session_id);

View File

@@ -1,8 +1,5 @@
use crate::console; use anyhow::anyhow;
use crate::console::messages::KickSession; use hyper::{Body, Request, Response, StatusCode};
use crate::waiters::NotifyError;
use anyhow::{anyhow, Context};
use hyper::{body, Body, Request, Response, StatusCode};
use std::net::TcpListener; use std::net::TcpListener;
use tracing::info; use tracing::info;
use utils::http::{endpoint, error::ApiError, json::json_response, RouterBuilder, RouterService}; use utils::http::{endpoint, error::ApiError, json::json_response, RouterBuilder, RouterService};
@@ -11,30 +8,8 @@ async fn status_handler(_: Request<Body>) -> Result<Response<Body>, ApiError> {
json_response(StatusCode::OK, "") json_response(StatusCode::OK, "")
} }
/// Process a session kick callback from the control plane. The body is a
/// KickSession as a JSON document.
///
/// TODO: authentication
async fn kick_session_handler(req: Request<Body>) -> Result<Response<Body>, ApiError> {
let body = &body::to_bytes(req.into_body())
.await
.context("Failed to get request body")
.map_err(ApiError::BadRequest)?;
let kick_session_json: KickSession = serde_json::from_slice(body)
.context("Failed to parse query as json")
.map_err(ApiError::BadRequest)?;
match console::mgmt::notify(kick_session_json.session_id, Ok(kick_session_json.result)) {
Ok(()) => json_response(StatusCode::OK, ""),
Err(NotifyError::NotFound(s)) => Err(ApiError::NotFound(anyhow::anyhow!(s))),
Err(e @ NotifyError::Hangup) => Err(ApiError::NotFound(anyhow::anyhow!(e))),
}
}
fn make_router() -> RouterBuilder<hyper::Body, ApiError> { fn make_router() -> RouterBuilder<hyper::Body, ApiError> {
endpoint::make_router() endpoint::make_router().get("/v1/status", status_handler)
.get("/v1/status", status_handler)
.post("/v1/kick_session", kick_session_handler)
} }
pub async fn task_main(http_listener: TcpListener) -> anyhow::Result<()> { pub async fn task_main(http_listener: TcpListener) -> anyhow::Result<()> {

View File

@@ -21,7 +21,6 @@ const PROXY_IO_BYTES_PER_CLIENT: &str = "proxy_io_bytes_per_client";
#[derive(Eq, Hash, PartialEq, Serialize, Debug)] #[derive(Eq, Hash, PartialEq, Serialize, Debug)]
pub struct Ids { pub struct Ids {
pub endpoint_id: String, pub endpoint_id: String,
pub branch_id: String,
} }
pub async fn task_main(config: &MetricCollectionConfig) -> anyhow::Result<()> { pub async fn task_main(config: &MetricCollectionConfig) -> anyhow::Result<()> {
@@ -75,23 +74,12 @@ fn gather_proxy_io_bytes_per_client() -> Vec<(Ids, (u64, DateTime<Utc>))> {
.find(|l| l.get_name() == "endpoint_id") .find(|l| l.get_name() == "endpoint_id")
.unwrap() .unwrap()
.get_value(); .get_value();
let branch_id = ms
.get_label()
.iter()
.find(|l| l.get_name() == "branch_id")
.unwrap()
.get_value();
let value = ms.get_counter().get_value() as u64; let value = ms.get_counter().get_value() as u64;
debug!( debug!("endpoint_id:val - {}: {}", endpoint_id, value);
"branch_id {} endpoint_id {} val: {}",
branch_id, endpoint_id, value
);
current_metrics.push(( current_metrics.push((
Ids { Ids {
endpoint_id: endpoint_id.to_string(), endpoint_id: endpoint_id.to_string(),
branch_id: "".to_string(),
}, },
(value, Utc::now()), (value, Utc::now()),
)); ));
@@ -143,7 +131,6 @@ async fn collect_metrics_iteration(
value, value,
extra: Ids { extra: Ids {
endpoint_id: curr_key.endpoint_id.clone(), endpoint_id: curr_key.endpoint_id.clone(),
branch_id: curr_key.branch_id.clone(),
}, },
}) })
}) })
@@ -185,7 +172,6 @@ async fn collect_metrics_iteration(
cached_metrics cached_metrics
.entry(Ids { .entry(Ids {
endpoint_id: send_metric.extra.endpoint_id.clone(), endpoint_id: send_metric.extra.endpoint_id.clone(),
branch_id: send_metric.extra.branch_id.clone(),
}) })
// update cached value (add delta) and time // update cached value (add delta) and time
.and_modify(|e| { .and_modify(|e| {

View File

@@ -8,7 +8,7 @@ use crate::{
config::{ProxyConfig, TlsConfig}, config::{ProxyConfig, TlsConfig},
console::{self, messages::MetricsAuxInfo}, console::{self, messages::MetricsAuxInfo},
error::io_error, error::io_error,
stream::{PqStream, Stream}, stream::{MeasuredStream, PqStream, Stream},
}; };
use anyhow::{bail, Context}; use anyhow::{bail, Context};
use futures::TryFutureExt; use futures::TryFutureExt;
@@ -18,7 +18,6 @@ use pq_proto::{BeMessage as Be, FeStartupPacket, StartupMessageParams};
use std::sync::Arc; use std::sync::Arc;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use utils::measured_stream::MeasuredStream;
/// Number of times we should retry the `/proxy_wake_compute` http request. /// Number of times we should retry the `/proxy_wake_compute` http request.
const NUM_RETRIES_WAKE_COMPUTE: usize = 1; const NUM_RETRIES_WAKE_COMPUTE: usize = 1;
@@ -354,24 +353,16 @@ async fn proxy_pass(
aux: &MetricsAuxInfo, aux: &MetricsAuxInfo,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let m_sent = NUM_BYTES_PROXIED_COUNTER.with_label_values(&aux.traffic_labels("tx")); let m_sent = NUM_BYTES_PROXIED_COUNTER.with_label_values(&aux.traffic_labels("tx"));
let mut client = MeasuredStream::new( let mut client = MeasuredStream::new(client, |cnt| {
client, // Number of bytes we sent to the client (outbound).
|_| {}, m_sent.inc_by(cnt as u64);
|cnt| { });
// Number of bytes we sent to the client (outbound).
m_sent.inc_by(cnt as u64);
},
);
let m_recv = NUM_BYTES_PROXIED_COUNTER.with_label_values(&aux.traffic_labels("rx")); let m_recv = NUM_BYTES_PROXIED_COUNTER.with_label_values(&aux.traffic_labels("rx"));
let mut compute = MeasuredStream::new( let mut compute = MeasuredStream::new(compute, |cnt| {
compute, // Number of bytes the client sent to the compute node (inbound).
|_| {}, m_recv.inc_by(cnt as u64);
|cnt| { });
// Number of bytes the client sent to the compute node (inbound).
m_recv.inc_by(cnt as u64);
},
);
// Starting from here we only proxy the client's traffic. // Starting from here we only proxy the client's traffic.
info!("performing the proxy pass..."); info!("performing the proxy pass...");

View File

@@ -14,7 +14,7 @@ pub const SCRAM_RAW_NONCE_LEN: usize = 18;
fn validate_sasl_extensions<'a>(parts: impl Iterator<Item = &'a str>) -> Option<()> { fn validate_sasl_extensions<'a>(parts: impl Iterator<Item = &'a str>) -> Option<()> {
for mut chars in parts.map(|s| s.chars()) { for mut chars in parts.map(|s| s.chars()) {
let attr = chars.next()?; let attr = chars.next()?;
if !attr.is_ascii_alphabetic() { if !('a'..='z').contains(&attr) && !('A'..='Z').contains(&attr) {
return None; return None;
} }
let eq = chars.next()?; let eq = chars.next()?;

View File

@@ -217,3 +217,68 @@ impl<S: AsyncRead + AsyncWrite + Unpin> AsyncWrite for Stream<S> {
} }
} }
} }
pin_project! {
/// This stream tracks all writes and calls user provided
/// callback when the underlying stream is flushed.
pub struct MeasuredStream<S, W> {
#[pin]
stream: S,
write_count: usize,
inc_write_count: W,
}
}
impl<S, W> MeasuredStream<S, W> {
pub fn new(stream: S, inc_write_count: W) -> Self {
Self {
stream,
write_count: 0,
inc_write_count,
}
}
}
impl<S: AsyncRead + Unpin, W> AsyncRead for MeasuredStream<S, W> {
fn poll_read(
self: Pin<&mut Self>,
context: &mut task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> task::Poll<io::Result<()>> {
self.project().stream.poll_read(context, buf)
}
}
impl<S: AsyncWrite + Unpin, W: FnMut(usize)> AsyncWrite for MeasuredStream<S, W> {
fn poll_write(
self: Pin<&mut Self>,
context: &mut task::Context<'_>,
buf: &[u8],
) -> task::Poll<io::Result<usize>> {
let this = self.project();
this.stream.poll_write(context, buf).map_ok(|cnt| {
// Increment the write count.
*this.write_count += cnt;
cnt
})
}
fn poll_flush(
self: Pin<&mut Self>,
context: &mut task::Context<'_>,
) -> task::Poll<io::Result<()>> {
let this = self.project();
this.stream.poll_flush(context).map_ok(|()| {
// Call the user provided callback and reset the write count.
(this.inc_write_count)(*this.write_count);
*this.write_count = 0;
})
}
fn poll_shutdown(
self: Pin<&mut Self>,
context: &mut task::Context<'_>,
) -> task::Poll<io::Result<()>> {
self.project().stream.poll_shutdown(context)
}
}

View File

@@ -35,10 +35,11 @@ types-toml = "^0.10.8"
pytest-httpserver = "^1.0.6" pytest-httpserver = "^1.0.6"
aiohttp = "3.7.4" aiohttp = "3.7.4"
[tool.poetry.group.dev.dependencies] [tool.poetry.dev-dependencies]
black = "^23.1.0" flake8 = "^5.0.4"
mypy = "==1.1.1" mypy = "==0.991"
ruff = "^0.0.255" black = "^22.6.0"
isort = "^5.10.1"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
@@ -52,6 +53,14 @@ extend-exclude = '''
)/ )/
''' '''
[tool.isort]
profile = "black"
line_length = 100
skip_gitignore = true
skip = [
"vendor",
]
[tool.mypy] [tool.mypy]
exclude = "^vendor/" exclude = "^vendor/"
check_untyped_defs = true check_untyped_defs = true
@@ -71,13 +80,3 @@ module = [
"pg8000.*", "pg8000.*",
] ]
ignore_missing_imports = true ignore_missing_imports = true
[tool.ruff]
extend-exclude = ["vendor/"]
ignore = ["E501"]
select = [
"E", # pycodestyle
"F", # Pyflakes
"I", # isort
"W", # pycodestyle
]

View File

@@ -8,13 +8,21 @@
# warnings and errors right in the editor. # warnings and errors right in the editor.
# In vscode, this setting is Rust-analyzer>Check On Save:Command # In vscode, this setting is Rust-analyzer>Check On Save:Command
# Not every feature is supported in macOS builds. Avoid running regular linting
# script that checks every feature.
#
# manual-range-contains wants # manual-range-contains wants
# !(4..=MAX_STARTUP_PACKET_LENGTH).contains(&len) # !(4..=MAX_STARTUP_PACKET_LENGTH).contains(&len)
# instead of # instead of
# len < 4 || len > MAX_STARTUP_PACKET_LENGTH # len < 4 || len > MAX_STARTUP_PACKET_LENGTH
# , let's disagree. # , let's disagree.
if [[ "$OSTYPE" == "darwin"* ]]; then
# * `-A unknown_lints` do not warn about unknown lint suppressions # no extra features to test currently, add more here when needed
# that people with newer toolchains might use cargo clippy --locked --all --all-targets --features testing -- -A unknown_lints -A clippy::manual-range-contains -D warnings
# * `-D warnings` - fail on any warnings (`cargo` returns non-zero exit status) else
cargo clippy --locked --all --all-targets --all-features -- -A unknown_lints -A clippy::manual-range-contains -D warnings # * `-A unknown_lints` do not warn about unknown lint suppressions
# that people with newer toolchains might use
# * `-D warnings` - fail on any warnings (`cargo` returns non-zero exit status)
cargo clippy --locked --all --all-targets --all-features -- -A unknown_lints -A clippy::manual-range-contains -D warnings
fi

View File

@@ -19,6 +19,7 @@ git-version.workspace = true
hex.workspace = true hex.workspace = true
humantime.workspace = true humantime.workspace = true
hyper.workspace = true hyper.workspace = true
nix.workspace = true
once_cell.workspace = true once_cell.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
postgres.workspace = true postgres.workspace = true

View File

@@ -3,13 +3,11 @@
use anyhow::Context; use anyhow::Context;
use std::str; use std::str;
use tokio::io::{AsyncRead, AsyncWrite};
use tracing::{info, info_span, Instrument}; use tracing::{info, info_span, Instrument};
use crate::auth::check_permission; use crate::auth::check_permission;
use crate::json_ctrl::{handle_json_ctrl, AppendLogicalMessage}; use crate::json_ctrl::{handle_json_ctrl, AppendLogicalMessage};
use crate::wal_service::ConnectionId;
use crate::{GlobalTimelines, SafeKeeperConf}; use crate::{GlobalTimelines, SafeKeeperConf};
use postgres_backend::QueryError; use postgres_backend::QueryError;
use postgres_backend::{self, PostgresBackend}; use postgres_backend::{self, PostgresBackend};
@@ -30,8 +28,6 @@ pub struct SafekeeperPostgresHandler {
pub tenant_id: Option<TenantId>, pub tenant_id: Option<TenantId>,
pub timeline_id: Option<TimelineId>, pub timeline_id: Option<TimelineId>,
pub ttid: TenantTimelineId, pub ttid: TenantTimelineId,
/// Unique connection id is logged in spans for observability.
pub conn_id: ConnectionId,
claims: Option<Claims>, claims: Option<Claims>,
} }
@@ -68,13 +64,11 @@ fn parse_cmd(cmd: &str) -> anyhow::Result<SafekeeperPostgresCommand> {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl<IO: AsyncRead + AsyncWrite + Unpin + Send> postgres_backend::Handler<IO> impl postgres_backend::Handler for SafekeeperPostgresHandler {
for SafekeeperPostgresHandler
{
// tenant_id and timeline_id are passed in connection string params // tenant_id and timeline_id are passed in connection string params
fn startup( fn startup(
&mut self, &mut self,
_pgb: &mut PostgresBackend<IO>, _pgb: &mut PostgresBackend,
sm: &FeStartupPacket, sm: &FeStartupPacket,
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
if let FeStartupPacket::StartupMessage { params, .. } = sm { if let FeStartupPacket::StartupMessage { params, .. } = sm {
@@ -113,7 +107,7 @@ impl<IO: AsyncRead + AsyncWrite + Unpin + Send> postgres_backend::Handler<IO>
fn check_auth_jwt( fn check_auth_jwt(
&mut self, &mut self,
_pgb: &mut PostgresBackend<IO>, _pgb: &mut PostgresBackend,
jwt_response: &[u8], jwt_response: &[u8],
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
// this unwrap is never triggered, because check_auth_jwt only called when auth_type is NeonJWT // this unwrap is never triggered, because check_auth_jwt only called when auth_type is NeonJWT
@@ -142,7 +136,7 @@ impl<IO: AsyncRead + AsyncWrite + Unpin + Send> postgres_backend::Handler<IO>
async fn process_query( async fn process_query(
&mut self, &mut self,
pgb: &mut PostgresBackend<IO>, pgb: &mut PostgresBackend,
query_string: &str, query_string: &str,
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
if query_string if query_string
@@ -187,14 +181,13 @@ impl<IO: AsyncRead + AsyncWrite + Unpin + Send> postgres_backend::Handler<IO>
} }
impl SafekeeperPostgresHandler { impl SafekeeperPostgresHandler {
pub fn new(conf: SafeKeeperConf, conn_id: u32) -> Self { pub fn new(conf: SafeKeeperConf) -> Self {
SafekeeperPostgresHandler { SafekeeperPostgresHandler {
conf, conf,
appname: None, appname: None,
tenant_id: None, tenant_id: None,
timeline_id: None, timeline_id: None,
ttid: TenantTimelineId::empty(), ttid: TenantTimelineId::empty(),
conn_id,
claims: None, claims: None,
} }
} }
@@ -219,9 +212,9 @@ impl SafekeeperPostgresHandler {
/// ///
/// Handle IDENTIFY_SYSTEM replication command /// Handle IDENTIFY_SYSTEM replication command
/// ///
async fn handle_identify_system<IO: AsyncRead + AsyncWrite + Unpin>( async fn handle_identify_system(
&mut self, &mut self,
pgb: &mut PostgresBackend<IO>, pgb: &mut PostgresBackend,
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
let tli = GlobalTimelines::get(self.ttid).map_err(|e| QueryError::Other(e.into()))?; let tli = GlobalTimelines::get(self.ttid).map_err(|e| QueryError::Other(e.into()))?;

View File

@@ -12,7 +12,6 @@ use anyhow::Context;
use bytes::Bytes; use bytes::Bytes;
use postgres_backend::QueryError; use postgres_backend::QueryError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::io::{AsyncRead, AsyncWrite};
use tracing::*; use tracing::*;
use utils::id::TenantTimelineId; use utils::id::TenantTimelineId;
@@ -61,9 +60,9 @@ struct AppendResult {
/// Handles command to craft logical message WAL record with given /// Handles command to craft logical message WAL record with given
/// content, and then append it with specified term and lsn. This /// content, and then append it with specified term and lsn. This
/// function is used to test safekeepers in different scenarios. /// function is used to test safekeepers in different scenarios.
pub async fn handle_json_ctrl<IO: AsyncRead + AsyncWrite + Unpin>( pub async fn handle_json_ctrl(
spg: &SafekeeperPostgresHandler, spg: &SafekeeperPostgresHandler,
pgb: &mut PostgresBackend<IO>, pgb: &mut PostgresBackend,
append_request: &AppendLogicalMessage, append_request: &AppendLogicalMessage,
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
info!("JSON_CTRL request: {append_request:?}"); info!("JSON_CTRL request: {append_request:?}");

View File

@@ -1,5 +1,4 @@
use remote_storage::RemoteStorageConfig; use remote_storage::RemoteStorageConfig;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
use storage_broker::Uri; use storage_broker::Uri;

View File

@@ -7,7 +7,7 @@ use anyhow::Result;
use metrics::{ use metrics::{
core::{AtomicU64, Collector, Desc, GenericGaugeVec, Opts}, core::{AtomicU64, Collector, Desc, GenericGaugeVec, Opts},
proto::MetricFamily, proto::MetricFamily,
register_int_counter_vec, Gauge, IntCounterVec, IntGaugeVec, Gauge, IntGaugeVec,
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use postgres_ffi::XLogSegNo; use postgres_ffi::XLogSegNo;
@@ -61,14 +61,6 @@ pub static PERSIST_CONTROL_FILE_SECONDS: Lazy<Histogram> = Lazy::new(|| {
) )
.expect("Failed to register safekeeper_persist_control_file_seconds histogram vec") .expect("Failed to register safekeeper_persist_control_file_seconds histogram vec")
}); });
pub static PG_IO_BYTES: Lazy<IntCounterVec> = Lazy::new(|| {
register_int_counter_vec!(
"safekeeper_pg_io_bytes",
"Bytes read from or written to any PostgreSQL connection",
&["direction"]
)
.expect("Failed to register safekeeper_pg_io_bytes gauge")
});
/// Metrics for WalStorage in a single timeline. /// Metrics for WalStorage in a single timeline.
#[derive(Clone, Default)] #[derive(Clone, Default)]

View File

@@ -7,10 +7,10 @@ use crate::safekeeper::AcceptorProposerMessage;
use crate::safekeeper::ProposerAcceptorMessage; use crate::safekeeper::ProposerAcceptorMessage;
use crate::safekeeper::ServerInfo; use crate::safekeeper::ServerInfo;
use crate::timeline::Timeline; use crate::timeline::Timeline;
use crate::wal_service::ConnectionId;
use crate::GlobalTimelines; use crate::GlobalTimelines;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use bytes::BytesMut; use bytes::BytesMut;
use nix::unistd::gettid;
use postgres_backend::CopyStreamHandlerEnd; use postgres_backend::CopyStreamHandlerEnd;
use postgres_backend::PostgresBackend; use postgres_backend::PostgresBackend;
use postgres_backend::PostgresBackendReader; use postgres_backend::PostgresBackendReader;
@@ -20,8 +20,6 @@ use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::thread;
use std::thread::JoinHandle; use std::thread::JoinHandle;
use tokio::io::AsyncRead;
use tokio::io::AsyncWrite;
use tokio::sync::mpsc::channel; use tokio::sync::mpsc::channel;
use tokio::sync::mpsc::error::TryRecvError; use tokio::sync::mpsc::error::TryRecvError;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
@@ -38,9 +36,9 @@ impl SafekeeperPostgresHandler {
/// Wrapper around handle_start_wal_push_guts handling result. Error is /// Wrapper around handle_start_wal_push_guts handling result. Error is
/// handled here while we're still in walreceiver ttid span; with API /// handled here while we're still in walreceiver ttid span; with API
/// extension, this can probably be moved into postgres_backend. /// extension, this can probably be moved into postgres_backend.
pub async fn handle_start_wal_push<IO: AsyncRead + AsyncWrite + Unpin>( pub async fn handle_start_wal_push(
&mut self, &mut self,
pgb: &mut PostgresBackend<IO>, pgb: &mut PostgresBackend,
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
if let Err(end) = self.handle_start_wal_push_guts(pgb).await { if let Err(end) = self.handle_start_wal_push_guts(pgb).await {
// Log the result and probably send it to the client, closing the stream. // Log the result and probably send it to the client, closing the stream.
@@ -49,9 +47,9 @@ impl SafekeeperPostgresHandler {
Ok(()) Ok(())
} }
pub async fn handle_start_wal_push_guts<IO: AsyncRead + AsyncWrite + Unpin>( pub async fn handle_start_wal_push_guts(
&mut self, &mut self,
pgb: &mut PostgresBackend<IO>, pgb: &mut PostgresBackend,
) -> Result<(), CopyStreamHandlerEnd> { ) -> Result<(), CopyStreamHandlerEnd> {
// Notify the libpq client that it's allowed to send `CopyData` messages // Notify the libpq client that it's allowed to send `CopyData` messages
pgb.write_message(&BeMessage::CopyBothResponse).await?; pgb.write_message(&BeMessage::CopyBothResponse).await?;
@@ -70,17 +68,10 @@ impl SafekeeperPostgresHandler {
// sends, so this avoids deadlocks. // sends, so this avoids deadlocks.
let mut pgb_reader = pgb.split().context("START_WAL_PUSH split")?; let mut pgb_reader = pgb.split().context("START_WAL_PUSH split")?;
let peer_addr = *pgb.get_peer_addr(); let peer_addr = *pgb.get_peer_addr();
let network_reader = NetworkReader {
ttid: self.ttid,
conn_id: self.conn_id,
pgb_reader: &mut pgb_reader,
peer_addr,
acceptor_handle: &mut acceptor_handle,
};
let res = tokio::select! { let res = tokio::select! {
// todo: add read|write .context to these errors // todo: add read|write .context to these errors
r = network_reader.run(msg_tx, msg_rx, reply_tx) => r, r = read_network(self.ttid, &mut pgb_reader, peer_addr, msg_tx, &mut acceptor_handle, msg_rx, reply_tx) => r,
r = network_write(pgb, reply_rx) => r, r = write_network(pgb, reply_rx) => r,
}; };
// Join pg backend back. // Join pg backend back.
@@ -113,67 +104,62 @@ impl SafekeeperPostgresHandler {
} }
} }
struct NetworkReader<'a, IO> {
ttid: TenantTimelineId,
conn_id: ConnectionId,
pgb_reader: &'a mut PostgresBackendReader<IO>,
peer_addr: SocketAddr,
// WalAcceptor is spawned when we learn server info from walproposer and
// create timeline; handle is put here.
acceptor_handle: &'a mut Option<JoinHandle<anyhow::Result<()>>>,
}
impl<'a, IO: AsyncRead + AsyncWrite + Unpin> NetworkReader<'a, IO> {
async fn run(
self,
msg_tx: Sender<ProposerAcceptorMessage>,
msg_rx: Receiver<ProposerAcceptorMessage>,
reply_tx: Sender<AcceptorProposerMessage>,
) -> Result<(), CopyStreamHandlerEnd> {
// Receive information about server to create timeline, if not yet.
let next_msg = read_message(self.pgb_reader).await?;
let tli = match next_msg {
ProposerAcceptorMessage::Greeting(ref greeting) => {
info!(
"start handshake with walproposer {} sysid {} timeline {}",
self.peer_addr, greeting.system_id, greeting.tli,
);
let server_info = ServerInfo {
pg_version: greeting.pg_version,
system_id: greeting.system_id,
wal_seg_size: greeting.wal_seg_size,
};
GlobalTimelines::create(self.ttid, server_info, Lsn::INVALID, Lsn::INVALID).await?
}
_ => {
return Err(CopyStreamHandlerEnd::Other(anyhow::anyhow!(
"unexpected message {next_msg:?} instead of greeting"
)))
}
};
*self.acceptor_handle = Some(
WalAcceptor::spawn(tli.clone(), msg_rx, reply_tx, self.conn_id)
.context("spawn WalAcceptor thread")?,
);
// Forward all messages to WalAcceptor
read_network_loop(self.pgb_reader, msg_tx, next_msg).await
}
}
/// Read next message from walproposer. /// Read next message from walproposer.
/// TODO: Return Ok(None) on graceful termination. /// TODO: Return Ok(None) on graceful termination.
async fn read_message<IO: AsyncRead + AsyncWrite + Unpin>( async fn read_message(
pgb_reader: &mut PostgresBackendReader<IO>, pgb_reader: &mut PostgresBackendReader,
) -> Result<ProposerAcceptorMessage, CopyStreamHandlerEnd> { ) -> Result<ProposerAcceptorMessage, CopyStreamHandlerEnd> {
let copy_data = pgb_reader.read_copy_message().await?; let copy_data = pgb_reader.read_copy_message().await?;
let msg = ProposerAcceptorMessage::parse(copy_data)?; let msg = ProposerAcceptorMessage::parse(copy_data)?;
Ok(msg) Ok(msg)
} }
async fn read_network_loop<IO: AsyncRead + AsyncWrite + Unpin>( /// Read messages from socket and pass it to WalAcceptor thread. Returns Ok(())
pgb_reader: &mut PostgresBackendReader<IO>, /// if msg_tx closed; it must mean WalAcceptor terminated, joining it should
/// tell the error.
async fn read_network(
ttid: TenantTimelineId,
pgb_reader: &mut PostgresBackendReader,
peer_addr: SocketAddr,
msg_tx: Sender<ProposerAcceptorMessage>,
// WalAcceptor is spawned when we learn server info from walproposer and
// create timeline; handle is put here.
acceptor_handle: &mut Option<JoinHandle<anyhow::Result<()>>>,
msg_rx: Receiver<ProposerAcceptorMessage>,
reply_tx: Sender<AcceptorProposerMessage>,
) -> Result<(), CopyStreamHandlerEnd> {
// Receive information about server to create timeline, if not yet.
let next_msg = read_message(pgb_reader).await?;
let tli = match next_msg {
ProposerAcceptorMessage::Greeting(ref greeting) => {
info!(
"start handshake with walproposer {} sysid {} timeline {}",
peer_addr, greeting.system_id, greeting.tli,
);
let server_info = ServerInfo {
pg_version: greeting.pg_version,
system_id: greeting.system_id,
wal_seg_size: greeting.wal_seg_size,
};
GlobalTimelines::create(ttid, server_info, Lsn::INVALID, Lsn::INVALID).await?
}
_ => {
return Err(CopyStreamHandlerEnd::Other(anyhow::anyhow!(
"unexpected message {next_msg:?} instead of greeting"
)))
}
};
*acceptor_handle = Some(
WalAcceptor::spawn(tli.clone(), msg_rx, reply_tx).context("spawn WalAcceptor thread")?,
);
// Forward all messages to WalAcceptor
read_network_loop(pgb_reader, msg_tx, next_msg).await
}
async fn read_network_loop(
pgb_reader: &mut PostgresBackendReader,
msg_tx: Sender<ProposerAcceptorMessage>, msg_tx: Sender<ProposerAcceptorMessage>,
mut next_msg: ProposerAcceptorMessage, mut next_msg: ProposerAcceptorMessage,
) -> Result<(), CopyStreamHandlerEnd> { ) -> Result<(), CopyStreamHandlerEnd> {
@@ -188,8 +174,8 @@ async fn read_network_loop<IO: AsyncRead + AsyncWrite + Unpin>(
/// Read replies from WalAcceptor and pass them back to socket. Returns Ok(()) /// Read replies from WalAcceptor and pass them back to socket. Returns Ok(())
/// if reply_rx closed; it must mean WalAcceptor terminated, joining it should /// if reply_rx closed; it must mean WalAcceptor terminated, joining it should
/// tell the error. /// tell the error.
async fn network_write<IO: AsyncRead + AsyncWrite + Unpin>( async fn write_network(
pgb_writer: &mut PostgresBackend<IO>, pgb_writer: &mut PostgresBackend,
mut reply_rx: Receiver<AcceptorProposerMessage>, mut reply_rx: Receiver<AcceptorProposerMessage>,
) -> Result<(), CopyStreamHandlerEnd> { ) -> Result<(), CopyStreamHandlerEnd> {
let mut buf = BytesMut::with_capacity(128); let mut buf = BytesMut::with_capacity(128);
@@ -219,7 +205,6 @@ impl WalAcceptor {
tli: Arc<Timeline>, tli: Arc<Timeline>,
msg_rx: Receiver<ProposerAcceptorMessage>, msg_rx: Receiver<ProposerAcceptorMessage>,
reply_tx: Sender<AcceptorProposerMessage>, reply_tx: Sender<AcceptorProposerMessage>,
conn_id: ConnectionId,
) -> anyhow::Result<JoinHandle<anyhow::Result<()>>> { ) -> anyhow::Result<JoinHandle<anyhow::Result<()>>> {
let thread_name = format!("WAL acceptor {}", tli.ttid); let thread_name = format!("WAL acceptor {}", tli.ttid);
thread::Builder::new() thread::Builder::new()
@@ -238,7 +223,7 @@ impl WalAcceptor {
let span_ttid = wa.tli.ttid; // satisfy borrow checker let span_ttid = wa.tli.ttid; // satisfy borrow checker
runtime.block_on( runtime.block_on(
wa.run() wa.run()
.instrument(info_span!("WAL acceptor", cid = %conn_id, ttid = %span_ttid)), .instrument(info_span!("WAL acceptor", tid = %gettid(), ttid = %span_ttid)),
) )
}) })
.map_err(anyhow::Error::from) .map_err(anyhow::Error::from)

View File

@@ -13,8 +13,6 @@ use postgres_ffi::get_current_timestamp;
use postgres_ffi::{TimestampTz, MAX_SEND_SIZE}; use postgres_ffi::{TimestampTz, MAX_SEND_SIZE};
use pq_proto::{BeMessage, ReplicationFeedback, WalSndKeepAlive, XLogDataBody}; use pq_proto::{BeMessage, ReplicationFeedback, WalSndKeepAlive, XLogDataBody};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::io::{AsyncRead, AsyncWrite};
use std::cmp::min; use std::cmp::min;
use std::str; use std::str;
use std::sync::Arc; use std::sync::Arc;
@@ -76,9 +74,9 @@ impl SafekeeperPostgresHandler {
/// Wrapper around handle_start_replication_guts handling result. Error is /// Wrapper around handle_start_replication_guts handling result. Error is
/// handled here while we're still in walsender ttid span; with API /// handled here while we're still in walsender ttid span; with API
/// extension, this can probably be moved into postgres_backend. /// extension, this can probably be moved into postgres_backend.
pub async fn handle_start_replication<IO: AsyncRead + AsyncWrite + Unpin>( pub async fn handle_start_replication(
&mut self, &mut self,
pgb: &mut PostgresBackend<IO>, pgb: &mut PostgresBackend,
start_pos: Lsn, start_pos: Lsn,
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
if let Err(end) = self.handle_start_replication_guts(pgb, start_pos).await { if let Err(end) = self.handle_start_replication_guts(pgb, start_pos).await {
@@ -88,9 +86,9 @@ impl SafekeeperPostgresHandler {
Ok(()) Ok(())
} }
pub async fn handle_start_replication_guts<IO: AsyncRead + AsyncWrite + Unpin>( pub async fn handle_start_replication_guts(
&mut self, &mut self,
pgb: &mut PostgresBackend<IO>, pgb: &mut PostgresBackend,
start_pos: Lsn, start_pos: Lsn,
) -> Result<(), CopyStreamHandlerEnd> { ) -> Result<(), CopyStreamHandlerEnd> {
let appname = self.appname.clone(); let appname = self.appname.clone();
@@ -178,8 +176,8 @@ impl SafekeeperPostgresHandler {
} }
/// A half driving sending WAL. /// A half driving sending WAL.
struct WalSender<'a, IO> { struct WalSender<'a> {
pgb: &'a mut PostgresBackend<IO>, pgb: &'a mut PostgresBackend,
tli: Arc<Timeline>, tli: Arc<Timeline>,
appname: Option<String>, appname: Option<String>,
// Position since which we are sending next chunk. // Position since which we are sending next chunk.
@@ -196,7 +194,7 @@ struct WalSender<'a, IO> {
send_buf: [u8; MAX_SEND_SIZE], send_buf: [u8; MAX_SEND_SIZE],
} }
impl<IO: AsyncRead + AsyncWrite + Unpin> WalSender<'_, IO> { impl WalSender<'_> {
/// Send WAL until /// Send WAL until
/// - an error occurs /// - an error occurs
/// - if we are streaming to walproposer, we've streamed until stop_pos /// - if we are streaming to walproposer, we've streamed until stop_pos
@@ -284,14 +282,14 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> WalSender<'_, IO> {
} }
/// A half driving receiving replies. /// A half driving receiving replies.
struct ReplyReader<IO> { struct ReplyReader {
reader: PostgresBackendReader<IO>, reader: PostgresBackendReader,
tli: Arc<Timeline>, tli: Arc<Timeline>,
replica_id: usize, replica_id: usize,
feedback: ReplicaState, feedback: ReplicaState,
} }
impl<IO: AsyncRead + AsyncWrite + Unpin> ReplyReader<IO> { impl ReplyReader {
async fn run(&mut self) -> Result<(), CopyStreamHandlerEnd> { async fn run(&mut self) -> Result<(), CopyStreamHandlerEnd> {
loop { loop {
let msg = self.reader.read_copy_message().await?; let msg = self.reader.read_copy_message().await?;

View File

@@ -3,14 +3,14 @@
//! receive WAL from wal_proposer and send it to WAL receivers //! receive WAL from wal_proposer and send it to WAL receivers
//! //!
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use nix::unistd::gettid;
use postgres_backend::QueryError; use postgres_backend::QueryError;
use std::{future, thread}; use std::{future, thread};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tracing::*; use tracing::*;
use utils::measured_stream::MeasuredStream;
use crate::handler::SafekeeperPostgresHandler;
use crate::SafeKeeperConf; use crate::SafeKeeperConf;
use crate::{handler::SafekeeperPostgresHandler, metrics::PG_IO_BYTES};
use postgres_backend::{AuthType, PostgresBackend}; use postgres_backend::{AuthType, PostgresBackend};
/// Accept incoming TCP connections and spawn them into a background thread. /// Accept incoming TCP connections and spawn them into a background thread.
@@ -27,19 +27,17 @@ pub fn thread_main(conf: SafeKeeperConf, pg_listener: std::net::TcpListener) {
// Tokio's from_std won't do this for us, per its comment. // Tokio's from_std won't do this for us, per its comment.
pg_listener.set_nonblocking(true)?; pg_listener.set_nonblocking(true)?;
let listener = tokio::net::TcpListener::from_std(pg_listener)?; let listener = tokio::net::TcpListener::from_std(pg_listener)?;
let mut connection_count: ConnectionCount = 0;
loop { loop {
match listener.accept().await { match listener.accept().await {
Ok((socket, peer_addr)) => { Ok((socket, peer_addr)) => {
debug!("accepted connection from {}", peer_addr); debug!("accepted connection from {}", peer_addr);
let conf = conf.clone(); let conf = conf.clone();
let conn_id = issue_connection_id(&mut connection_count);
let _ = thread::Builder::new() let _ = thread::Builder::new()
.name("WAL service thread".into()) .name("WAL service thread".into())
.spawn(move || { .spawn(move || {
if let Err(err) = handle_socket(socket, conf, conn_id) { if let Err(err) = handle_socket(socket, conf) {
error!("connection handler exited: {}", err); error!("connection handler exited: {}", err);
} }
}) })
@@ -56,41 +54,22 @@ pub fn thread_main(conf: SafeKeeperConf, pg_listener: std::net::TcpListener) {
/// This is run by `thread_main` above, inside a background thread. /// This is run by `thread_main` above, inside a background thread.
/// ///
fn handle_socket( fn handle_socket(socket: TcpStream, conf: SafeKeeperConf) -> Result<(), QueryError> {
socket: TcpStream, let _enter = info_span!("", tid = %gettid()).entered();
conf: SafeKeeperConf,
conn_id: ConnectionId,
) -> Result<(), QueryError> {
let _enter = info_span!("", cid = %conn_id).entered();
let runtime = tokio::runtime::Builder::new_current_thread() let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all() .enable_all()
.build()?; .build()?;
let local = tokio::task::LocalSet::new(); let local = tokio::task::LocalSet::new();
let read_metrics = PG_IO_BYTES.with_label_values(&["read"]);
let write_metrics = PG_IO_BYTES.with_label_values(&["write"]);
socket.set_nodelay(true)?; socket.set_nodelay(true)?;
let peer_addr = socket.peer_addr()?;
// TODO: measure cross-az traffic
let socket = MeasuredStream::new(
socket,
|cnt| {
read_metrics.inc_by(cnt as u64);
},
|cnt| {
write_metrics.inc_by(cnt as u64);
},
);
let auth_type = match conf.auth { let auth_type = match conf.auth {
None => AuthType::Trust, None => AuthType::Trust,
Some(_) => AuthType::NeonJWT, Some(_) => AuthType::NeonJWT,
}; };
let mut conn_handler = SafekeeperPostgresHandler::new(conf, conn_id); let mut conn_handler = SafekeeperPostgresHandler::new(conf);
let pgbackend = PostgresBackend::new_from_io(socket, peer_addr, auth_type, None)?; let pgbackend = PostgresBackend::new(socket, auth_type, None)?;
// libpq protocol between safekeeper and walproposer / pageserver // libpq protocol between safekeeper and walproposer / pageserver
// We don't use shutdown. // We don't use shutdown.
local.block_on( local.block_on(
@@ -100,12 +79,3 @@ fn handle_socket(
Ok(()) Ok(())
} }
/// Unique WAL service connection ids are logged in spans for observability.
pub type ConnectionId = u32;
pub type ConnectionCount = u32;
pub fn issue_connection_id(count: &mut ConnectionCount) -> ConnectionId {
*count = count.wrapping_add(1);
*count
}

View File

@@ -308,8 +308,8 @@ def lsn_to_hex(num: int) -> str:
def lsn_from_hex(lsn_hex: str) -> int: def lsn_from_hex(lsn_hex: str) -> int:
"""Convert lsn from hex notation to int.""" """Convert lsn from hex notation to int."""
left, right = lsn_hex.split("/") l, r = lsn_hex.split("/")
return (int(left, 16) << 32) + int(right, 16) return (int(l, 16) << 32) + int(r, 16)
def remote_consistent_lsn( def remote_consistent_lsn(
@@ -398,6 +398,7 @@ def reconstruct_paths(log_dir, pg_bin, base_tar, port: int):
result = vanilla_pg.safe_psql(query, user="cloud_admin", dbname=database) result = vanilla_pg.safe_psql(query, user="cloud_admin", dbname=database)
for relname, filepath in result: for relname, filepath in result:
if filepath is not None: if filepath is not None:
if database == "template0copy": if database == "template0copy":
# Add all template0copy paths to template0 # Add all template0copy paths to template0
prefix = f"base/{oid}/" prefix = f"base/{oid}/"

View File

@@ -6,5 +6,6 @@ set -euox pipefail
echo 'Reformatting Rust code' echo 'Reformatting Rust code'
cargo fmt cargo fmt
echo 'Reformatting Python code' echo 'Reformatting Python code'
poetry run ruff --fix test_runner scripts poetry run isort test_runner scripts
poetry run flake8 test_runner scripts
poetry run black test_runner scripts poetry run black test_runner scripts

View File

@@ -68,6 +68,7 @@ def call_delete_tenant_api(tenant_id):
def cleanup_tenant(tenant_id): def cleanup_tenant(tenant_id):
tenant_dir = Path(f"/storage/safekeeper/data/{tenant_id}") tenant_dir = Path(f"/storage/safekeeper/data/{tenant_id}")
if not tenant_dir.exists(): if not tenant_dir.exists():

8
setup.cfg Normal file
View File

@@ -0,0 +1,8 @@
[flake8]
# Move config to pyproject.toml as soon as flake8 supports it
# https://github.com/PyCQA/flake8/issues/234
extend-ignore =
E203, # Whitespace before ':' -- conflicts with black
E266, # Too many leading '#' for block comment -- we use it for formatting sometimes
E501 # Line too long -- black sorts it out
extend-exclude = vendor/

View File

@@ -17,7 +17,6 @@ import pytest
from _pytest.config import Config from _pytest.config import Config
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.terminal import TerminalReporter from _pytest.terminal import TerminalReporter
from fixtures.neon_fixtures import NeonPageserver from fixtures.neon_fixtures import NeonPageserver
from fixtures.types import TenantId, TimelineId from fixtures.types import TenantId, TimelineId

View File

@@ -6,15 +6,8 @@ from typing import Dict, Iterator, List
import pytest import pytest
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
from fixtures.benchmark_fixture import MetricReport, NeonBenchmarker from fixtures.benchmark_fixture import MetricReport, NeonBenchmarker
from fixtures.neon_fixtures import ( from fixtures.neon_fixtures import NeonEnv, PgBin, PgProtocol, RemotePostgres, VanillaPostgres
NeonEnv,
PgBin,
PgProtocol,
RemotePostgres,
VanillaPostgres,
)
from fixtures.pg_stats import PgStatTable from fixtures.pg_stats import PgStatTable

View File

@@ -25,7 +25,6 @@ from types import TracebackType
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union, cast from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union, cast
from urllib.parse import urlparse from urllib.parse import urlparse
import aiohttp
import asyncpg import asyncpg
import backoff # type: ignore import backoff # type: ignore
import boto3 import boto3
@@ -36,13 +35,6 @@ import requests
from _pytest.config import Config from _pytest.config import Config
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
# Type-related stuff
from psycopg2.extensions import connection as PgConnection
from psycopg2.extensions import cursor as PgCursor
from psycopg2.extensions import make_dsn, parse_dsn
from typing_extensions import Literal
from fixtures.log_helper import log from fixtures.log_helper import log
from fixtures.metrics import Metrics, parse_metrics from fixtures.metrics import Metrics, parse_metrics
from fixtures.types import Lsn, TenantId, TimelineId from fixtures.types import Lsn, TenantId, TimelineId
@@ -54,6 +46,12 @@ from fixtures.utils import (
subprocess_capture, subprocess_capture,
) )
# Type-related stuff
from psycopg2.extensions import connection as PgConnection
from psycopg2.extensions import cursor as PgCursor
from psycopg2.extensions import make_dsn, parse_dsn
from typing_extensions import Literal
""" """
This file contains pytest fixtures. A fixture is a test resource that can be This file contains pytest fixtures. A fixture is a test resource that can be
summoned by placing its name in the test's arguments. summoned by placing its name in the test's arguments.
@@ -1245,6 +1243,7 @@ class PageserverHttpClient(requests.Session):
include_non_incremental_logical_size: bool = False, include_non_incremental_logical_size: bool = False,
include_timeline_dir_layer_file_size_sum: bool = False, include_timeline_dir_layer_file_size_sum: bool = False,
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
params = {} params = {}
if include_non_incremental_logical_size: if include_non_incremental_logical_size:
params["include-non-incremental-logical-size"] = "true" params["include-non-incremental-logical-size"] = "true"
@@ -1376,6 +1375,7 @@ class PageserverHttpClient(requests.Session):
timeline_id: TimelineId, timeline_id: TimelineId,
max_concurrent_downloads: int, max_concurrent_downloads: int,
) -> dict[str, Any]: ) -> dict[str, Any]:
body = { body = {
"max_concurrent_downloads": max_concurrent_downloads, "max_concurrent_downloads": max_concurrent_downloads,
} }
@@ -1668,7 +1668,7 @@ class AbstractNeonCli(abc.ABC):
env_vars["POSTGRES_DISTRIB_DIR"] = str(self.env.pg_distrib_dir) env_vars["POSTGRES_DISTRIB_DIR"] = str(self.env.pg_distrib_dir)
if self.env.rust_log_override is not None: if self.env.rust_log_override is not None:
env_vars["RUST_LOG"] = self.env.rust_log_override env_vars["RUST_LOG"] = self.env.rust_log_override
for extra_env_key, extra_env_value in (extra_env_vars or {}).items(): for (extra_env_key, extra_env_value) in (extra_env_vars or {}).items():
env_vars[extra_env_key] = extra_env_value env_vars[extra_env_key] = extra_env_value
# Pass coverage settings # Pass coverage settings
@@ -2155,7 +2155,7 @@ class NeonPageserver(PgProtocol):
def assert_no_errors(self): def assert_no_errors(self):
logfile = open(os.path.join(self.env.repo_dir, "pageserver.log"), "r") logfile = open(os.path.join(self.env.repo_dir, "pageserver.log"), "r")
error_or_warn = re.compile(r"\s(ERROR|WARN)") error_or_warn = re.compile("ERROR|WARN")
errors = [] errors = []
while True: while True:
line = logfile.readline() line = logfile.readline()
@@ -2562,11 +2562,7 @@ class NeonProxy(PgProtocol):
@staticmethod @staticmethod
async def activate_link_auth( async def activate_link_auth(
local_vanilla_pg, local_vanilla_pg, proxy_with_metric_collector, psql_session_id, create_user=True
proxy_with_metric_collector,
psql_session_id,
create_user=True,
use_legacy_mgmt_api=False,
): ):
pg_user = "proxy" pg_user = "proxy"
@@ -2575,40 +2571,33 @@ class NeonProxy(PgProtocol):
local_vanilla_pg.start() local_vanilla_pg.start()
local_vanilla_pg.safe_psql(f"create user {pg_user} with login superuser") local_vanilla_pg.safe_psql(f"create user {pg_user} with login superuser")
db_info = { db_info = json.dumps(
"session_id": psql_session_id, {
"result": { "session_id": psql_session_id,
"Success": { "result": {
"host": local_vanilla_pg.default_options["host"], "Success": {
"port": local_vanilla_pg.default_options["port"], "host": local_vanilla_pg.default_options["host"],
"dbname": local_vanilla_pg.default_options["dbname"], "port": local_vanilla_pg.default_options["port"],
"user": pg_user, "dbname": local_vanilla_pg.default_options["dbname"],
"aux": { "user": pg_user,
"project_id": "test_project_id", "aux": {
"endpoint_id": "test_endpoint_id", "project_id": "test_project_id",
"branch_id": "test_branch_id", "endpoint_id": "test_endpoint_id",
}, "branch_id": "test_branch_id",
} },
}, }
} },
}
)
if use_legacy_mgmt_api: log.info("sending session activation message")
log.info("sending session activation message using legacy libpq mgmt interface") psql = await PSQL(
psql = await PSQL( host=proxy_with_metric_collector.host,
host=proxy_with_metric_collector.host, port=proxy_with_metric_collector.mgmt_port,
port=proxy_with_metric_collector.mgmt_port, ).run(db_info)
).run(json.dumps(db_info)) assert psql.stdout is not None
assert psql.stdout is not None out = (await psql.stdout.read()).decode("utf-8").strip()
out = (await psql.stdout.read()).decode("utf-8").strip() assert out == "ok"
assert out == "ok"
else:
log.info("sending session activation message using HTTP mgmt interface")
async with aiohttp.request(
"POST",
f"http://{proxy_with_metric_collector.host}:{proxy_with_metric_collector.http_port}/v1/kick_session",
json=db_info,
) as resp:
assert resp.ok
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@@ -2863,6 +2852,7 @@ class PostgresFactory:
lsn: Optional[Lsn] = None, lsn: Optional[Lsn] = None,
config_lines: Optional[List[str]] = None, config_lines: Optional[List[str]] = None,
) -> Postgres: ) -> Postgres:
pg = Postgres( pg = Postgres(
self.env, self.env,
tenant_id=tenant_id or self.env.initial_tenant, tenant_id=tenant_id or self.env.initial_tenant,
@@ -2886,6 +2876,7 @@ class PostgresFactory:
lsn: Optional[Lsn] = None, lsn: Optional[Lsn] = None,
config_lines: Optional[List[str]] = None, config_lines: Optional[List[str]] = None,
) -> Postgres: ) -> Postgres:
pg = Postgres( pg = Postgres(
self.env, self.env,
tenant_id=tenant_id or self.env.initial_tenant, tenant_id=tenant_id or self.env.initial_tenant,
@@ -3332,6 +3323,7 @@ def check_restored_datadir_content(
log.info(f"filecmp result mismatch and error lists:\n\t mismatch={mismatch}\n\t error={error}") log.info(f"filecmp result mismatch and error lists:\n\t mismatch={mismatch}\n\t error={error}")
for f in mismatch: for f in mismatch:
f1 = os.path.join(pg.pgdata_dir, f) f1 = os.path.join(pg.pgdata_dir, f)
f2 = os.path.join(restored_dir_path, f) f2 = os.path.join(restored_dir_path, f)
stdout_filename = "{}.filediff".format(f2) stdout_filename = "{}.filediff".format(f2)

View File

@@ -17,8 +17,8 @@ class Lsn:
self.lsn_int = x self.lsn_int = x
else: else:
"""Convert lsn from hex notation to int.""" """Convert lsn from hex notation to int."""
left, right = x.split("/") l, r = x.split("/")
self.lsn_int = (int(left, 16) << 32) + int(right, 16) self.lsn_int = (int(l, 16) << 32) + int(r, 16)
assert 0 <= self.lsn_int <= 0xFFFFFFFF_FFFFFFFF assert 0 <= self.lsn_int <= 0xFFFFFFFF_FFFFFFFF
def __str__(self) -> str: def __str__(self) -> str:

View File

@@ -8,9 +8,8 @@ from pathlib import Path
from typing import Any, Callable, Dict, List, Tuple, TypeVar from typing import Any, Callable, Dict, List, Tuple, TypeVar
import allure # type: ignore import allure # type: ignore
from psycopg2.extensions import cursor
from fixtures.log_helper import log from fixtures.log_helper import log
from psycopg2.extensions import cursor
Fn = TypeVar("Fn", bound=Callable[..., Any]) Fn = TypeVar("Fn", bound=Callable[..., Any])

View File

@@ -5,7 +5,6 @@ from typing import List
from fixtures.benchmark_fixture import PgBenchRunResult from fixtures.benchmark_fixture import PgBenchRunResult
from fixtures.compare_fixtures import NeonCompare from fixtures.compare_fixtures import NeonCompare
from fixtures.neon_fixtures import fork_at_current_lsn from fixtures.neon_fixtures import fork_at_current_lsn
from performance.test_perf_pgbench import utc_now_timestamp from performance.test_perf_pgbench import utc_now_timestamp
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------

View File

@@ -13,6 +13,7 @@ from fixtures.neon_fixtures import NeonEnvBuilder, wait_for_last_flush_lsn
@pytest.mark.timeout(10000) @pytest.mark.timeout(10000)
@pytest.mark.parametrize("fillfactor", [10, 50, 100]) @pytest.mark.parametrize("fillfactor", [10, 50, 100])
def test_bulk_update(neon_env_builder: NeonEnvBuilder, zenbenchmark, fillfactor): def test_bulk_update(neon_env_builder: NeonEnvBuilder, zenbenchmark, fillfactor):
env = neon_env_builder.init_start() env = neon_env_builder.init_start()
n_records = 1000000 n_records = 1000000

View File

@@ -6,7 +6,6 @@ from typing import List
import pytest import pytest
from fixtures.compare_fixtures import PgCompare from fixtures.compare_fixtures import PgCompare
from fixtures.pg_stats import PgStatTable from fixtures.pg_stats import PgStatTable
from performance.test_perf_pgbench import get_durations_matrix, get_scales_matrix from performance.test_perf_pgbench import get_durations_matrix, get_scales_matrix

View File

@@ -13,6 +13,7 @@ def test_gist_buffering_build(neon_with_baseline: PgCompare):
with closing(env.pg.connect()) as conn: with closing(env.pg.connect()) as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
# Create test table. # Create test table.
cur.execute("create table gist_point_tbl(id int4, p point)") cur.execute("create table gist_point_tbl(id int4, p point)")
cur.execute( cur.execute(

View File

@@ -3,7 +3,6 @@ import threading
import pytest import pytest
from fixtures.compare_fixtures import PgCompare from fixtures.compare_fixtures import PgCompare
from fixtures.neon_fixtures import Postgres from fixtures.neon_fixtures import Postgres
from performance.test_perf_pgbench import get_scales_matrix from performance.test_perf_pgbench import get_scales_matrix
from performance.test_wal_backpressure import record_read_latency from performance.test_wal_backpressure import record_read_latency

View File

@@ -7,6 +7,7 @@ from fixtures.neon_fixtures import NeonEnvBuilder
# Benchmark searching the layer map, when there are a lot of small layer files. # Benchmark searching the layer map, when there are a lot of small layer files.
# #
def test_layer_map(neon_env_builder: NeonEnvBuilder, zenbenchmark): def test_layer_map(neon_env_builder: NeonEnvBuilder, zenbenchmark):
env = neon_env_builder.init_start() env = neon_env_builder.init_start()
n_iters = 10 n_iters = 10
n_records = 100000 n_records = 100000

View File

@@ -36,6 +36,7 @@ async def parallel_load_different_tables(pg: PgProtocol, n_parallel: int):
# Load 5 different tables in parallel with COPY TO # Load 5 different tables in parallel with COPY TO
def test_parallel_copy_different_tables(neon_with_baseline: PgCompare, n_parallel=5): def test_parallel_copy_different_tables(neon_with_baseline: PgCompare, n_parallel=5):
env = neon_with_baseline env = neon_with_baseline
conn = env.pg.connect() conn = env.pg.connect()
cur = conn.cursor() cur = conn.cursor()

View File

@@ -10,7 +10,6 @@ from fixtures.compare_fixtures import NeonCompare, PgCompare, VanillaCompare
from fixtures.log_helper import log from fixtures.log_helper import log
from fixtures.neon_fixtures import DEFAULT_BRANCH_NAME, NeonEnvBuilder, PgBin from fixtures.neon_fixtures import DEFAULT_BRANCH_NAME, NeonEnvBuilder, PgBin
from fixtures.types import Lsn from fixtures.types import Lsn
from performance.test_perf_pgbench import get_durations_matrix, get_scales_matrix from performance.test_perf_pgbench import get_durations_matrix, get_scales_matrix

View File

@@ -22,6 +22,7 @@ def test_write_amplification(neon_with_baseline: PgCompare):
with conn.cursor() as cur: with conn.cursor() as cur:
with env.record_pageserver_writes("pageserver_writes"): with env.record_pageserver_writes("pageserver_writes"):
with env.record_duration("run"): with env.record_duration("run"):
# NOTE: Because each iteration updates every table already created, # NOTE: Because each iteration updates every table already created,
# the runtime and write amplification is O(n^2), where n is the # the runtime and write amplification is O(n^2), where n is the
# number of iterations. # number of iterations.

View File

@@ -1 +0,0 @@
target/

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +0,0 @@
[package]
name = "rust-neon-example"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
native-tls = "0.2.11"
postgres-native-tls = "0.5.0"
tokio = { version = "1.26", features=["rt", "macros"] }
tokio-postgres = "0.7.7"
# This is not part of the main 'neon' workspace
[workspace]

View File

@@ -1,6 +0,0 @@
FROM rust:1.67
WORKDIR /source
COPY . .
RUN cargo build
CMD ["/source/target/debug/rust-neon-example"]

View File

@@ -1,43 +0,0 @@
use std::env::VarError;
use tokio_postgres;
fn get_env(key: &str) -> String {
match std::env::var(key) {
Ok(val) => val,
Err(VarError::NotPresent) => panic!("{key} env variable not set"),
Err(VarError::NotUnicode(_)) => panic!("{key} is not valid unicode"),
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), tokio_postgres::Error> {
let host = get_env("NEON_HOST");
let database = get_env("NEON_DATABASE");
let user = get_env("NEON_USER");
let password = get_env("NEON_PASSWORD");
let url = format!("postgresql://{user}:{password}@{host}/{database}");
// Use the native TLS implementation (Neon requires TLS)
let tls_connector =
postgres_native_tls::MakeTlsConnector::new(native_tls::TlsConnector::new().unwrap());
// Connect to the database.
let (client, connection) = tokio_postgres::connect(&url, tls_connector).await?;
// The connection object performs the actual communication with the database,
// so spawn it off to run on its own.
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {}", e);
}
});
let result = client.query("SELECT 1", &[]).await?;
let value: i32 = result[0].get(0);
assert_eq!(value, 1);
println!("{value}");
Ok(())
}

View File

@@ -13,7 +13,6 @@ from fixtures.utils import subprocess_capture
[ [
"csharp/npgsql", "csharp/npgsql",
"java/jdbc", "java/jdbc",
"rust/tokio-postgres",
"python/asyncpg", "python/asyncpg",
"python/pg8000", "python/pg8000",
pytest.param( pytest.param(

View File

@@ -2,7 +2,6 @@ import pytest
from fixtures.log_helper import log from fixtures.log_helper import log
from fixtures.neon_fixtures import NeonEnvBuilder, WalCraft from fixtures.neon_fixtures import NeonEnvBuilder, WalCraft
# Restart nodes with WAL end having specially crafted shape, like last record # Restart nodes with WAL end having specially crafted shape, like last record
# crossing segment boundary, to test decoding issues. # crossing segment boundary, to test decoding issues.

View File

@@ -2,12 +2,7 @@ import os
from pathlib import Path from pathlib import Path
from fixtures.log_helper import log from fixtures.log_helper import log
from fixtures.neon_fixtures import ( from fixtures.neon_fixtures import NeonEnvBuilder, PgBin, PortDistributor, VanillaPostgres
NeonEnvBuilder,
PgBin,
PortDistributor,
VanillaPostgres,
)
from fixtures.types import Lsn, TimelineId from fixtures.types import Lsn, TimelineId
from fixtures.utils import query_scalar, subprocess_capture from fixtures.utils import query_scalar, subprocess_capture

View File

@@ -68,6 +68,7 @@ async def update_and_gc(env: NeonEnv, pg: Postgres, timeline: TimelineId):
# (repro for https://github.com/neondatabase/neon/issues/1047) # (repro for https://github.com/neondatabase/neon/issues/1047)
# #
def test_gc_aggressive(neon_env_builder: NeonEnvBuilder): def test_gc_aggressive(neon_env_builder: NeonEnvBuilder):
# Disable pitr, because here we want to test branch creation after GC # Disable pitr, because here we want to test branch creation after GC
neon_env_builder.pageserver_config_override = "tenant_config={pitr_interval = '0 sec'}" neon_env_builder.pageserver_config_override = "tenant_config={pitr_interval = '0 sec'}"
env = neon_env_builder.init_start() env = neon_env_builder.init_start()
@@ -100,6 +101,7 @@ def test_gc_aggressive(neon_env_builder: NeonEnvBuilder):
# #
@pytest.mark.parametrize("remote_storage_kind", [RemoteStorageKind.LOCAL_FS]) @pytest.mark.parametrize("remote_storage_kind", [RemoteStorageKind.LOCAL_FS])
def test_gc_index_upload(neon_env_builder: NeonEnvBuilder, remote_storage_kind: RemoteStorageKind): def test_gc_index_upload(neon_env_builder: NeonEnvBuilder, remote_storage_kind: RemoteStorageKind):
# Disable time-based pitr, we will use LSN-based thresholds in the manual GC calls # Disable time-based pitr, we will use LSN-based thresholds in the manual GC calls
neon_env_builder.pageserver_config_override = "tenant_config={pitr_interval = '0 sec'}" neon_env_builder.pageserver_config_override = "tenant_config={pitr_interval = '0 sec'}"

View File

@@ -61,12 +61,6 @@ def test_import_from_vanilla(test_output_dir, pg_bin, vanilla_pg, neon_env_build
cwd=unpacked_base, cwd=unpacked_base,
) )
# Make copy of base.tar and append some garbage to it.
base_plus_garbage_tar = os.path.join(basebackup_dir, "base-plus-garbage.tar")
shutil.copyfile(base_tar, base_plus_garbage_tar)
with open(base_plus_garbage_tar, "a") as f:
f.write("trailing garbage")
# Get start_lsn and end_lsn # Get start_lsn and end_lsn
with open(os.path.join(basebackup_dir, "backup_manifest")) as f: with open(os.path.join(basebackup_dir, "backup_manifest")) as f:
manifest = json.load(f) manifest = json.load(f)
@@ -80,8 +74,7 @@ def test_import_from_vanilla(test_output_dir, pg_bin, vanilla_pg, neon_env_build
# Set up pageserver for import # Set up pageserver for import
neon_env_builder.enable_local_fs_remote_storage() neon_env_builder.enable_local_fs_remote_storage()
env = neon_env_builder.init_start() env = neon_env_builder.init_start()
client = env.pageserver.http_client() env.pageserver.http_client().tenant_create(tenant)
client.tenant_create(tenant)
env.pageserver.allowed_errors.extend( env.pageserver.allowed_errors.extend(
[ [
@@ -92,7 +85,6 @@ def test_import_from_vanilla(test_output_dir, pg_bin, vanilla_pg, neon_env_build
".*InternalServerError.*Tenant .* not found.*", ".*InternalServerError.*Tenant .* not found.*",
".*InternalServerError.*Timeline .* not found.*", ".*InternalServerError.*Timeline .* not found.*",
".*InternalServerError.*Cannot delete timeline which has child timelines.*", ".*InternalServerError.*Cannot delete timeline which has child timelines.*",
".*ignored .* unexpected bytes after the tar archive.*",
] ]
) )
@@ -138,18 +130,11 @@ def test_import_from_vanilla(test_output_dir, pg_bin, vanilla_pg, neon_env_build
with pytest.raises(Exception): with pytest.raises(Exception):
import_tar(corrupt_base_tar, wal_tar) import_tar(corrupt_base_tar, wal_tar)
# A tar with trailing garbage is currently accepted. It prints a warnings
# to the pageserver log, however. Check that.
import_tar(base_plus_garbage_tar, wal_tar)
assert env.pageserver.log_contains(
".*WARN.*ignored .* unexpected bytes after the tar archive.*"
)
client.timeline_delete(tenant, timeline)
# Importing correct backup works # Importing correct backup works
import_tar(base_tar, wal_tar) import_tar(base_tar, wal_tar)
# Wait for data to land in s3 # Wait for data to land in s3
client = env.pageserver.http_client()
wait_for_last_record_lsn(client, tenant, timeline, Lsn(end_lsn)) wait_for_last_record_lsn(client, tenant, timeline, Lsn(end_lsn))
wait_for_upload(client, tenant, timeline, Lsn(end_lsn)) wait_for_upload(client, tenant, timeline, Lsn(end_lsn))

View File

@@ -146,6 +146,7 @@ def test_basic_eviction(
def test_gc_of_remote_layers(neon_env_builder: NeonEnvBuilder): def test_gc_of_remote_layers(neon_env_builder: NeonEnvBuilder):
neon_env_builder.enable_remote_storage( neon_env_builder.enable_remote_storage(
remote_storage_kind=RemoteStorageKind.LOCAL_FS, remote_storage_kind=RemoteStorageKind.LOCAL_FS,
test_name="test_gc_of_remote_layers", test_name="test_gc_of_remote_layers",

View File

@@ -4,7 +4,7 @@
import time import time
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
from typing import Any, DefaultDict, Dict, Tuple from typing import Any, DefaultDict, Dict
import pytest import pytest
from fixtures.log_helper import log from fixtures.log_helper import log
@@ -250,7 +250,7 @@ def test_ondemand_download_timetravel(
# Run queries at different points in time # Run queries at different points in time
num_layers_downloaded = [0] num_layers_downloaded = [0]
resident_size = [get_resident_physical_size()] resident_size = [get_resident_physical_size()]
for checkpoint_number, lsn in lsns: for (checkpoint_number, lsn) in lsns:
pg_old = env.postgres.create_start( pg_old = env.postgres.create_start(
branch_name="main", node_name=f"test_old_lsn_{checkpoint_number}", lsn=lsn branch_name="main", node_name=f"test_old_lsn_{checkpoint_number}", lsn=lsn
) )
@@ -497,17 +497,6 @@ def test_compaction_downloads_on_demand_without_image_creation(
# pitr_interval and gc_horizon are not interesting because we dont run gc # pitr_interval and gc_horizon are not interesting because we dont run gc
} }
def downloaded_bytes_and_count(pageserver_http: PageserverHttpClient) -> Tuple[int, int]:
m = pageserver_http.get_metrics()
# these are global counters
total_bytes = m.query_one("pageserver_remote_ondemand_downloaded_bytes_total").value
assert (
total_bytes < 2**53 and total_bytes.is_integer()
), "bytes should still be safe integer-in-f64"
count = m.query_one("pageserver_remote_ondemand_downloaded_layers_total").value
assert count < 2**53 and count.is_integer(), "count should still be safe integer-in-f64"
return (int(total_bytes), int(count))
# Override defaults, to create more layers # Override defaults, to create more layers
tenant_id, timeline_id = env.neon_cli.create_tenant(conf=stringify(conf)) tenant_id, timeline_id = env.neon_cli.create_tenant(conf=stringify(conf))
env.initial_tenant = tenant_id env.initial_tenant = tenant_id
@@ -528,14 +517,10 @@ def test_compaction_downloads_on_demand_without_image_creation(
layers = pageserver_http.layer_map_info(tenant_id, timeline_id) layers = pageserver_http.layer_map_info(tenant_id, timeline_id)
assert not layers.in_memory_layers, "no inmemory layers expected after post-commit checkpoint" assert not layers.in_memory_layers, "no inmemory layers expected after post-commit checkpoint"
assert len(layers.historic_layers) == 1 + 2, "should have initdb layer and 2 deltas" assert len(layers.historic_layers) == 1 + 2, "should have inidb layer and 2 deltas"
layer_sizes = 0
for layer in layers.historic_layers: for layer in layers.historic_layers:
log.info(f"pre-compact: {layer}") log.info(f"pre-compact: {layer}")
assert layer.layer_file_size is not None, "we must know layer file sizes"
layer_sizes += layer.layer_file_size
pageserver_http.evict_layer(tenant_id, timeline_id, layer.layer_file_name) pageserver_http.evict_layer(tenant_id, timeline_id, layer.layer_file_name)
env.neon_cli.config_tenant(tenant_id, {"compaction_threshold": "3"}) env.neon_cli.config_tenant(tenant_id, {"compaction_threshold": "3"})
@@ -546,12 +531,6 @@ def test_compaction_downloads_on_demand_without_image_creation(
log.info(f"post compact: {layer}") log.info(f"post compact: {layer}")
assert len(layers.historic_layers) == 1, "should have compacted to single layer" assert len(layers.historic_layers) == 1, "should have compacted to single layer"
post_compact = downloaded_bytes_and_count(pageserver_http)
# use gte to allow pageserver to do other random stuff; this test could be run on a shared pageserver
assert post_compact[0] >= layer_sizes
assert post_compact[1] >= 3, "should had downloaded the three layers"
@pytest.mark.parametrize("remote_storage_kind", [RemoteStorageKind.MOCK_S3]) @pytest.mark.parametrize("remote_storage_kind", [RemoteStorageKind.MOCK_S3])
def test_compaction_downloads_on_demand_with_image_creation( def test_compaction_downloads_on_demand_with_image_creation(

View File

@@ -4,20 +4,10 @@ from fixtures.neon_fixtures import PSQL, NeonProxy, VanillaPostgres
def test_proxy_select_1(static_proxy: NeonProxy): def test_proxy_select_1(static_proxy: NeonProxy):
""" static_proxy.safe_psql("select 1", options="project=generic-project-name")
A simplest smoke test: check proxy against a local postgres instance.
"""
out = static_proxy.safe_psql("select 1", options="project=generic-project-name")
assert out[0][0] == 1
def test_password_hack(static_proxy: NeonProxy): def test_password_hack(static_proxy: NeonProxy):
"""
Check the PasswordHack auth flow: an alternative to SCRAM auth for
clients which can't provide the project/endpoint name via SNI or `options`.
"""
user = "borat" user = "borat"
password = "password" password = "password"
static_proxy.safe_psql( static_proxy.safe_psql(
@@ -35,14 +25,7 @@ def test_password_hack(static_proxy: NeonProxy):
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("use_legacy_mgmt_api", [True, False]) async def test_psql_session_id(vanilla_pg: VanillaPostgres, link_proxy: NeonProxy):
async def test_link_auth(
vanilla_pg: VanillaPostgres, link_proxy: NeonProxy, use_legacy_mgmt_api: bool
):
"""
Check the Link auth flow: a lightweight auth method which delegates
all necessary checks to the console by sending client an auth URL.
"""
psql = await PSQL(host=link_proxy.host, port=link_proxy.proxy_port).run("select 42") psql = await PSQL(host=link_proxy.host, port=link_proxy.proxy_port).run("select 42")
@@ -50,39 +33,23 @@ async def test_link_auth(
link = await NeonProxy.find_auth_link(base_uri, psql) link = await NeonProxy.find_auth_link(base_uri, psql)
psql_session_id = NeonProxy.get_session_id(base_uri, link) psql_session_id = NeonProxy.get_session_id(base_uri, link)
await NeonProxy.activate_link_auth( await NeonProxy.activate_link_auth(vanilla_pg, link_proxy, psql_session_id)
vanilla_pg,
link_proxy,
psql_session_id,
use_legacy_mgmt_api=use_legacy_mgmt_api,
)
assert psql.stdout is not None assert psql.stdout is not None
out = (await psql.stdout.read()).decode("utf-8").strip() out = (await psql.stdout.read()).decode("utf-8").strip()
assert out == "42" assert out == "42"
# Pass extra options to the server.
def test_proxy_options(static_proxy: NeonProxy): def test_proxy_options(static_proxy: NeonProxy):
""" with static_proxy.connect(options="project=irrelevant -cproxytest.option=value") as conn:
Check that we pass extra `options` to the PostgreSQL server: with conn.cursor() as cur:
* `project=...` shouldn't be passed at all (otherwise postgres will raise an error). cur.execute("SHOW proxytest.option")
* everything else should be passed as-is. value = cur.fetchall()[0][0]
""" assert value == "value"
options = "project=irrelevant -cproxytest.option=value"
out = static_proxy.safe_psql("show proxytest.option", options=options)
assert out[0][0] == "value"
options = "-c proxytest.foo=\\ str project=irrelevant"
out = static_proxy.safe_psql("show proxytest.foo", options=options)
assert out[0][0] == " str"
def test_auth_errors(static_proxy: NeonProxy): def test_auth_errors(static_proxy: NeonProxy):
"""
Check that we throw very specific errors in some unsuccessful auth scenarios.
"""
# User does not exist # User does not exist
with pytest.raises(psycopg2.Error) as exprinfo: with pytest.raises(psycopg2.Error) as exprinfo:
static_proxy.connect(user="pinocchio", options="project=irrelevant") static_proxy.connect(user="pinocchio", options="project=irrelevant")
@@ -111,10 +78,6 @@ def test_auth_errors(static_proxy: NeonProxy):
def test_forward_params_to_client(static_proxy: NeonProxy): def test_forward_params_to_client(static_proxy: NeonProxy):
"""
Check that we forward all necessary PostgreSQL server params to client.
"""
# A subset of parameters (GUCs) which postgres # A subset of parameters (GUCs) which postgres
# sends to the client during connection setup. # sends to the client during connection setup.
# Unfortunately, `GUC_REPORT` can't be queried. # Unfortunately, `GUC_REPORT` can't be queried.

View File

@@ -22,6 +22,7 @@ def test_read_validation(neon_simple_env: NeonEnv):
with closing(pg.connect()) as con: with closing(pg.connect()) as con:
with con.cursor() as c: with con.cursor() as c:
for e in extensions: for e in extensions:
c.execute("create extension if not exists {};".format(e)) c.execute("create extension if not exists {};".format(e))
@@ -149,6 +150,7 @@ def test_read_validation_neg(neon_simple_env: NeonEnv):
with closing(pg.connect()) as con: with closing(pg.connect()) as con:
with con.cursor() as c: with con.cursor() as c:
for e in extensions: for e in extensions:
c.execute("create extension if not exists {};".format(e)) c.execute("create extension if not exists {};".format(e))

View File

@@ -146,7 +146,7 @@ def test_timetravel(neon_simple_env: NeonEnv):
env.pageserver.stop() env.pageserver.stop()
env.pageserver.start() env.pageserver.start()
for i, lsn in lsns: for (i, lsn) in lsns:
pg_old = env.postgres.create_start( pg_old = env.postgres.create_start(
branch_name="test_timetravel", node_name=f"test_old_lsn_{i}", lsn=lsn branch_name="test_timetravel", node_name=f"test_old_lsn_{i}", lsn=lsn
) )

View File

@@ -212,6 +212,7 @@ def test_remote_storage_upload_queue_retries(
neon_env_builder: NeonEnvBuilder, neon_env_builder: NeonEnvBuilder,
remote_storage_kind: RemoteStorageKind, remote_storage_kind: RemoteStorageKind,
): ):
neon_env_builder.enable_remote_storage( neon_env_builder.enable_remote_storage(
remote_storage_kind=remote_storage_kind, remote_storage_kind=remote_storage_kind,
test_name="test_remote_storage_upload_queue_retries", test_name="test_remote_storage_upload_queue_retries",
@@ -373,6 +374,7 @@ def test_remote_timeline_client_calls_started_metric(
neon_env_builder: NeonEnvBuilder, neon_env_builder: NeonEnvBuilder,
remote_storage_kind: RemoteStorageKind, remote_storage_kind: RemoteStorageKind,
): ):
neon_env_builder.enable_remote_storage( neon_env_builder.enable_remote_storage(
remote_storage_kind=remote_storage_kind, remote_storage_kind=remote_storage_kind,
test_name="test_remote_timeline_client_metrics", test_name="test_remote_timeline_client_metrics",
@@ -617,7 +619,7 @@ def test_timeline_deletion_with_files_stuck_in_upload_queue(
log.info("sending delete request") log.info("sending delete request")
checkpoint_allowed_to_fail.set() checkpoint_allowed_to_fail.set()
env.pageserver.allowed_errors.append( env.pageserver.allowed_errors.append(
".* ERROR .*Error processing HTTP request: InternalServerError\\(timeline is Stopping" ".+ERROR Error processing HTTP request: InternalServerError\\(timeline is Stopping"
) )
client.timeline_delete(tenant_id, timeline_id) client.timeline_delete(tenant_id, timeline_id)

View File

@@ -177,7 +177,6 @@ async def reattach_while_busy(
# running, and when we retry the queries, they should start working # running, and when we retry the queries, they should start working
# after the attach has finished. # after the attach has finished.
# FIXME: # FIXME:
# #
# This is pretty unstable at the moment. I've seen it fail with a warning like this: # This is pretty unstable at the moment. I've seen it fail with a warning like this:

View File

@@ -375,6 +375,7 @@ def test_tenant_relocation(
neon_env_builder.broker, neon_env_builder.broker,
neon_env_builder.pg_distrib_dir, neon_env_builder.pg_distrib_dir,
): ):
# Migrate either by attaching from s3 or import/export basebackup # Migrate either by attaching from s3 or import/export basebackup
if method == "major": if method == "major":
cmd = [ cmd = [
@@ -434,13 +435,10 @@ def test_tenant_relocation(
) )
# rewrite neon cli config to use new pageserver for basebackup to start new compute # rewrite neon cli config to use new pageserver for basebackup to start new compute
lines = (env.repo_dir / "config").read_text().splitlines() cli_config_lines = (env.repo_dir / "config").read_text().splitlines()
for i, line in enumerate(lines): cli_config_lines[-2] = f"listen_http_addr = 'localhost:{new_pageserver_http_port}'"
if line.startswith("listen_http_addr"): cli_config_lines[-1] = f"listen_pg_addr = 'localhost:{new_pageserver_pg_port}'"
lines[i] = f"listen_http_addr = 'localhost:{new_pageserver_http_port}'" (env.repo_dir / "config").write_text("\n".join(cli_config_lines))
if line.startswith("listen_pg_addr"):
lines[i] = f"listen_pg_addr = 'localhost:{new_pageserver_pg_port}'"
(env.repo_dir / "config").write_text("\n".join(lines))
old_local_path_main = switch_pg_to_new_pageserver( old_local_path_main = switch_pg_to_new_pageserver(
env, env,
@@ -499,10 +497,7 @@ def test_tenant_relocation(
# bring old pageserver back for clean shutdown via neon cli # bring old pageserver back for clean shutdown via neon cli
# new pageserver will be shut down by the context manager # new pageserver will be shut down by the context manager
lines = (env.repo_dir / "config").read_text().splitlines() cli_config_lines = (env.repo_dir / "config").read_text().splitlines()
for i, line in enumerate(lines): cli_config_lines[-2] = f"listen_http_addr = 'localhost:{env.pageserver.service_port.http}'"
if line.startswith("listen_http_addr"): cli_config_lines[-1] = f"listen_pg_addr = 'localhost:{env.pageserver.service_port.pg}'"
lines[i] = f"listen_http_addr = 'localhost:{env.pageserver.service_port.http}'" (env.repo_dir / "config").write_text("\n".join(cli_config_lines))
if line.startswith("listen_pg_addr"):
lines[i] = f"listen_pg_addr = 'localhost:{env.pageserver.service_port.pg}'"
(env.repo_dir / "config").write_text("\n".join(lines))

View File

@@ -40,6 +40,7 @@ def test_timeline_delete(neon_simple_env: NeonEnv):
with pytest.raises( with pytest.raises(
PageserverApiException, match="Cannot delete timeline which has child timelines" PageserverApiException, match="Cannot delete timeline which has child timelines"
): ):
timeline_path = ( timeline_path = (
env.repo_dir env.repo_dir
/ "tenants" / "tenants"

View File

@@ -90,6 +90,7 @@ def test_timeline_size_createdropdb(neon_simple_env: NeonEnv):
cur.execute("CREATE DATABASE foodb") cur.execute("CREATE DATABASE foodb")
with closing(pgmain.connect(dbname="foodb")) as conn: with closing(pgmain.connect(dbname="foodb")) as conn:
with conn.cursor() as cur2: with conn.cursor() as cur2:
cur2.execute("CREATE TABLE foo (t text)") cur2.execute("CREATE TABLE foo (t text)")
cur2.execute( cur2.execute(
""" """
@@ -307,6 +308,7 @@ def test_timeline_initial_logical_size_calculation_cancellation(
def test_timeline_physical_size_init( def test_timeline_physical_size_init(
neon_env_builder: NeonEnvBuilder, remote_storage_kind: Optional[RemoteStorageKind] neon_env_builder: NeonEnvBuilder, remote_storage_kind: Optional[RemoteStorageKind]
): ):
if remote_storage_kind is not None: if remote_storage_kind is not None:
neon_env_builder.enable_remote_storage( neon_env_builder.enable_remote_storage(
remote_storage_kind, "test_timeline_physical_size_init" remote_storage_kind, "test_timeline_physical_size_init"
@@ -383,6 +385,7 @@ def test_timeline_physical_size_post_checkpoint(
def test_timeline_physical_size_post_compaction( def test_timeline_physical_size_post_compaction(
neon_env_builder: NeonEnvBuilder, remote_storage_kind: Optional[RemoteStorageKind] neon_env_builder: NeonEnvBuilder, remote_storage_kind: Optional[RemoteStorageKind]
): ):
if remote_storage_kind is not None: if remote_storage_kind is not None:
neon_env_builder.enable_remote_storage( neon_env_builder.enable_remote_storage(
remote_storage_kind, "test_timeline_physical_size_init" remote_storage_kind, "test_timeline_physical_size_init"
@@ -437,6 +440,7 @@ def test_timeline_physical_size_post_compaction(
def test_timeline_physical_size_post_gc( def test_timeline_physical_size_post_gc(
neon_env_builder: NeonEnvBuilder, remote_storage_kind: Optional[RemoteStorageKind] neon_env_builder: NeonEnvBuilder, remote_storage_kind: Optional[RemoteStorageKind]
): ):
if remote_storage_kind is not None: if remote_storage_kind is not None:
neon_env_builder.enable_remote_storage( neon_env_builder.enable_remote_storage(
remote_storage_kind, "test_timeline_physical_size_init" remote_storage_kind, "test_timeline_physical_size_init"

View File

@@ -7,6 +7,7 @@ from fixtures.neon_fixtures import NeonEnvBuilder
# Test truncation of FSM and VM forks of a relation # Test truncation of FSM and VM forks of a relation
# #
def test_truncate(neon_env_builder: NeonEnvBuilder, zenbenchmark): def test_truncate(neon_env_builder: NeonEnvBuilder, zenbenchmark):
env = neon_env_builder.init_start() env = neon_env_builder.init_start()
n_records = 10000 n_records = 10000
n_iter = 10 n_iter = 10

Some files were not shown because too many files have changed in this diff Show More