mirror of
https://github.com/neondatabase/neon.git
synced 2026-08-15 02:28:41 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4bd82cb81 | |||
| c64798956d | |||
| 39bbaecb03 | |||
| 26dca374eb | |||
| 9787227c35 | |||
| ef80a902c8 | |||
| 66cdba990a | |||
| 82484e8241 | |||
| 36fee50f4d | |||
| 330083638f | |||
| 952d6e43a2 | |||
| b6447462dc | |||
| b190c3e6c3 | |||
| f4db85de40 | |||
| 210be6b6ab | |||
| daa79b150f | |||
| db14355367 | |||
| cb83495744 | |||
| f4f300732a | |||
| ccf653c1f4 | |||
| 2d6a022bb8 | |||
| 2cdf07f12c | |||
| 200a520e6c | |||
| 4e359db4c7 | |||
| be177f82dc | |||
| 339a3e3146 | |||
| a560b28829 | |||
| 024109fbeb | |||
| 2b25f0dfa0 | |||
| 057cceb559 | |||
| ae805b985d | |||
| 85e76090ea | |||
| 08e7d2407b | |||
| ab2757f64a | |||
| e5617021a7 | |||
| 83ba02b431 | |||
| 37ecebe45b | |||
| 6052ecee07 | |||
| e11ba24ec5 | |||
| f276f21636 | |||
| 7126197000 | |||
| afc48e2cd9 | |||
| df52587bef | |||
| 35bb10757d | |||
| 2a3f54002c |
@@ -57,14 +57,14 @@ runs:
|
|||||||
if ! which allure; then
|
if ! which allure; then
|
||||||
ALLURE_ZIP=allure-${ALLURE_VERSION}.zip
|
ALLURE_ZIP=allure-${ALLURE_VERSION}.zip
|
||||||
wget -q https://github.com/allure-framework/allure2/releases/download/${ALLURE_VERSION}/${ALLURE_ZIP}
|
wget -q https://github.com/allure-framework/allure2/releases/download/${ALLURE_VERSION}/${ALLURE_ZIP}
|
||||||
echo "${ALLURE_ZIP_MD5} ${ALLURE_ZIP}" | md5sum -c
|
echo "${ALLURE_ZIP_SHA256} ${ALLURE_ZIP}" | sha256sum --check
|
||||||
unzip -q ${ALLURE_ZIP}
|
unzip -q ${ALLURE_ZIP}
|
||||||
echo "$(pwd)/allure-${ALLURE_VERSION}/bin" >> $GITHUB_PATH
|
echo "$(pwd)/allure-${ALLURE_VERSION}/bin" >> $GITHUB_PATH
|
||||||
rm -f ${ALLURE_ZIP}
|
rm -f ${ALLURE_ZIP}
|
||||||
fi
|
fi
|
||||||
env:
|
env:
|
||||||
ALLURE_VERSION: 2.22.0
|
ALLURE_VERSION: 2.22.1
|
||||||
ALLURE_ZIP_MD5: d5c9f0989b896482536956340a7d5ec9
|
ALLURE_ZIP_SHA256: fdc7a62d94b14c5e0bf25198ae1feded6b005fdbed864b4d3cb4e5e901720b0b
|
||||||
|
|
||||||
# Potentially we could have several running build for the same key (for example, for the main branch), so we use improvised lock for this
|
# Potentially we could have several running build for the same key (for example, for the main branch), so we use improvised lock for this
|
||||||
- name: Acquire lock
|
- name: Acquire lock
|
||||||
|
|||||||
@@ -36,14 +36,6 @@ inputs:
|
|||||||
description: 'Region name for real s3 tests'
|
description: 'Region name for real s3 tests'
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
real_s3_access_key_id:
|
|
||||||
description: 'Access key id'
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
real_s3_secret_access_key:
|
|
||||||
description: 'Secret access key'
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
rerun_flaky:
|
rerun_flaky:
|
||||||
description: 'Whether to rerun flaky tests'
|
description: 'Whether to rerun flaky tests'
|
||||||
required: false
|
required: false
|
||||||
@@ -104,8 +96,6 @@ runs:
|
|||||||
COMPATIBILITY_POSTGRES_DISTRIB_DIR: /tmp/neon-previous/pg_install
|
COMPATIBILITY_POSTGRES_DISTRIB_DIR: /tmp/neon-previous/pg_install
|
||||||
TEST_OUTPUT: /tmp/test_output
|
TEST_OUTPUT: /tmp/test_output
|
||||||
BUILD_TYPE: ${{ inputs.build_type }}
|
BUILD_TYPE: ${{ inputs.build_type }}
|
||||||
AWS_ACCESS_KEY_ID: ${{ inputs.real_s3_access_key_id }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ inputs.real_s3_secret_access_key }}
|
|
||||||
COMPATIBILITY_SNAPSHOT_DIR: /tmp/compatibility_snapshot_pg${{ inputs.pg_version }}
|
COMPATIBILITY_SNAPSHOT_DIR: /tmp/compatibility_snapshot_pg${{ inputs.pg_version }}
|
||||||
ALLOW_BACKWARD_COMPATIBILITY_BREAKAGE: contains(github.event.pull_request.labels.*.name, 'backward compatibility breakage')
|
ALLOW_BACKWARD_COMPATIBILITY_BREAKAGE: contains(github.event.pull_request.labels.*.name, 'backward compatibility breakage')
|
||||||
ALLOW_FORWARD_COMPATIBILITY_BREAKAGE: contains(github.event.pull_request.labels.*.name, 'forward compatibility breakage')
|
ALLOW_FORWARD_COMPATIBILITY_BREAKAGE: contains(github.event.pull_request.labels.*.name, 'forward compatibility breakage')
|
||||||
|
|||||||
@@ -346,10 +346,8 @@ jobs:
|
|||||||
test_selection: regress
|
test_selection: regress
|
||||||
needs_postgres_source: true
|
needs_postgres_source: true
|
||||||
run_with_real_s3: true
|
run_with_real_s3: true
|
||||||
real_s3_bucket: ci-tests-s3
|
real_s3_bucket: neon-github-ci-tests
|
||||||
real_s3_region: us-west-2
|
real_s3_region: eu-central-1
|
||||||
real_s3_access_key_id: "${{ secrets.AWS_ACCESS_KEY_ID_CI_TESTS_S3 }}"
|
|
||||||
real_s3_secret_access_key: "${{ secrets.AWS_SECRET_ACCESS_KEY_CI_TESTS_S3 }}"
|
|
||||||
rerun_flaky: true
|
rerun_flaky: true
|
||||||
pg_version: ${{ matrix.pg_version }}
|
pg_version: ${{ matrix.pg_version }}
|
||||||
env:
|
env:
|
||||||
@@ -409,9 +407,7 @@ jobs:
|
|||||||
uses: ./.github/actions/allure-report-generate
|
uses: ./.github/actions/allure-report-generate
|
||||||
|
|
||||||
- uses: actions/github-script@v6
|
- uses: actions/github-script@v6
|
||||||
if: >
|
if: ${{ !cancelled() }}
|
||||||
!cancelled() &&
|
|
||||||
github.event_name == 'pull_request'
|
|
||||||
with:
|
with:
|
||||||
# Retry script for 5XX server errors: https://github.com/actions/github-script#retries
|
# Retry script for 5XX server errors: https://github.com/actions/github-script#retries
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -421,7 +417,7 @@ jobs:
|
|||||||
reportJsonUrl: "${{ steps.create-allure-report.outputs.report-json-url }}",
|
reportJsonUrl: "${{ steps.create-allure-report.outputs.report-json-url }}",
|
||||||
}
|
}
|
||||||
|
|
||||||
const script = require("./scripts/pr-comment-test-report.js")
|
const script = require("./scripts/comment-test-report.js")
|
||||||
await script({
|
await script({
|
||||||
github,
|
github,
|
||||||
context,
|
context,
|
||||||
@@ -496,19 +492,24 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha || github.sha }}
|
COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
run: |
|
run: |
|
||||||
scripts/coverage \
|
scripts/coverage --dir=/tmp/coverage \
|
||||||
--dir=/tmp/coverage report \
|
report \
|
||||||
--input-objects=/tmp/coverage/binaries.list \
|
--input-objects=/tmp/coverage/binaries.list \
|
||||||
--commit-url=${COMMIT_URL} \
|
--commit-url=${COMMIT_URL} \
|
||||||
--format=github
|
--format=github
|
||||||
|
|
||||||
|
scripts/coverage --dir=/tmp/coverage \
|
||||||
|
report \
|
||||||
|
--input-objects=/tmp/coverage/binaries.list \
|
||||||
|
--format=lcov
|
||||||
|
|
||||||
- name: Upload coverage report
|
- name: Upload coverage report
|
||||||
id: upload-coverage-report
|
id: upload-coverage-report
|
||||||
env:
|
env:
|
||||||
BUCKET: neon-github-public-dev
|
BUCKET: neon-github-public-dev
|
||||||
COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
run: |
|
run: |
|
||||||
aws s3 cp --only-show-errors --recursive /tmp/coverage/report s3://neon-github-public-dev/code-coverage/${COMMIT_SHA}
|
aws s3 cp --only-show-errors --recursive /tmp/coverage/report s3://${BUCKET}/code-coverage/${COMMIT_SHA}
|
||||||
|
|
||||||
REPORT_URL=https://${BUCKET}.s3.amazonaws.com/code-coverage/${COMMIT_SHA}/index.html
|
REPORT_URL=https://${BUCKET}.s3.amazonaws.com/code-coverage/${COMMIT_SHA}/index.html
|
||||||
echo "report-url=${REPORT_URL}" >> $GITHUB_OUTPUT
|
echo "report-url=${REPORT_URL}" >> $GITHUB_OUTPUT
|
||||||
@@ -663,6 +664,9 @@ jobs:
|
|||||||
project: nrdv0s4kcs
|
project: nrdv0s4kcs
|
||||||
push: true
|
push: true
|
||||||
tags: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:depot-${{needs.tag.outputs.build-tag}}
|
tags: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:depot-${{needs.tag.outputs.build-tag}}
|
||||||
|
build-args: |
|
||||||
|
GIT_VERSION=${{ github.sha }}
|
||||||
|
REPOSITORY=369495373322.dkr.ecr.eu-central-1.amazonaws.com
|
||||||
|
|
||||||
compute-tools-image:
|
compute-tools-image:
|
||||||
runs-on: [ self-hosted, gen3, large ]
|
runs-on: [ self-hosted, gen3, large ]
|
||||||
@@ -777,7 +781,7 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
shell: sh -eu {0}
|
shell: sh -eu {0}
|
||||||
env:
|
env:
|
||||||
VM_BUILDER_VERSION: v0.7.3-alpha3
|
VM_BUILDER_VERSION: v0.8.0
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -798,7 +802,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build vm image
|
- name: Build vm image
|
||||||
run: |
|
run: |
|
||||||
./vm-builder -src=369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}} -dst=369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
./vm-builder -enable-file-cache -src=369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}} -dst=369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
||||||
|
|
||||||
- name: Pushing vm-compute-node image
|
- name: Pushing vm-compute-node image
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
Generated
+290
-338
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -3,6 +3,7 @@ members = [
|
|||||||
"compute_tools",
|
"compute_tools",
|
||||||
"control_plane",
|
"control_plane",
|
||||||
"pageserver",
|
"pageserver",
|
||||||
|
"pageserver/ctl",
|
||||||
"proxy",
|
"proxy",
|
||||||
"safekeeper",
|
"safekeeper",
|
||||||
"storage_broker",
|
"storage_broker",
|
||||||
@@ -22,7 +23,7 @@ async-stream = "0.3"
|
|||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
atty = "0.2.14"
|
atty = "0.2.14"
|
||||||
aws-config = { version = "0.55", default-features = false, features=["rustls"] }
|
aws-config = { version = "0.55", default-features = false, features=["rustls"] }
|
||||||
aws-sdk-s3 = "0.25"
|
aws-sdk-s3 = "0.27"
|
||||||
aws-smithy-http = "0.55"
|
aws-smithy-http = "0.55"
|
||||||
aws-credential-types = "0.55"
|
aws-credential-types = "0.55"
|
||||||
aws-types = "0.55"
|
aws-types = "0.55"
|
||||||
|
|||||||
+2
-4
@@ -47,8 +47,7 @@ RUN set -e \
|
|||||||
&& mold -run cargo build \
|
&& mold -run cargo build \
|
||||||
--bin pg_sni_router \
|
--bin pg_sni_router \
|
||||||
--bin pageserver \
|
--bin pageserver \
|
||||||
--bin pageserver_binutils \
|
--bin pagectl \
|
||||||
--bin draw_timeline_dir \
|
|
||||||
--bin safekeeper \
|
--bin safekeeper \
|
||||||
--bin storage_broker \
|
--bin storage_broker \
|
||||||
--bin proxy \
|
--bin proxy \
|
||||||
@@ -73,8 +72,7 @@ RUN set -e \
|
|||||||
|
|
||||||
COPY --from=build --chown=neon:neon /home/nonroot/target/release/pg_sni_router /usr/local/bin
|
COPY --from=build --chown=neon:neon /home/nonroot/target/release/pg_sni_router /usr/local/bin
|
||||||
COPY --from=build --chown=neon:neon /home/nonroot/target/release/pageserver /usr/local/bin
|
COPY --from=build --chown=neon:neon /home/nonroot/target/release/pageserver /usr/local/bin
|
||||||
COPY --from=build --chown=neon:neon /home/nonroot/target/release/pageserver_binutils /usr/local/bin
|
COPY --from=build --chown=neon:neon /home/nonroot/target/release/pagectl /usr/local/bin
|
||||||
COPY --from=build --chown=neon:neon /home/nonroot/target/release/draw_timeline_dir /usr/local/bin
|
|
||||||
COPY --from=build --chown=neon:neon /home/nonroot/target/release/safekeeper /usr/local/bin
|
COPY --from=build --chown=neon:neon /home/nonroot/target/release/safekeeper /usr/local/bin
|
||||||
COPY --from=build --chown=neon:neon /home/nonroot/target/release/storage_broker /usr/local/bin
|
COPY --from=build --chown=neon:neon /home/nonroot/target/release/storage_broker /usr/local/bin
|
||||||
COPY --from=build --chown=neon:neon /home/nonroot/target/release/proxy /usr/local/bin
|
COPY --from=build --chown=neon:neon /home/nonroot/target/release/proxy /usr/local/bin
|
||||||
|
|||||||
@@ -632,6 +632,7 @@ RUN apt update && \
|
|||||||
libxml2 \
|
libxml2 \
|
||||||
libxslt1.1 \
|
libxslt1.1 \
|
||||||
libzstd1 \
|
libzstd1 \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
procps && \
|
procps && \
|
||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
|
||||||
localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8
|
localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8
|
||||||
|
|||||||
@@ -362,6 +362,8 @@ impl ComputeNode {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Proceed with post-startup configuration. Note, that order of operations is important.
|
// Proceed with post-startup configuration. Note, that order of operations is important.
|
||||||
|
// Disable DDL forwarding because control plane already knows about these roles/databases.
|
||||||
|
client.simple_query("SET neon.forward_ddl = false")?;
|
||||||
let spec = &compute_state.pspec.as_ref().expect("spec must be set").spec;
|
let spec = &compute_state.pspec.as_ref().expect("spec must be set").spec;
|
||||||
handle_roles(spec, &mut client)?;
|
handle_roles(spec, &mut client)?;
|
||||||
handle_databases(spec, &mut client)?;
|
handle_databases(spec, &mut client)?;
|
||||||
@@ -403,7 +405,9 @@ impl ComputeNode {
|
|||||||
self.pg_reload_conf(&mut client)?;
|
self.pg_reload_conf(&mut client)?;
|
||||||
|
|
||||||
// Proceed with post-startup configuration. Note, that order of operations is important.
|
// Proceed with post-startup configuration. Note, that order of operations is important.
|
||||||
|
// Disable DDL forwarding because control plane already knows about these roles/databases.
|
||||||
if spec.mode == ComputeMode::Primary {
|
if spec.mode == ComputeMode::Primary {
|
||||||
|
client.simple_query("SET neon.forward_ddl = false")?;
|
||||||
handle_roles(&spec, &mut client)?;
|
handle_roles(&spec, &mut client)?;
|
||||||
handle_databases(&spec, &mut client)?;
|
handle_databases(&spec, &mut client)?;
|
||||||
handle_role_deletions(&spec, self.connstr.as_str(), &mut client)?;
|
handle_role_deletions(&spec, self.connstr.as_str(), &mut client)?;
|
||||||
|
|||||||
@@ -33,5 +33,7 @@ pub fn init_tracing_and_logging(default_log_level: &str) -> anyhow::Result<()> {
|
|||||||
.init();
|
.init();
|
||||||
tracing::info!("logging and tracing started");
|
tracing::info!("logging and tracing started");
|
||||||
|
|
||||||
|
utils::logging::replace_panic_hook_with_tracing_panic_hook().forget();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,9 +121,8 @@ impl RoleExt for Role {
|
|||||||
/// string of arguments.
|
/// string of arguments.
|
||||||
fn to_pg_options(&self) -> String {
|
fn to_pg_options(&self) -> String {
|
||||||
// XXX: consider putting LOGIN as a default option somewhere higher, e.g. in control-plane.
|
// XXX: consider putting LOGIN as a default option somewhere higher, e.g. in control-plane.
|
||||||
// For now, we do not use generic `options` for roles. Once used, add
|
let mut params: String = self.options.as_pg_options();
|
||||||
// `self.options.as_pg_options()` somewhere here.
|
params.push_str(" LOGIN");
|
||||||
let mut params: String = "LOGIN".to_string();
|
|
||||||
|
|
||||||
if let Some(pass) = &self.encrypted_password {
|
if let Some(pass) = &self.encrypted_password {
|
||||||
// Some time ago we supported only md5 and treated all encrypted_password as md5.
|
// Some time ago we supported only md5 and treated all encrypted_password as md5.
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ fn do_control_plane_request(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request spec from the control-plane by compute_id. If `NEON_CONSOLE_JWT`
|
/// Request spec from the control-plane by compute_id. If `NEON_CONTROL_PLANE_TOKEN`
|
||||||
/// env variable is set, it will be used for authorization.
|
/// env variable is set, it will be used for authorization.
|
||||||
pub fn get_spec_from_control_plane(
|
pub fn get_spec_from_control_plane(
|
||||||
base_uri: &str,
|
base_uri: &str,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ mod pg_helpers_tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
spec.cluster.roles.first().unwrap().to_pg_options(),
|
spec.cluster.roles.first().unwrap().to_pg_options(),
|
||||||
"LOGIN PASSWORD 'md56b1d16b78004bbd51fa06af9eda75972'"
|
" LOGIN PASSWORD 'md56b1d16b78004bbd51fa06af9eda75972'"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const DEFAULT_PAGESERVER_ID: NodeId = NodeId(1);
|
|||||||
const DEFAULT_BRANCH_NAME: &str = "main";
|
const DEFAULT_BRANCH_NAME: &str = "main";
|
||||||
project_git_version!(GIT_VERSION);
|
project_git_version!(GIT_VERSION);
|
||||||
|
|
||||||
const DEFAULT_PG_VERSION: &str = "14";
|
const DEFAULT_PG_VERSION: &str = "15";
|
||||||
|
|
||||||
fn default_conf() -> String {
|
fn default_conf() -> String {
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use utils::{
|
|||||||
|
|
||||||
use crate::safekeeper::SafekeeperNode;
|
use crate::safekeeper::SafekeeperNode;
|
||||||
|
|
||||||
pub const DEFAULT_PG_VERSION: u32 = 14;
|
pub const DEFAULT_PG_VERSION: u32 = 15;
|
||||||
|
|
||||||
//
|
//
|
||||||
// This data structures represents neon_local CLI config
|
// This data structures represents neon_local CLI config
|
||||||
|
|||||||
@@ -369,7 +369,16 @@ impl PageServerNode {
|
|||||||
evictions_low_residence_duration_metric_threshold: settings
|
evictions_low_residence_duration_metric_threshold: settings
|
||||||
.remove("evictions_low_residence_duration_metric_threshold")
|
.remove("evictions_low_residence_duration_metric_threshold")
|
||||||
.map(|x| x.to_string()),
|
.map(|x| x.to_string()),
|
||||||
|
gc_feedback: settings
|
||||||
|
.remove("gc_feedback")
|
||||||
|
.map(|x| x.parse::<bool>())
|
||||||
|
.transpose()
|
||||||
|
.context("Failed to parse 'gc_feedback' as bool")?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If tenant ID was not specified, generate one
|
||||||
|
let new_tenant_id = new_tenant_id.unwrap_or(TenantId::generate());
|
||||||
|
|
||||||
let request = models::TenantCreateRequest {
|
let request = models::TenantCreateRequest {
|
||||||
new_tenant_id,
|
new_tenant_id,
|
||||||
config,
|
config,
|
||||||
@@ -459,6 +468,11 @@ impl PageServerNode {
|
|||||||
evictions_low_residence_duration_metric_threshold: settings
|
evictions_low_residence_duration_metric_threshold: settings
|
||||||
.remove("evictions_low_residence_duration_metric_threshold")
|
.remove("evictions_low_residence_duration_metric_threshold")
|
||||||
.map(|x| x.to_string()),
|
.map(|x| x.to_string()),
|
||||||
|
gc_feedback: settings
|
||||||
|
.remove("gc_feedback")
|
||||||
|
.map(|x| x.parse::<bool>())
|
||||||
|
.transpose()
|
||||||
|
.context("Failed to parse 'gc_feedback' as bool")?,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -495,6 +509,9 @@ impl PageServerNode {
|
|||||||
ancestor_timeline_id: Option<TimelineId>,
|
ancestor_timeline_id: Option<TimelineId>,
|
||||||
pg_version: Option<u32>,
|
pg_version: Option<u32>,
|
||||||
) -> anyhow::Result<TimelineInfo> {
|
) -> anyhow::Result<TimelineInfo> {
|
||||||
|
// If timeline ID was not specified, generate one
|
||||||
|
let new_timeline_id = new_timeline_id.unwrap_or(TimelineId::generate());
|
||||||
|
|
||||||
self.http_request(
|
self.http_request(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
format!("{}/tenant/{}/timeline", self.http_base_url, tenant_id),
|
format!("{}/tenant/{}/timeline", self.http_base_url, tenant_id),
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -eux
|
set -eux
|
||||||
|
|
||||||
|
# Generate a random tenant or timeline ID
|
||||||
|
#
|
||||||
|
# Takes a variable name as argument. The result is stored in that variable.
|
||||||
|
generate_id() {
|
||||||
|
local -n resvar=$1
|
||||||
|
printf -v resvar '%08x%08x%08x%08x' $SRANDOM $SRANDOM $SRANDOM $SRANDOM
|
||||||
|
}
|
||||||
|
|
||||||
PG_VERSION=${PG_VERSION:-14}
|
PG_VERSION=${PG_VERSION:-14}
|
||||||
|
|
||||||
SPEC_FILE_ORG=/var/db/postgres/specs/spec.json
|
SPEC_FILE_ORG=/var/db/postgres/specs/spec.json
|
||||||
@@ -13,29 +21,29 @@ done
|
|||||||
echo "Page server is ready."
|
echo "Page server is ready."
|
||||||
|
|
||||||
echo "Create a tenant and timeline"
|
echo "Create a tenant and timeline"
|
||||||
|
generate_id tenant_id
|
||||||
PARAMS=(
|
PARAMS=(
|
||||||
-sb
|
-sb
|
||||||
-X POST
|
-X POST
|
||||||
-H "Content-Type: application/json"
|
-H "Content-Type: application/json"
|
||||||
-d "{}"
|
-d "{\"new_tenant_id\": \"${tenant_id}\"}"
|
||||||
http://pageserver:9898/v1/tenant/
|
http://pageserver:9898/v1/tenant/
|
||||||
)
|
)
|
||||||
tenant_id=$(curl "${PARAMS[@]}" | sed 's/"//g')
|
result=$(curl "${PARAMS[@]}")
|
||||||
|
echo $result | jq .
|
||||||
|
|
||||||
|
generate_id timeline_id
|
||||||
PARAMS=(
|
PARAMS=(
|
||||||
-sb
|
-sb
|
||||||
-X POST
|
-X POST
|
||||||
-H "Content-Type: application/json"
|
-H "Content-Type: application/json"
|
||||||
-d "{\"tenant_id\":\"${tenant_id}\", \"pg_version\": ${PG_VERSION}}"
|
-d "{\"new_timeline_id\": \"${timeline_id}\", \"pg_version\": ${PG_VERSION}}"
|
||||||
"http://pageserver:9898/v1/tenant/${tenant_id}/timeline/"
|
"http://pageserver:9898/v1/tenant/${tenant_id}/timeline/"
|
||||||
)
|
)
|
||||||
result=$(curl "${PARAMS[@]}")
|
result=$(curl "${PARAMS[@]}")
|
||||||
echo $result | jq .
|
echo $result | jq .
|
||||||
|
|
||||||
echo "Overwrite tenant id and timeline id in spec file"
|
echo "Overwrite tenant id and timeline id in spec file"
|
||||||
tenant_id=$(echo ${result} | jq -r .tenant_id)
|
|
||||||
timeline_id=$(echo ${result} | jq -r .timeline_id)
|
|
||||||
|
|
||||||
sed "s/TENANT_ID/${tenant_id}/" ${SPEC_FILE_ORG} > ${SPEC_FILE}
|
sed "s/TENANT_ID/${tenant_id}/" ${SPEC_FILE_ORG} > ${SPEC_FILE}
|
||||||
sed -i "s/TIMELINE_ID/${timeline_id}/" ${SPEC_FILE}
|
sed -i "s/TIMELINE_ID/${timeline_id}/" ${SPEC_FILE}
|
||||||
|
|
||||||
|
|||||||
@@ -52,9 +52,7 @@ completion, or shield the rest of the code from surprise cancellations
|
|||||||
by spawning a separate task. The code that handles incoming HTTP
|
by spawning a separate task. The code that handles incoming HTTP
|
||||||
requests, for example, spawns a separate task for each request,
|
requests, for example, spawns a separate task for each request,
|
||||||
because Hyper will drop the request-handling Future if the HTTP
|
because Hyper will drop the request-handling Future if the HTTP
|
||||||
connection is lost. (FIXME: our HTTP handlers do not do that
|
connection is lost.
|
||||||
currently, but we should fix that. See [issue
|
|
||||||
3478](https://github.com/neondatabase/neon/issues/3478)).
|
|
||||||
|
|
||||||
|
|
||||||
#### How to cancel, then?
|
#### How to cancel, then?
|
||||||
|
|||||||
@@ -18,7 +18,29 @@ use crate::reltag::RelTag;
|
|||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use bytes::{BufMut, Bytes, BytesMut};
|
use bytes::{BufMut, Bytes, BytesMut};
|
||||||
|
|
||||||
/// A state of a tenant in pageserver's memory.
|
/// The state of a tenant in this pageserver.
|
||||||
|
///
|
||||||
|
/// ```mermaid
|
||||||
|
/// stateDiagram-v2
|
||||||
|
///
|
||||||
|
/// [*] --> Loading: spawn_load()
|
||||||
|
/// [*] --> Attaching: spawn_attach()
|
||||||
|
///
|
||||||
|
/// Loading --> Activating: activate()
|
||||||
|
/// Attaching --> Activating: activate()
|
||||||
|
/// Activating --> Active: infallible
|
||||||
|
///
|
||||||
|
/// Loading --> Broken: load() failure
|
||||||
|
/// Attaching --> Broken: attach() failure
|
||||||
|
///
|
||||||
|
/// Active --> Stopping: set_stopping(), part of shutdown & detach
|
||||||
|
/// Stopping --> Broken: late error in remove_tenant_from_memory
|
||||||
|
///
|
||||||
|
/// Broken --> [*]: ignore / detach / shutdown
|
||||||
|
/// Stopping --> [*]: remove_from_memory complete
|
||||||
|
///
|
||||||
|
/// Active --> Broken: cfg(testing)-only tenant break point
|
||||||
|
/// ```
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone,
|
Clone,
|
||||||
PartialEq,
|
PartialEq,
|
||||||
@@ -26,40 +48,63 @@ use bytes::{BufMut, Bytes, BytesMut};
|
|||||||
serde::Serialize,
|
serde::Serialize,
|
||||||
serde::Deserialize,
|
serde::Deserialize,
|
||||||
strum_macros::Display,
|
strum_macros::Display,
|
||||||
strum_macros::EnumString,
|
|
||||||
strum_macros::EnumVariantNames,
|
strum_macros::EnumVariantNames,
|
||||||
strum_macros::AsRefStr,
|
strum_macros::AsRefStr,
|
||||||
strum_macros::IntoStaticStr,
|
strum_macros::IntoStaticStr,
|
||||||
)]
|
)]
|
||||||
#[serde(tag = "slug", content = "data")]
|
#[serde(tag = "slug", content = "data")]
|
||||||
pub enum TenantState {
|
pub enum TenantState {
|
||||||
/// This tenant is being loaded from local disk
|
/// This tenant is being loaded from local disk.
|
||||||
|
///
|
||||||
|
/// `set_stopping()` and `set_broken()` do not work in this state and wait for it to pass.
|
||||||
Loading,
|
Loading,
|
||||||
/// This tenant is being downloaded from cloud storage.
|
/// This tenant is being attached to the pageserver.
|
||||||
|
///
|
||||||
|
/// `set_stopping()` and `set_broken()` do not work in this state and wait for it to pass.
|
||||||
Attaching,
|
Attaching,
|
||||||
/// Tenant is fully operational
|
/// The tenant is transitioning from Loading/Attaching to Active.
|
||||||
|
///
|
||||||
|
/// While in this state, the individual timelines are being activated.
|
||||||
|
///
|
||||||
|
/// `set_stopping()` and `set_broken()` do not work in this state and wait for it to pass.
|
||||||
|
Activating(ActivatingFrom),
|
||||||
|
/// The tenant has finished activating and is open for business.
|
||||||
|
///
|
||||||
|
/// Transitions out of this state are possible through `set_stopping()` and `set_broken()`.
|
||||||
Active,
|
Active,
|
||||||
/// A tenant is recognized by pageserver, but it is being detached or the
|
/// The tenant is recognized by pageserver, but it is being detached or the
|
||||||
/// system is being shut down.
|
/// system is being shut down.
|
||||||
|
///
|
||||||
|
/// Transitions out of this state are possible through `set_broken()`.
|
||||||
Stopping,
|
Stopping,
|
||||||
/// A tenant is recognized by the pageserver, but can no longer be used for
|
/// The tenant is recognized by the pageserver, but can no longer be used for
|
||||||
/// any operations, because it failed to be activated.
|
/// any operations.
|
||||||
|
///
|
||||||
|
/// If the tenant fails to load or attach, it will transition to this state
|
||||||
|
/// and it is guaranteed that no background tasks are running in its name.
|
||||||
|
///
|
||||||
|
/// The other way to transition into this state is from `Stopping` state
|
||||||
|
/// through `set_broken()` called from `remove_tenant_from_memory()`. That happens
|
||||||
|
/// if the cleanup future executed by `remove_tenant_from_memory()` fails.
|
||||||
Broken { reason: String, backtrace: String },
|
Broken { reason: String, backtrace: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TenantState {
|
impl TenantState {
|
||||||
pub fn attachment_status(&self) -> TenantAttachmentStatus {
|
pub fn attachment_status(&self) -> TenantAttachmentStatus {
|
||||||
use TenantAttachmentStatus::*;
|
use TenantAttachmentStatus::*;
|
||||||
|
|
||||||
|
// Below TenantState::Activating is used as "transient" or "transparent" state for
|
||||||
|
// attachment_status determining.
|
||||||
match self {
|
match self {
|
||||||
// The attach procedure writes the marker file before adding the Attaching tenant to the tenants map.
|
// The attach procedure writes the marker file before adding the Attaching tenant to the tenants map.
|
||||||
// So, technically, we can return Attached here.
|
// So, technically, we can return Attached here.
|
||||||
// However, as soon as Console observes Attached, it will proceed with the Postgres-level health check.
|
// However, as soon as Console observes Attached, it will proceed with the Postgres-level health check.
|
||||||
// But, our attach task might still be fetching the remote timelines, etc.
|
// But, our attach task might still be fetching the remote timelines, etc.
|
||||||
// So, return `Maybe` while Attaching, making Console wait for the attach task to finish.
|
// So, return `Maybe` while Attaching, making Console wait for the attach task to finish.
|
||||||
Self::Attaching => Maybe,
|
Self::Attaching | Self::Activating(ActivatingFrom::Attaching) => Maybe,
|
||||||
// tenant mgr startup distinguishes attaching from loading via marker file.
|
// tenant mgr startup distinguishes attaching from loading via marker file.
|
||||||
// If it's loading, there is no attach marker file, i.e., attach had finished in the past.
|
// If it's loading, there is no attach marker file, i.e., attach had finished in the past.
|
||||||
Self::Loading => Attached,
|
Self::Loading | Self::Activating(ActivatingFrom::Loading) => Attached,
|
||||||
// We only reach Active after successful load / attach.
|
// We only reach Active after successful load / attach.
|
||||||
// So, call atttachment status Attached.
|
// So, call atttachment status Attached.
|
||||||
Self::Active => Attached,
|
Self::Active => Attached,
|
||||||
@@ -98,6 +143,15 @@ impl std::fmt::Debug for TenantState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The only [`TenantState`] variants we could be `TenantState::Activating` from.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum ActivatingFrom {
|
||||||
|
/// Arrived to [`TenantState::Activating`] from [`TenantState::Loading`]
|
||||||
|
Loading,
|
||||||
|
/// Arrived to [`TenantState::Activating`] from [`TenantState::Attaching`]
|
||||||
|
Attaching,
|
||||||
|
}
|
||||||
|
|
||||||
/// A state of a timeline in pageserver's memory.
|
/// A state of a timeline in pageserver's memory.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum TimelineState {
|
pub enum TimelineState {
|
||||||
@@ -118,9 +172,8 @@ pub enum TimelineState {
|
|||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct TimelineCreateRequest {
|
pub struct TimelineCreateRequest {
|
||||||
#[serde(default)]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
pub new_timeline_id: TimelineId,
|
||||||
pub new_timeline_id: Option<TimelineId>,
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
pub ancestor_timeline_id: Option<TimelineId>,
|
pub ancestor_timeline_id: Option<TimelineId>,
|
||||||
@@ -131,12 +184,11 @@ pub struct TimelineCreateRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct TenantCreateRequest {
|
pub struct TenantCreateRequest {
|
||||||
#[serde(default)]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
pub new_tenant_id: TenantId,
|
||||||
pub new_tenant_id: Option<TenantId>,
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub config: TenantConfig, // as we have a flattened field, we should reject all unknown fields in it
|
pub config: TenantConfig, // as we have a flattened field, we should reject all unknown fields in it
|
||||||
}
|
}
|
||||||
@@ -171,6 +223,7 @@ pub struct TenantConfig {
|
|||||||
pub eviction_policy: Option<serde_json::Value>,
|
pub eviction_policy: Option<serde_json::Value>,
|
||||||
pub min_resident_size_override: Option<u64>,
|
pub min_resident_size_override: Option<u64>,
|
||||||
pub evictions_low_residence_duration_metric_threshold: Option<String>,
|
pub evictions_low_residence_duration_metric_threshold: Option<String>,
|
||||||
|
pub gc_feedback: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
@@ -184,10 +237,10 @@ pub struct StatusResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TenantCreateRequest {
|
impl TenantCreateRequest {
|
||||||
pub fn new(new_tenant_id: Option<TenantId>) -> TenantCreateRequest {
|
pub fn new(new_tenant_id: TenantId) -> TenantCreateRequest {
|
||||||
TenantCreateRequest {
|
TenantCreateRequest {
|
||||||
new_tenant_id,
|
new_tenant_id,
|
||||||
..Default::default()
|
config: TenantConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,11 +282,34 @@ impl TenantConfigRequest {
|
|||||||
eviction_policy: None,
|
eviction_policy: None,
|
||||||
min_resident_size_override: None,
|
min_resident_size_override: None,
|
||||||
evictions_low_residence_duration_metric_threshold: None,
|
evictions_low_residence_duration_metric_threshold: None,
|
||||||
|
gc_feedback: None,
|
||||||
};
|
};
|
||||||
TenantConfigRequest { tenant_id, config }
|
TenantConfigRequest { tenant_id, config }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TenantAttachRequest {
|
||||||
|
pub config: TenantAttachConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Newtype to enforce deny_unknown_fields on TenantConfig for
|
||||||
|
/// its usage inside `TenantAttachRequest`.
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct TenantAttachConfig {
|
||||||
|
#[serde(flatten)]
|
||||||
|
allowing_unknown_fields: TenantConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for TenantAttachConfig {
|
||||||
|
type Target = TenantConfig;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.allowing_unknown_fields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// See [`TenantState::attachment_status`] and the OpenAPI docs for context.
|
/// See [`TenantState::attachment_status`] and the OpenAPI docs for context.
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
@@ -796,5 +872,68 @@ mod tests {
|
|||||||
"expect unknown field `unknown_field` error, got: {}",
|
"expect unknown field `unknown_field` error, got: {}",
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let attach_request = json!({
|
||||||
|
"config": {
|
||||||
|
"unknown_field": "unknown_value".to_string(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let err = serde_json::from_value::<TenantAttachRequest>(attach_request).unwrap_err();
|
||||||
|
assert!(
|
||||||
|
err.to_string().contains("unknown field `unknown_field`"),
|
||||||
|
"expect unknown field `unknown_field` error, got: {}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tenantstatus_activating_serde() {
|
||||||
|
let states = [
|
||||||
|
TenantState::Activating(ActivatingFrom::Loading),
|
||||||
|
TenantState::Activating(ActivatingFrom::Attaching),
|
||||||
|
];
|
||||||
|
let expected = "[{\"slug\":\"Activating\",\"data\":\"Loading\"},{\"slug\":\"Activating\",\"data\":\"Attaching\"}]";
|
||||||
|
|
||||||
|
let actual = serde_json::to_string(&states).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
|
||||||
|
let parsed = serde_json::from_str::<Vec<TenantState>>(&actual).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(states.as_slice(), &parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tenantstatus_activating_strum() {
|
||||||
|
// tests added, because we use these for metrics
|
||||||
|
let examples = [
|
||||||
|
(line!(), TenantState::Loading, "Loading"),
|
||||||
|
(line!(), TenantState::Attaching, "Attaching"),
|
||||||
|
(
|
||||||
|
line!(),
|
||||||
|
TenantState::Activating(ActivatingFrom::Loading),
|
||||||
|
"Activating",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
line!(),
|
||||||
|
TenantState::Activating(ActivatingFrom::Attaching),
|
||||||
|
"Activating",
|
||||||
|
),
|
||||||
|
(line!(), TenantState::Active, "Active"),
|
||||||
|
(line!(), TenantState::Stopping, "Stopping"),
|
||||||
|
(
|
||||||
|
line!(),
|
||||||
|
TenantState::Broken {
|
||||||
|
reason: "Example".into(),
|
||||||
|
backtrace: "Looooong backtrace".into(),
|
||||||
|
},
|
||||||
|
"Broken",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (line, rendered, expected) in examples {
|
||||||
|
let actual: &'static str = rendered.into();
|
||||||
|
assert_eq!(actual, expected, "example on {line}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tokio::sync::{mpsc, Mutex};
|
||||||
|
|
||||||
|
/// While a reference is kept around, the associated [`Barrier::wait`] will wait.
|
||||||
|
///
|
||||||
|
/// Can be cloned, moved and kept around in futures as "guard objects".
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Completion(mpsc::Sender<()>);
|
||||||
|
|
||||||
|
/// Barrier will wait until all clones of [`Completion`] have been dropped.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Barrier(Arc<Mutex<mpsc::Receiver<()>>>);
|
||||||
|
|
||||||
|
impl Barrier {
|
||||||
|
pub async fn wait(self) {
|
||||||
|
self.0.lock().await.recv().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn maybe_wait(barrier: Option<Barrier>) {
|
||||||
|
if let Some(b) = barrier {
|
||||||
|
b.wait().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create new Guard and Barrier pair.
|
||||||
|
pub fn channel() -> (Completion, Barrier) {
|
||||||
|
let (tx, rx) = mpsc::channel::<()>(1);
|
||||||
|
let rx = Mutex::new(rx);
|
||||||
|
let rx = Arc::new(rx);
|
||||||
|
(Completion(tx), Barrier(rx))
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::auth::{Claims, JwtAuth};
|
use crate::auth::{Claims, JwtAuth};
|
||||||
use crate::http::error;
|
use crate::http::error::{api_error_handler, route_error_handler, ApiError};
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use hyper::header::{HeaderName, AUTHORIZATION};
|
use hyper::header::{HeaderName, AUTHORIZATION};
|
||||||
use hyper::http::HeaderValue;
|
use hyper::http::HeaderValue;
|
||||||
@@ -16,8 +16,6 @@ use std::future::Future;
|
|||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use super::error::ApiError;
|
|
||||||
|
|
||||||
static SERVE_METRICS_COUNT: Lazy<IntCounter> = Lazy::new(|| {
|
static SERVE_METRICS_COUNT: Lazy<IntCounter> = Lazy::new(|| {
|
||||||
register_int_counter!(
|
register_int_counter!(
|
||||||
"libmetrics_metric_handler_requests_total",
|
"libmetrics_metric_handler_requests_total",
|
||||||
@@ -35,8 +33,18 @@ struct RequestId(String);
|
|||||||
/// Adds a tracing info_span! instrumentation around the handler events,
|
/// Adds a tracing info_span! instrumentation around the handler events,
|
||||||
/// logs the request start and end events for non-GET requests and non-200 responses.
|
/// logs the request start and end events for non-GET requests and non-200 responses.
|
||||||
///
|
///
|
||||||
|
/// Usage: Replace `my_handler` with `|r| request_span(r, my_handler)`
|
||||||
|
///
|
||||||
/// Use this to distinguish between logs of different HTTP requests: every request handler wrapped
|
/// Use this to distinguish between logs of different HTTP requests: every request handler wrapped
|
||||||
/// in this type will get request info logged in the wrapping span, including the unique request ID.
|
/// with this will get request info logged in the wrapping span, including the unique request ID.
|
||||||
|
///
|
||||||
|
/// This also handles errors, logging them and converting them to an HTTP error response.
|
||||||
|
///
|
||||||
|
/// NB: If the client disconnects, Hyper will drop the Future, without polling it to
|
||||||
|
/// completion. In other words, the handler must be async cancellation safe! request_span
|
||||||
|
/// prints a warning to the log when that happens, so that you have some trace of it in
|
||||||
|
/// the log.
|
||||||
|
///
|
||||||
///
|
///
|
||||||
/// There could be other ways to implement similar functionality:
|
/// There could be other ways to implement similar functionality:
|
||||||
///
|
///
|
||||||
@@ -54,60 +62,56 @@ struct RequestId(String);
|
|||||||
/// tries to achive with its `.instrument` used in the current approach.
|
/// tries to achive with its `.instrument` used in the current approach.
|
||||||
///
|
///
|
||||||
/// If needed, a declarative macro to substitute the |r| ... closure boilerplate could be introduced.
|
/// If needed, a declarative macro to substitute the |r| ... closure boilerplate could be introduced.
|
||||||
pub struct RequestSpan<E, R, H>(pub H)
|
pub async fn request_span<R, H>(request: Request<Body>, handler: H) -> R::Output
|
||||||
where
|
where
|
||||||
E: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
|
R: Future<Output = Result<Response<Body>, ApiError>> + Send + 'static,
|
||||||
R: Future<Output = Result<Response<Body>, E>> + Send + 'static,
|
H: FnOnce(Request<Body>) -> R + Send + Sync + 'static,
|
||||||
H: Fn(Request<Body>) -> R + Send + Sync + 'static;
|
|
||||||
|
|
||||||
impl<E, R, H> RequestSpan<E, R, H>
|
|
||||||
where
|
|
||||||
E: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
|
|
||||||
R: Future<Output = Result<Response<Body>, E>> + Send + 'static,
|
|
||||||
H: Fn(Request<Body>) -> R + Send + Sync + 'static,
|
|
||||||
{
|
{
|
||||||
/// Creates a tracing span around inner request handler and executes the request handler in the contex of that span.
|
let request_id = request.context::<RequestId>().unwrap_or_default().0;
|
||||||
/// Use as `|r| RequestSpan(my_handler).handle(r)` instead of `my_handler` as the request handler to get the span enabled.
|
let method = request.method();
|
||||||
pub async fn handle(self, request: Request<Body>) -> Result<Response<Body>, E> {
|
let path = request.uri().path();
|
||||||
let request_id = request.context::<RequestId>().unwrap_or_default().0;
|
let request_span = info_span!("request", %method, %path, %request_id);
|
||||||
let method = request.method();
|
|
||||||
let path = request.uri().path();
|
|
||||||
let request_span = info_span!("request", %method, %path, %request_id);
|
|
||||||
|
|
||||||
let log_quietly = method == Method::GET;
|
let log_quietly = method == Method::GET;
|
||||||
async move {
|
async move {
|
||||||
let cancellation_guard = RequestCancelled::warn_when_dropped_without_responding();
|
let cancellation_guard = RequestCancelled::warn_when_dropped_without_responding();
|
||||||
if log_quietly {
|
if log_quietly {
|
||||||
debug!("Handling request");
|
debug!("Handling request");
|
||||||
} else {
|
} else {
|
||||||
info!("Handling request");
|
info!("Handling request");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that we reuse `error::handler` here and not returning and error at all,
|
// No special handling for panics here. There's a `tracing_panic_hook` from another
|
||||||
// yet cannot use `!` directly in the method signature due to `routerify::RouterBuilder` limitation.
|
// module to do that globally.
|
||||||
// Usage of the error handler also means that we expect only the `ApiError` errors to be raised in this call.
|
let res = handler(request).await;
|
||||||
//
|
|
||||||
// Panics are not handled separately, there's a `tracing_panic_hook` from another module to do that globally.
|
cancellation_guard.disarm();
|
||||||
let res = (self.0)(request).await;
|
|
||||||
|
// Log the result if needed.
|
||||||
cancellation_guard.disarm();
|
//
|
||||||
|
// We also convert any errors into an Ok response with HTTP error code here.
|
||||||
match res {
|
// `make_router` sets a last-resort error handler that would do the same, but
|
||||||
Ok(response) => {
|
// we prefer to do it here, before we exit the request span, so that the error
|
||||||
let response_status = response.status();
|
// is still logged with the span.
|
||||||
if log_quietly && response_status.is_success() {
|
//
|
||||||
debug!("Request handled, status: {response_status}");
|
// (Because we convert errors to Ok response, we never actually return an error,
|
||||||
} else {
|
// and we could declare the function to return the never type (`!`). However,
|
||||||
info!("Request handled, status: {response_status}");
|
// using `routerify::RouterBuilder` requires a proper error type.)
|
||||||
}
|
match res {
|
||||||
Ok(response)
|
Ok(response) => {
|
||||||
}
|
let response_status = response.status();
|
||||||
Err(e) => Ok(error::handler(e.into()).await),
|
if log_quietly && response_status.is_success() {
|
||||||
}
|
debug!("Request handled, status: {response_status}");
|
||||||
|
} else {
|
||||||
|
info!("Request handled, status: {response_status}");
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
Err(err) => Ok(api_error_handler(err)),
|
||||||
}
|
}
|
||||||
.instrument(request_span)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
.instrument(request_span)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drop guard to WARN in case the request was dropped before completion.
|
/// Drop guard to WARN in case the request was dropped before completion.
|
||||||
@@ -207,10 +211,8 @@ pub fn make_router() -> RouterBuilder<hyper::Body, ApiError> {
|
|||||||
.middleware(Middleware::post_with_info(
|
.middleware(Middleware::post_with_info(
|
||||||
add_request_id_header_to_response,
|
add_request_id_header_to_response,
|
||||||
))
|
))
|
||||||
.get("/metrics", |r| {
|
.get("/metrics", |r| request_span(r, prometheus_metrics_handler))
|
||||||
RequestSpan(prometheus_metrics_handler).handle(r)
|
.err_handler(route_error_handler)
|
||||||
})
|
|
||||||
.err_handler(error::handler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn attach_openapi_ui(
|
pub fn attach_openapi_ui(
|
||||||
@@ -220,12 +222,14 @@ pub fn attach_openapi_ui(
|
|||||||
ui_mount_path: &'static str,
|
ui_mount_path: &'static str,
|
||||||
) -> RouterBuilder<hyper::Body, ApiError> {
|
) -> RouterBuilder<hyper::Body, ApiError> {
|
||||||
router_builder
|
router_builder
|
||||||
.get(spec_mount_path, move |r| {
|
.get(spec_mount_path,
|
||||||
RequestSpan(move |_| async move { Ok(Response::builder().body(Body::from(spec)).unwrap()) })
|
move |r| request_span(r, move |_| async move {
|
||||||
.handle(r)
|
Ok(Response::builder().body(Body::from(spec)).unwrap())
|
||||||
})
|
})
|
||||||
.get(ui_mount_path, move |r| RequestSpan( move |_| async move {
|
)
|
||||||
Ok(Response::builder().body(Body::from(format!(r#"
|
.get(ui_mount_path,
|
||||||
|
move |r| request_span(r, move |_| async move {
|
||||||
|
Ok(Response::builder().body(Body::from(format!(r#"
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@@ -255,7 +259,8 @@ pub fn attach_openapi_ui(
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"#, spec_mount_path))).unwrap())
|
"#, spec_mount_path))).unwrap())
|
||||||
}).handle(r))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_token(header_value: &str) -> Result<&str, ApiError> {
|
fn parse_token(header_value: &str) -> Result<&str, ApiError> {
|
||||||
|
|||||||
@@ -83,13 +83,24 @@ impl HttpErrorBody {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handler(err: routerify::RouteError) -> Response<Body> {
|
pub async fn route_error_handler(err: routerify::RouteError) -> Response<Body> {
|
||||||
let api_error = err
|
match err.downcast::<ApiError>() {
|
||||||
.downcast::<ApiError>()
|
Ok(api_error) => api_error_handler(*api_error),
|
||||||
.expect("handler should always return api error");
|
Err(other_error) => {
|
||||||
|
// We expect all the request handlers to return an ApiError, so this should
|
||||||
|
// not be reached. But just in case.
|
||||||
|
error!("Error processing HTTP request: {other_error:?}");
|
||||||
|
HttpErrorBody::response_from_msg_and_status(
|
||||||
|
other_error.to_string(),
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn api_error_handler(api_error: ApiError) -> Response<Body> {
|
||||||
// Print a stack trace for Internal Server errors
|
// Print a stack trace for Internal Server errors
|
||||||
if let ApiError::InternalServerError(_) = api_error.as_ref() {
|
if let ApiError::InternalServerError(_) = api_error {
|
||||||
error!("Error processing HTTP request: {api_error:?}");
|
error!("Error processing HTTP request: {api_error:?}");
|
||||||
} else {
|
} else {
|
||||||
error!("Error processing HTTP request: {api_error:#}");
|
error!("Error processing HTTP request: {api_error:#}");
|
||||||
|
|||||||
@@ -8,12 +8,26 @@ use super::error::ApiError;
|
|||||||
pub async fn json_request<T: for<'de> Deserialize<'de>>(
|
pub async fn json_request<T: for<'de> Deserialize<'de>>(
|
||||||
request: &mut Request<Body>,
|
request: &mut Request<Body>,
|
||||||
) -> Result<T, ApiError> {
|
) -> Result<T, ApiError> {
|
||||||
let whole_body = hyper::body::aggregate(request.body_mut())
|
json_request_or_empty_body(request)
|
||||||
|
.await?
|
||||||
|
.context("missing request body")
|
||||||
|
.map_err(ApiError::BadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Will be removed as part of https://github.com/neondatabase/neon/issues/4282
|
||||||
|
pub async fn json_request_or_empty_body<T: for<'de> Deserialize<'de>>(
|
||||||
|
request: &mut Request<Body>,
|
||||||
|
) -> Result<Option<T>, ApiError> {
|
||||||
|
let body = hyper::body::aggregate(request.body_mut())
|
||||||
.await
|
.await
|
||||||
.context("Failed to read request body")
|
.context("Failed to read request body")
|
||||||
.map_err(ApiError::BadRequest)?;
|
.map_err(ApiError::BadRequest)?;
|
||||||
serde_json::from_reader(whole_body.reader())
|
if body.remaining() == 0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
serde_json::from_reader(body.reader())
|
||||||
.context("Failed to parse json request")
|
.context("Failed to parse json request")
|
||||||
|
.map(Some)
|
||||||
.map_err(ApiError::BadRequest)
|
.map_err(ApiError::BadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ pub mod tracing_span_assert;
|
|||||||
|
|
||||||
pub mod rate_limit;
|
pub mod rate_limit;
|
||||||
|
|
||||||
|
/// Simple once-barrier and a guard which keeps barrier awaiting.
|
||||||
|
pub mod completion;
|
||||||
|
|
||||||
mod failpoint_macro_helpers {
|
mod failpoint_macro_helpers {
|
||||||
|
|
||||||
/// use with fail::cfg("$name", "return(2000)")
|
/// use with fail::cfg("$name", "return(2000)")
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "pagectl"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
bytes.workspace = true
|
||||||
|
clap = { workspace = true, features = ["string"] }
|
||||||
|
git-version.workspace = true
|
||||||
|
pageserver = { path = ".." }
|
||||||
|
postgres_ffi.workspace = true
|
||||||
|
utils.workspace = true
|
||||||
|
svg_fmt.workspace = true
|
||||||
|
workspace_hack.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
//! Example use:
|
//! Example use:
|
||||||
//! ```
|
//! ```
|
||||||
//! $ ls test_output/test_pgbench\[neon-45-684\]/repo/tenants/$TENANT/timelines/$TIMELINE | \
|
//! $ ls test_output/test_pgbench\[neon-45-684\]/repo/tenants/$TENANT/timelines/$TIMELINE | \
|
||||||
//! $ grep "__" | cargo run --release --bin draw_timeline_dir > out.svg
|
//! $ grep "__" | cargo run --release --bin pagectl draw-timeline-dir > out.svg
|
||||||
//! $ firefox out.svg
|
//! $ firefox out.svg
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
@@ -62,7 +62,7 @@ fn parse_filename(name: &str) -> (Range<Key>, Range<Lsn>) {
|
|||||||
(keys, lsns)
|
(keys, lsns)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
pub fn main() -> Result<()> {
|
||||||
// Parse layer filenames from stdin
|
// Parse layer filenames from stdin
|
||||||
let mut ranges: Vec<(Range<Key>, Range<Lsn>)> = vec![];
|
let mut ranges: Vec<(Range<Key>, Range<Lsn>)> = vec![];
|
||||||
let stdin = io::stdin();
|
let stdin = io::stdin();
|
||||||
+13
-20
@@ -6,7 +6,7 @@ use anyhow::Result;
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::BinaryHeap;
|
use std::collections::BinaryHeap;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::{env, fs, path::Path, path::PathBuf, str, str::FromStr};
|
use std::{fs, path::Path, str};
|
||||||
|
|
||||||
use pageserver::page_cache::PAGE_SZ;
|
use pageserver::page_cache::PAGE_SZ;
|
||||||
use pageserver::repository::{Key, KEY_SIZE};
|
use pageserver::repository::{Key, KEY_SIZE};
|
||||||
@@ -18,12 +18,14 @@ use pageserver::virtual_file::VirtualFile;
|
|||||||
|
|
||||||
use utils::{bin_ser::BeSer, lsn::Lsn};
|
use utils::{bin_ser::BeSer, lsn::Lsn};
|
||||||
|
|
||||||
|
use crate::AnalyzeLayerMapCmd;
|
||||||
|
|
||||||
const MIN_HOLE_LENGTH: i128 = (128 * 1024 * 1024 / PAGE_SZ) as i128;
|
const MIN_HOLE_LENGTH: i128 = (128 * 1024 * 1024 / PAGE_SZ) as i128;
|
||||||
const DEFAULT_MAX_HOLES: usize = 10;
|
const DEFAULT_MAX_HOLES: usize = 10;
|
||||||
|
|
||||||
/// Wrapper for key range to provide reverse ordering by range length for BinaryHeap
|
/// Wrapper for key range to provide reverse ordering by range length for BinaryHeap
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
struct Hole(Range<Key>);
|
pub struct Hole(Range<Key>);
|
||||||
|
|
||||||
impl Ord for Hole {
|
impl Ord for Hole {
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
@@ -39,11 +41,11 @@ impl PartialOrd for Hole {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LayerFile {
|
pub(crate) struct LayerFile {
|
||||||
key_range: Range<Key>,
|
pub key_range: Range<Key>,
|
||||||
lsn_range: Range<Lsn>,
|
pub lsn_range: Range<Lsn>,
|
||||||
is_delta: bool,
|
pub is_delta: bool,
|
||||||
holes: Vec<Hole>,
|
pub holes: Vec<Hole>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayerFile {
|
impl LayerFile {
|
||||||
@@ -67,7 +69,7 @@ impl LayerFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_filename(name: &str) -> Option<LayerFile> {
|
pub(crate) fn parse_filename(name: &str) -> Option<LayerFile> {
|
||||||
let split: Vec<&str> = name.split("__").collect();
|
let split: Vec<&str> = name.split("__").collect();
|
||||||
if split.len() != 2 {
|
if split.len() != 2 {
|
||||||
return None;
|
return None;
|
||||||
@@ -127,18 +129,9 @@ fn get_holes(path: &Path, max_holes: usize) -> Result<Vec<Hole>> {
|
|||||||
Ok(holes)
|
Ok(holes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
pub(crate) fn main(cmd: &AnalyzeLayerMapCmd) -> Result<()> {
|
||||||
let args: Vec<String> = env::args().collect();
|
let storage_path = &cmd.path;
|
||||||
if args.len() < 2 {
|
let max_holes = cmd.max_holes.unwrap_or(DEFAULT_MAX_HOLES);
|
||||||
println!("Usage: layer_map_analyzer PAGESERVER_DATA_DIR [MAX_HOLES]");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let storage_path = PathBuf::from_str(&args[1])?;
|
|
||||||
let max_holes = if args.len() > 2 {
|
|
||||||
args[2].parse::<usize>().unwrap()
|
|
||||||
} else {
|
|
||||||
DEFAULT_MAX_HOLES
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize virtual_file (file desriptor cache) and page cache which are needed to access layer persistent B-Tree.
|
// Initialize virtual_file (file desriptor cache) and page cache which are needed to access layer persistent B-Tree.
|
||||||
pageserver::virtual_file::init(10);
|
pageserver::virtual_file::init(10);
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
use pageserver::tenant::block_io::BlockCursor;
|
||||||
|
use pageserver::tenant::disk_btree::DiskBtreeReader;
|
||||||
|
use pageserver::tenant::storage_layer::delta_layer::{BlobRef, Summary};
|
||||||
|
use pageserver::{page_cache, virtual_file};
|
||||||
|
use pageserver::{
|
||||||
|
repository::{Key, KEY_SIZE},
|
||||||
|
tenant::{
|
||||||
|
block_io::FileBlockReader, disk_btree::VisitDirection,
|
||||||
|
storage_layer::delta_layer::DELTA_KEY_SIZE,
|
||||||
|
},
|
||||||
|
virtual_file::VirtualFile,
|
||||||
|
};
|
||||||
|
use std::fs;
|
||||||
|
use utils::bin_ser::BeSer;
|
||||||
|
|
||||||
|
use crate::layer_map_analyzer::parse_filename;
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub(crate) enum LayerCmd {
|
||||||
|
/// List all tenants and timelines under the pageserver path
|
||||||
|
///
|
||||||
|
/// Example: `cargo run --bin pagectl layer list .neon/`
|
||||||
|
List { path: PathBuf },
|
||||||
|
/// List all layers of a given tenant and timeline
|
||||||
|
///
|
||||||
|
/// Example: `cargo run --bin pagectl layer list .neon/`
|
||||||
|
ListLayer {
|
||||||
|
path: PathBuf,
|
||||||
|
tenant: String,
|
||||||
|
timeline: String,
|
||||||
|
},
|
||||||
|
/// Dump all information of a layer file
|
||||||
|
DumpLayer {
|
||||||
|
path: PathBuf,
|
||||||
|
tenant: String,
|
||||||
|
timeline: String,
|
||||||
|
/// The id from list-layer command
|
||||||
|
id: usize,
|
||||||
|
},
|
||||||
|
/// Output layer statistics
|
||||||
|
GetStats {
|
||||||
|
path: PathBuf,
|
||||||
|
tenant: String,
|
||||||
|
timeline: String,
|
||||||
|
/// The id from list-layer command
|
||||||
|
id: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return (key, value.len) for all keys, sorted by key.
|
||||||
|
fn read_delta_file(path: impl AsRef<Path>) -> Result<Vec<(Key, usize)>> {
|
||||||
|
use pageserver::tenant::blob_io::BlobCursor;
|
||||||
|
use pageserver::tenant::block_io::BlockReader;
|
||||||
|
|
||||||
|
let path = path.as_ref();
|
||||||
|
virtual_file::init(10);
|
||||||
|
page_cache::init(100);
|
||||||
|
let file = FileBlockReader::new(VirtualFile::open(path)?);
|
||||||
|
let summary_blk = file.read_blk(0)?;
|
||||||
|
let actual_summary = Summary::des_prefix(summary_blk.as_ref())?;
|
||||||
|
let tree_reader = DiskBtreeReader::<_, DELTA_KEY_SIZE>::new(
|
||||||
|
actual_summary.index_start_blk,
|
||||||
|
actual_summary.index_root_blk,
|
||||||
|
&file,
|
||||||
|
);
|
||||||
|
// TODO(chi): dedup w/ `delta_layer.rs` by exposing the API.
|
||||||
|
let mut all = vec![];
|
||||||
|
tree_reader.visit(
|
||||||
|
&[0u8; DELTA_KEY_SIZE],
|
||||||
|
VisitDirection::Forwards,
|
||||||
|
|key, value_offset| {
|
||||||
|
let curr = Key::from_slice(&key[..KEY_SIZE]);
|
||||||
|
all.push((curr, BlobRef(value_offset)));
|
||||||
|
true
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let mut cursor = BlockCursor::new(&file);
|
||||||
|
|
||||||
|
let mut result = vec![];
|
||||||
|
for (k, v) in all {
|
||||||
|
let value = cursor.read_blob(v.pos())?;
|
||||||
|
result.push((k, value.len()));
|
||||||
|
}
|
||||||
|
// TODO(chi): special handling for last key?
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We divide the entire i128 keyspace into pre-assigned fixed segments,
|
||||||
|
// 8MB each. Group keys by segment, and report segment size for each.
|
||||||
|
//
|
||||||
|
// 8MB is chosen as the segment size because we're unlikely to make
|
||||||
|
// s3 partial downloads smaller than 8MB (due to cost). So summarizing
|
||||||
|
// layer metadata in 8MB segments could be enough to generate good test
|
||||||
|
// data for write amplification tests.
|
||||||
|
//
|
||||||
|
// Note that the segments are fixed, and don't depend on what keyspace
|
||||||
|
// is actually in use.
|
||||||
|
fn read_delta_segments(path: impl AsRef<Path>) -> Result<Vec<(i128, usize)>> {
|
||||||
|
fn key_to_segment(key: &Key) -> i128 {
|
||||||
|
// A page is 8KB. So 1024 pages are 8MB.
|
||||||
|
key.to_i128() >> 10
|
||||||
|
}
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
let delta_metadata = read_delta_file(path)?;
|
||||||
|
let group_iter = delta_metadata.iter().group_by(|(k, _)| key_to_segment(k));
|
||||||
|
let group_sizes = group_iter.into_iter().map(|(segment, lengths_group)| {
|
||||||
|
let sum: usize = lengths_group.map(|(_k, len)| len).sum();
|
||||||
|
(segment, sum)
|
||||||
|
});
|
||||||
|
Ok(group_sizes.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn summarize_delta_file(path: impl AsRef<Path>) -> Result<()> {
|
||||||
|
// TODO write in some compressed binary format
|
||||||
|
for (segment, size) in read_delta_segments(path)? {
|
||||||
|
println!("segment:{} size:{}", segment, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn main(cmd: &LayerCmd) -> Result<()> {
|
||||||
|
match cmd {
|
||||||
|
LayerCmd::List { path } => {
|
||||||
|
for tenant in fs::read_dir(path.join("tenants"))? {
|
||||||
|
let tenant = tenant?;
|
||||||
|
if !tenant.file_type()?.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println!("tenant {}", tenant.file_name().to_string_lossy());
|
||||||
|
for timeline in fs::read_dir(tenant.path().join("timelines"))? {
|
||||||
|
let timeline = timeline?;
|
||||||
|
if !timeline.file_type()?.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println!("- timeline {}", timeline.file_name().to_string_lossy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LayerCmd::ListLayer {
|
||||||
|
path,
|
||||||
|
tenant,
|
||||||
|
timeline,
|
||||||
|
} => {
|
||||||
|
let timeline_path = path
|
||||||
|
.join("tenants")
|
||||||
|
.join(tenant)
|
||||||
|
.join("timelines")
|
||||||
|
.join(timeline);
|
||||||
|
let mut idx = 0;
|
||||||
|
for layer in fs::read_dir(timeline_path)? {
|
||||||
|
let layer = layer?;
|
||||||
|
if let Some(layer_file) = parse_filename(&layer.file_name().into_string().unwrap())
|
||||||
|
{
|
||||||
|
println!(
|
||||||
|
"[{:3}] key:{}-{}\n lsn:{}-{}\n delta:{}",
|
||||||
|
idx,
|
||||||
|
layer_file.key_range.start,
|
||||||
|
layer_file.key_range.end,
|
||||||
|
layer_file.lsn_range.start,
|
||||||
|
layer_file.lsn_range.end,
|
||||||
|
layer_file.is_delta,
|
||||||
|
);
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LayerCmd::DumpLayer {
|
||||||
|
path,
|
||||||
|
tenant,
|
||||||
|
timeline,
|
||||||
|
id,
|
||||||
|
} => {
|
||||||
|
let timeline_path = path
|
||||||
|
.join("tenants")
|
||||||
|
.join(tenant)
|
||||||
|
.join("timelines")
|
||||||
|
.join(timeline);
|
||||||
|
let mut idx = 0;
|
||||||
|
for layer in fs::read_dir(timeline_path)? {
|
||||||
|
let layer = layer?;
|
||||||
|
if let Some(layer_file) = parse_filename(&layer.file_name().into_string().unwrap())
|
||||||
|
{
|
||||||
|
if *id == idx {
|
||||||
|
// TODO(chi): dedup code
|
||||||
|
println!(
|
||||||
|
"[{:3}] key:{}-{}\n lsn:{}-{}\n delta:{}",
|
||||||
|
idx,
|
||||||
|
layer_file.key_range.start,
|
||||||
|
layer_file.key_range.end,
|
||||||
|
layer_file.lsn_range.start,
|
||||||
|
layer_file.lsn_range.end,
|
||||||
|
layer_file.is_delta,
|
||||||
|
);
|
||||||
|
|
||||||
|
if layer_file.is_delta {
|
||||||
|
for (k, len) in read_delta_file(layer.path())? {
|
||||||
|
println!("key:{} value_len:{}", k, len);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("not supported yet :(");
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LayerCmd::GetStats {
|
||||||
|
path,
|
||||||
|
tenant,
|
||||||
|
timeline,
|
||||||
|
id,
|
||||||
|
} => {
|
||||||
|
let timeline_path = path
|
||||||
|
.join("tenants")
|
||||||
|
.join(tenant)
|
||||||
|
.join("timelines")
|
||||||
|
.join(timeline);
|
||||||
|
let mut idx = 0;
|
||||||
|
for layer in fs::read_dir(timeline_path)? {
|
||||||
|
let layer = layer?;
|
||||||
|
if let Some(layer_file) = parse_filename(&layer.file_name().into_string().unwrap())
|
||||||
|
{
|
||||||
|
if *id == idx {
|
||||||
|
if layer_file.is_delta {
|
||||||
|
summarize_delta_file(layer.path())?;
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("not supported yet :(");
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
//! A helper tool to manage pageserver binary files.
|
||||||
|
//! Accepts a file as an argument, attempts to parse it with all ways possible
|
||||||
|
//! and prints its interpreted context.
|
||||||
|
//!
|
||||||
|
//! Separate, `metadata` subcommand allows to print and update pageserver's metadata file.
|
||||||
|
|
||||||
|
mod draw_timeline_dir;
|
||||||
|
mod layer_map_analyzer;
|
||||||
|
mod layers;
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use layers::LayerCmd;
|
||||||
|
use pageserver::{
|
||||||
|
context::{DownloadBehavior, RequestContext},
|
||||||
|
page_cache,
|
||||||
|
task_mgr::TaskKind,
|
||||||
|
tenant::{dump_layerfile_from_path, metadata::TimelineMetadata},
|
||||||
|
virtual_file,
|
||||||
|
};
|
||||||
|
use postgres_ffi::ControlFileData;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use utils::{lsn::Lsn, project_git_version};
|
||||||
|
|
||||||
|
project_git_version!(GIT_VERSION);
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(
|
||||||
|
version = GIT_VERSION,
|
||||||
|
about = "Neon Pageserver binutils",
|
||||||
|
long_about = "Reads pageserver (and related) binary files management utility"
|
||||||
|
)]
|
||||||
|
#[command(propagate_version = true)]
|
||||||
|
struct CliOpts {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
Metadata(MetadataCmd),
|
||||||
|
PrintLayerFile(PrintLayerFileCmd),
|
||||||
|
DrawTimeline {},
|
||||||
|
AnalyzeLayerMap(AnalyzeLayerMapCmd),
|
||||||
|
#[command(subcommand)]
|
||||||
|
Layer(LayerCmd),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read and update pageserver metadata file
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct MetadataCmd {
|
||||||
|
/// Input metadata file path
|
||||||
|
metadata_path: PathBuf,
|
||||||
|
/// Replace disk consistent Lsn
|
||||||
|
disk_consistent_lsn: Option<Lsn>,
|
||||||
|
/// Replace previous record Lsn
|
||||||
|
prev_record_lsn: Option<Lsn>,
|
||||||
|
/// Replace latest gc cuttoff
|
||||||
|
latest_gc_cuttoff: Option<Lsn>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct PrintLayerFileCmd {
|
||||||
|
/// Pageserver data path
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct AnalyzeLayerMapCmd {
|
||||||
|
/// Pageserver data path
|
||||||
|
path: PathBuf,
|
||||||
|
/// Max holes
|
||||||
|
max_holes: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let cli = CliOpts::parse();
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Commands::Layer(cmd) => {
|
||||||
|
layers::main(&cmd)?;
|
||||||
|
}
|
||||||
|
Commands::Metadata(cmd) => {
|
||||||
|
handle_metadata(&cmd)?;
|
||||||
|
}
|
||||||
|
Commands::DrawTimeline {} => {
|
||||||
|
draw_timeline_dir::main()?;
|
||||||
|
}
|
||||||
|
Commands::AnalyzeLayerMap(cmd) => {
|
||||||
|
layer_map_analyzer::main(&cmd)?;
|
||||||
|
}
|
||||||
|
Commands::PrintLayerFile(cmd) => {
|
||||||
|
if let Err(e) = read_pg_control_file(&cmd.path) {
|
||||||
|
println!(
|
||||||
|
"Failed to read input file as a pg control one: {e:#}\n\
|
||||||
|
Attempting to read it as layer file"
|
||||||
|
);
|
||||||
|
print_layerfile(&cmd.path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_pg_control_file(control_file_path: &Path) -> anyhow::Result<()> {
|
||||||
|
let control_file = ControlFileData::decode(&std::fs::read(control_file_path)?)?;
|
||||||
|
println!("{control_file:?}");
|
||||||
|
let control_file_initdb = Lsn(control_file.checkPoint);
|
||||||
|
println!(
|
||||||
|
"pg_initdb_lsn: {}, aligned: {}",
|
||||||
|
control_file_initdb,
|
||||||
|
control_file_initdb.align()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_layerfile(path: &Path) -> anyhow::Result<()> {
|
||||||
|
// Basic initialization of things that don't change after startup
|
||||||
|
virtual_file::init(10);
|
||||||
|
page_cache::init(100);
|
||||||
|
let ctx = RequestContext::new(TaskKind::DebugTool, DownloadBehavior::Error);
|
||||||
|
dump_layerfile_from_path(path, true, &ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_metadata(
|
||||||
|
MetadataCmd {
|
||||||
|
metadata_path: path,
|
||||||
|
disk_consistent_lsn,
|
||||||
|
prev_record_lsn,
|
||||||
|
latest_gc_cuttoff,
|
||||||
|
}: &MetadataCmd,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
let metadata_bytes = std::fs::read(path)?;
|
||||||
|
let mut meta = TimelineMetadata::from_bytes(&metadata_bytes)?;
|
||||||
|
println!("Current metadata:\n{meta:?}");
|
||||||
|
let mut update_meta = false;
|
||||||
|
if let Some(disk_consistent_lsn) = disk_consistent_lsn {
|
||||||
|
meta = TimelineMetadata::new(
|
||||||
|
*disk_consistent_lsn,
|
||||||
|
meta.prev_record_lsn(),
|
||||||
|
meta.ancestor_timeline(),
|
||||||
|
meta.ancestor_lsn(),
|
||||||
|
meta.latest_gc_cutoff_lsn(),
|
||||||
|
meta.initdb_lsn(),
|
||||||
|
meta.pg_version(),
|
||||||
|
);
|
||||||
|
update_meta = true;
|
||||||
|
}
|
||||||
|
if let Some(prev_record_lsn) = prev_record_lsn {
|
||||||
|
meta = TimelineMetadata::new(
|
||||||
|
meta.disk_consistent_lsn(),
|
||||||
|
Some(*prev_record_lsn),
|
||||||
|
meta.ancestor_timeline(),
|
||||||
|
meta.ancestor_lsn(),
|
||||||
|
meta.latest_gc_cutoff_lsn(),
|
||||||
|
meta.initdb_lsn(),
|
||||||
|
meta.pg_version(),
|
||||||
|
);
|
||||||
|
update_meta = true;
|
||||||
|
}
|
||||||
|
if let Some(latest_gc_cuttoff) = latest_gc_cuttoff {
|
||||||
|
meta = TimelineMetadata::new(
|
||||||
|
meta.disk_consistent_lsn(),
|
||||||
|
meta.prev_record_lsn(),
|
||||||
|
meta.ancestor_timeline(),
|
||||||
|
meta.ancestor_lsn(),
|
||||||
|
*latest_gc_cuttoff,
|
||||||
|
meta.initdb_lsn(),
|
||||||
|
meta.pg_version(),
|
||||||
|
);
|
||||||
|
update_meta = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if update_meta {
|
||||||
|
let metadata_bytes = meta.to_bytes()?;
|
||||||
|
std::fs::write(path, metadata_bytes)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ use clap::{Arg, ArgAction, Command};
|
|||||||
use fail::FailScenario;
|
use fail::FailScenario;
|
||||||
use metrics::launch_timestamp::{set_launch_timestamp_metric, LaunchTimestamp};
|
use metrics::launch_timestamp::{set_launch_timestamp_metric, LaunchTimestamp};
|
||||||
use pageserver::disk_usage_eviction_task::{self, launch_disk_usage_global_eviction_task};
|
use pageserver::disk_usage_eviction_task::{self, launch_disk_usage_global_eviction_task};
|
||||||
|
use pageserver::task_mgr::WALRECEIVER_RUNTIME;
|
||||||
use remote_storage::GenericRemoteStorage;
|
use remote_storage::GenericRemoteStorage;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
@@ -18,9 +19,7 @@ use pageserver::{
|
|||||||
context::{DownloadBehavior, RequestContext},
|
context::{DownloadBehavior, RequestContext},
|
||||||
http, page_cache, page_service, task_mgr,
|
http, page_cache, page_service, task_mgr,
|
||||||
task_mgr::TaskKind,
|
task_mgr::TaskKind,
|
||||||
task_mgr::{
|
task_mgr::{BACKGROUND_RUNTIME, COMPUTE_REQUEST_RUNTIME, MGMT_REQUEST_RUNTIME},
|
||||||
BACKGROUND_RUNTIME, COMPUTE_REQUEST_RUNTIME, MGMT_REQUEST_RUNTIME, WALRECEIVER_RUNTIME,
|
|
||||||
},
|
|
||||||
tenant::mgr,
|
tenant::mgr,
|
||||||
virtual_file,
|
virtual_file,
|
||||||
};
|
};
|
||||||
@@ -276,7 +275,18 @@ fn start_pageserver(
|
|||||||
let pageserver_listener = tcp_listener::bind(pg_addr)?;
|
let pageserver_listener = tcp_listener::bind(pg_addr)?;
|
||||||
|
|
||||||
// Launch broker client
|
// Launch broker client
|
||||||
WALRECEIVER_RUNTIME.block_on(pageserver::broker_client::init_broker_client(conf))?;
|
// The storage_broker::connect call needs to happen inside a tokio runtime thread.
|
||||||
|
let broker_client = WALRECEIVER_RUNTIME
|
||||||
|
.block_on(async {
|
||||||
|
// Note: we do not attempt connecting here (but validate endpoints sanity).
|
||||||
|
storage_broker::connect(conf.broker_endpoint.clone(), conf.broker_keepalive_interval)
|
||||||
|
})
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"create broker client for uri={:?} keepalive_interval={:?}",
|
||||||
|
&conf.broker_endpoint, conf.broker_keepalive_interval,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
// Initialize authentication for incoming connections
|
// Initialize authentication for incoming connections
|
||||||
let http_auth;
|
let http_auth;
|
||||||
@@ -325,8 +335,33 @@ fn start_pageserver(
|
|||||||
// Set up remote storage client
|
// Set up remote storage client
|
||||||
let remote_storage = create_remote_storage_client(conf)?;
|
let remote_storage = create_remote_storage_client(conf)?;
|
||||||
|
|
||||||
|
// All tenant load operations carry this while they are ongoing; it will be dropped once those
|
||||||
|
// operations finish either successfully or in some other manner. However, the initial load
|
||||||
|
// will be then done, and we can start the global background tasks.
|
||||||
|
let (init_done_tx, init_done_rx) = utils::completion::channel();
|
||||||
|
|
||||||
// Scan the local 'tenants/' directory and start loading the tenants
|
// Scan the local 'tenants/' directory and start loading the tenants
|
||||||
BACKGROUND_RUNTIME.block_on(mgr::init_tenant_mgr(conf, remote_storage.clone()))?;
|
let init_started_at = std::time::Instant::now();
|
||||||
|
BACKGROUND_RUNTIME.block_on(mgr::init_tenant_mgr(
|
||||||
|
conf,
|
||||||
|
broker_client.clone(),
|
||||||
|
remote_storage.clone(),
|
||||||
|
(init_done_tx, init_done_rx.clone()),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
BACKGROUND_RUNTIME.spawn({
|
||||||
|
let init_done_rx = init_done_rx.clone();
|
||||||
|
async move {
|
||||||
|
init_done_rx.wait().await;
|
||||||
|
|
||||||
|
let elapsed = init_started_at.elapsed();
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
elapsed_millis = elapsed.as_millis(),
|
||||||
|
"Initial load completed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// shared state between the disk-usage backed eviction background task and the http endpoint
|
// shared state between the disk-usage backed eviction background task and the http endpoint
|
||||||
// that allows triggering disk-usage based eviction manually. note that the http endpoint
|
// that allows triggering disk-usage based eviction manually. note that the http endpoint
|
||||||
@@ -339,6 +374,7 @@ fn start_pageserver(
|
|||||||
conf,
|
conf,
|
||||||
remote_storage.clone(),
|
remote_storage.clone(),
|
||||||
disk_usage_eviction_state.clone(),
|
disk_usage_eviction_state.clone(),
|
||||||
|
init_done_rx.clone(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,6 +387,7 @@ fn start_pageserver(
|
|||||||
conf,
|
conf,
|
||||||
launch_ts,
|
launch_ts,
|
||||||
http_auth,
|
http_auth,
|
||||||
|
broker_client.clone(),
|
||||||
remote_storage,
|
remote_storage,
|
||||||
disk_usage_eviction_state,
|
disk_usage_eviction_state,
|
||||||
)?
|
)?
|
||||||
@@ -375,6 +412,7 @@ fn start_pageserver(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if let Some(metric_collection_endpoint) = &conf.metric_collection_endpoint {
|
if let Some(metric_collection_endpoint) = &conf.metric_collection_endpoint {
|
||||||
|
let init_done_rx = init_done_rx;
|
||||||
let metrics_ctx = RequestContext::todo_child(
|
let metrics_ctx = RequestContext::todo_child(
|
||||||
TaskKind::MetricsCollection,
|
TaskKind::MetricsCollection,
|
||||||
// This task itself shouldn't download anything.
|
// This task itself shouldn't download anything.
|
||||||
@@ -390,6 +428,13 @@ fn start_pageserver(
|
|||||||
"consumption metrics collection",
|
"consumption metrics collection",
|
||||||
true,
|
true,
|
||||||
async move {
|
async move {
|
||||||
|
// first wait for initial load to complete before first iteration.
|
||||||
|
//
|
||||||
|
// this is because we only process active tenants and timelines, and the
|
||||||
|
// Timeline::get_current_logical_size will spawn the logical size calculation,
|
||||||
|
// which will not be rate-limited.
|
||||||
|
init_done_rx.wait().await;
|
||||||
|
|
||||||
pageserver::consumption_metrics::collect_metrics(
|
pageserver::consumption_metrics::collect_metrics(
|
||||||
metric_collection_endpoint,
|
metric_collection_endpoint,
|
||||||
conf.metric_collection_interval,
|
conf.metric_collection_interval,
|
||||||
@@ -427,6 +472,7 @@ fn start_pageserver(
|
|||||||
async move {
|
async move {
|
||||||
page_service::libpq_listener_main(
|
page_service::libpq_listener_main(
|
||||||
conf,
|
conf,
|
||||||
|
broker_client,
|
||||||
pg_auth,
|
pg_auth,
|
||||||
pageserver_listener,
|
pageserver_listener,
|
||||||
conf.pg_auth_type,
|
conf.pg_auth_type,
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
//! A helper tool to manage pageserver binary files.
|
|
||||||
//! Accepts a file as an argument, attempts to parse it with all ways possible
|
|
||||||
//! and prints its interpreted context.
|
|
||||||
//!
|
|
||||||
//! Separate, `metadata` subcommand allows to print and update pageserver's metadata file.
|
|
||||||
use std::{
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
str::FromStr,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use clap::{value_parser, Arg, Command};
|
|
||||||
|
|
||||||
use pageserver::{
|
|
||||||
context::{DownloadBehavior, RequestContext},
|
|
||||||
page_cache,
|
|
||||||
task_mgr::TaskKind,
|
|
||||||
tenant::{dump_layerfile_from_path, metadata::TimelineMetadata},
|
|
||||||
virtual_file,
|
|
||||||
};
|
|
||||||
use postgres_ffi::ControlFileData;
|
|
||||||
use utils::{lsn::Lsn, project_git_version};
|
|
||||||
|
|
||||||
project_git_version!(GIT_VERSION);
|
|
||||||
|
|
||||||
const METADATA_SUBCOMMAND: &str = "metadata";
|
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
|
||||||
let arg_matches = cli().get_matches();
|
|
||||||
|
|
||||||
match arg_matches.subcommand() {
|
|
||||||
Some((subcommand_name, subcommand_matches)) => {
|
|
||||||
let path = subcommand_matches
|
|
||||||
.get_one::<PathBuf>("metadata_path")
|
|
||||||
.context("'metadata_path' argument is missing")?
|
|
||||||
.to_path_buf();
|
|
||||||
anyhow::ensure!(
|
|
||||||
subcommand_name == METADATA_SUBCOMMAND,
|
|
||||||
"Unknown subcommand {subcommand_name}"
|
|
||||||
);
|
|
||||||
handle_metadata(&path, subcommand_matches)?;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let path = arg_matches
|
|
||||||
.get_one::<PathBuf>("path")
|
|
||||||
.context("'path' argument is missing")?
|
|
||||||
.to_path_buf();
|
|
||||||
println!(
|
|
||||||
"No subcommand specified, attempting to guess the format for file {}",
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
if let Err(e) = read_pg_control_file(&path) {
|
|
||||||
println!(
|
|
||||||
"Failed to read input file as a pg control one: {e:#}\n\
|
|
||||||
Attempting to read it as layer file"
|
|
||||||
);
|
|
||||||
print_layerfile(&path)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_pg_control_file(control_file_path: &Path) -> anyhow::Result<()> {
|
|
||||||
let control_file = ControlFileData::decode(&std::fs::read(control_file_path)?)?;
|
|
||||||
println!("{control_file:?}");
|
|
||||||
let control_file_initdb = Lsn(control_file.checkPoint);
|
|
||||||
println!(
|
|
||||||
"pg_initdb_lsn: {}, aligned: {}",
|
|
||||||
control_file_initdb,
|
|
||||||
control_file_initdb.align()
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_layerfile(path: &Path) -> anyhow::Result<()> {
|
|
||||||
// Basic initialization of things that don't change after startup
|
|
||||||
virtual_file::init(10);
|
|
||||||
page_cache::init(100);
|
|
||||||
let ctx = RequestContext::new(TaskKind::DebugTool, DownloadBehavior::Error);
|
|
||||||
dump_layerfile_from_path(path, true, &ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_metadata(path: &Path, arg_matches: &clap::ArgMatches) -> Result<(), anyhow::Error> {
|
|
||||||
let metadata_bytes = std::fs::read(path)?;
|
|
||||||
let mut meta = TimelineMetadata::from_bytes(&metadata_bytes)?;
|
|
||||||
println!("Current metadata:\n{meta:?}");
|
|
||||||
let mut update_meta = false;
|
|
||||||
if let Some(disk_consistent_lsn) = arg_matches.get_one::<String>("disk_consistent_lsn") {
|
|
||||||
meta = TimelineMetadata::new(
|
|
||||||
Lsn::from_str(disk_consistent_lsn)?,
|
|
||||||
meta.prev_record_lsn(),
|
|
||||||
meta.ancestor_timeline(),
|
|
||||||
meta.ancestor_lsn(),
|
|
||||||
meta.latest_gc_cutoff_lsn(),
|
|
||||||
meta.initdb_lsn(),
|
|
||||||
meta.pg_version(),
|
|
||||||
);
|
|
||||||
update_meta = true;
|
|
||||||
}
|
|
||||||
if let Some(prev_record_lsn) = arg_matches.get_one::<String>("prev_record_lsn") {
|
|
||||||
meta = TimelineMetadata::new(
|
|
||||||
meta.disk_consistent_lsn(),
|
|
||||||
Some(Lsn::from_str(prev_record_lsn)?),
|
|
||||||
meta.ancestor_timeline(),
|
|
||||||
meta.ancestor_lsn(),
|
|
||||||
meta.latest_gc_cutoff_lsn(),
|
|
||||||
meta.initdb_lsn(),
|
|
||||||
meta.pg_version(),
|
|
||||||
);
|
|
||||||
update_meta = true;
|
|
||||||
}
|
|
||||||
if let Some(latest_gc_cuttoff) = arg_matches.get_one::<String>("latest_gc_cuttoff") {
|
|
||||||
meta = TimelineMetadata::new(
|
|
||||||
meta.disk_consistent_lsn(),
|
|
||||||
meta.prev_record_lsn(),
|
|
||||||
meta.ancestor_timeline(),
|
|
||||||
meta.ancestor_lsn(),
|
|
||||||
Lsn::from_str(latest_gc_cuttoff)?,
|
|
||||||
meta.initdb_lsn(),
|
|
||||||
meta.pg_version(),
|
|
||||||
);
|
|
||||||
update_meta = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if update_meta {
|
|
||||||
let metadata_bytes = meta.to_bytes()?;
|
|
||||||
std::fs::write(path, metadata_bytes)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cli() -> Command {
|
|
||||||
Command::new("Neon Pageserver binutils")
|
|
||||||
.about("Reads pageserver (and related) binary files management utility")
|
|
||||||
.version(GIT_VERSION)
|
|
||||||
.arg(
|
|
||||||
Arg::new("path")
|
|
||||||
.help("Input file path")
|
|
||||||
.value_parser(value_parser!(PathBuf))
|
|
||||||
.required(false),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new(METADATA_SUBCOMMAND)
|
|
||||||
.about("Read and update pageserver metadata file")
|
|
||||||
.arg(
|
|
||||||
Arg::new("metadata_path")
|
|
||||||
.help("Input metadata file path")
|
|
||||||
.value_parser(value_parser!(PathBuf))
|
|
||||||
.required(false),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("disk_consistent_lsn")
|
|
||||||
.long("disk_consistent_lsn")
|
|
||||||
.help("Replace disk consistent Lsn"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("prev_record_lsn")
|
|
||||||
.long("prev_record_lsn")
|
|
||||||
.help("Replace previous record Lsn"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("latest_gc_cuttoff")
|
|
||||||
.long("latest_gc_cuttoff")
|
|
||||||
.help("Replace latest gc cuttoff"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn verify_cli() {
|
|
||||||
cli().debug_assert();
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
//! The broker client instance of the pageserver, created during pageserver startup.
|
|
||||||
//! Used by each timelines' [`walreceiver`].
|
|
||||||
|
|
||||||
use crate::config::PageServerConf;
|
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use storage_broker::BrokerClientChannel;
|
|
||||||
use tracing::*;
|
|
||||||
|
|
||||||
static BROKER_CLIENT: OnceCell<BrokerClientChannel> = OnceCell::new();
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Initialize the broker client. This must be called once at page server startup.
|
|
||||||
///
|
|
||||||
pub async fn init_broker_client(conf: &'static PageServerConf) -> anyhow::Result<()> {
|
|
||||||
let broker_endpoint = conf.broker_endpoint.clone();
|
|
||||||
|
|
||||||
// Note: we do not attempt connecting here (but validate endpoints sanity).
|
|
||||||
let broker_client =
|
|
||||||
storage_broker::connect(broker_endpoint.clone(), conf.broker_keepalive_interval).context(
|
|
||||||
format!(
|
|
||||||
"Failed to create broker client to {}",
|
|
||||||
&conf.broker_endpoint
|
|
||||||
),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if BROKER_CLIENT.set(broker_client).is_err() {
|
|
||||||
panic!("broker already initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Initialized broker client with endpoints: {}",
|
|
||||||
broker_endpoint
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Get a handle to the broker client
|
|
||||||
///
|
|
||||||
pub fn get_broker_client() -> &'static BrokerClientChannel {
|
|
||||||
BROKER_CLIENT.get().expect("broker client not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_broker_client_initialized() -> bool {
|
|
||||||
BROKER_CLIENT.get().is_some()
|
|
||||||
}
|
|
||||||
@@ -108,7 +108,7 @@ pub mod defaults {
|
|||||||
|
|
||||||
#min_resident_size_override = .. # in bytes
|
#min_resident_size_override = .. # in bytes
|
||||||
#evictions_low_residence_duration_metric_threshold = '{DEFAULT_EVICTIONS_LOW_RESIDENCE_DURATION_METRIC_THRESHOLD}'
|
#evictions_low_residence_duration_metric_threshold = '{DEFAULT_EVICTIONS_LOW_RESIDENCE_DURATION_METRIC_THRESHOLD}'
|
||||||
|
#gc_feedback = false
|
||||||
# [remote_storage]
|
# [remote_storage]
|
||||||
|
|
||||||
"###
|
"###
|
||||||
@@ -828,6 +828,14 @@ impl PageServerConf {
|
|||||||
)?);
|
)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(gc_feedback) = item.get("gc_feedback") {
|
||||||
|
t_conf.gc_feedback = Some(
|
||||||
|
gc_feedback
|
||||||
|
.as_bool()
|
||||||
|
.with_context(|| "configure option gc_feedback is not a bool".to_string())?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(t_conf)
|
Ok(t_conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,7 @@
|
|||||||
use crate::task_mgr::TaskKind;
|
use crate::task_mgr::TaskKind;
|
||||||
|
|
||||||
// The main structure of this module, see module-level comment.
|
// The main structure of this module, see module-level comment.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct RequestContext {
|
pub struct RequestContext {
|
||||||
task_kind: TaskKind,
|
task_kind: TaskKind,
|
||||||
download_behavior: DownloadBehavior,
|
download_behavior: DownloadBehavior,
|
||||||
@@ -95,7 +96,7 @@ pub struct RequestContext {
|
|||||||
|
|
||||||
/// Desired behavior if the operation requires an on-demand download
|
/// Desired behavior if the operation requires an on-demand download
|
||||||
/// to proceed.
|
/// to proceed.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
pub enum DownloadBehavior {
|
pub enum DownloadBehavior {
|
||||||
/// Download the layer file. It can take a while.
|
/// Download the layer file. It can take a while.
|
||||||
Download,
|
Download,
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::{debug, error, info, instrument, warn, Instrument};
|
use tracing::{debug, error, info, instrument, warn, Instrument};
|
||||||
|
use utils::completion;
|
||||||
use utils::serde_percent::Percent;
|
use utils::serde_percent::Percent;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -82,6 +83,7 @@ pub fn launch_disk_usage_global_eviction_task(
|
|||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
storage: GenericRemoteStorage,
|
storage: GenericRemoteStorage,
|
||||||
state: Arc<State>,
|
state: Arc<State>,
|
||||||
|
init_done: completion::Barrier,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let Some(task_config) = &conf.disk_usage_based_eviction else {
|
let Some(task_config) = &conf.disk_usage_based_eviction else {
|
||||||
info!("disk usage based eviction task not configured");
|
info!("disk usage based eviction task not configured");
|
||||||
@@ -98,6 +100,9 @@ pub fn launch_disk_usage_global_eviction_task(
|
|||||||
"disk usage based eviction",
|
"disk usage based eviction",
|
||||||
false,
|
false,
|
||||||
async move {
|
async move {
|
||||||
|
// wait until initial load is complete, because we cannot evict from loading tenants.
|
||||||
|
init_done.wait().await;
|
||||||
|
|
||||||
disk_usage_eviction_task(
|
disk_usage_eviction_task(
|
||||||
&state,
|
&state,
|
||||||
task_config,
|
task_config,
|
||||||
|
|||||||
@@ -363,11 +363,29 @@ paths:
|
|||||||
* MUST NOT ASSUME that the request has been lost, based on the observation
|
* MUST NOT ASSUME that the request has been lost, based on the observation
|
||||||
that a subsequent tenant status request returns 404. The request may
|
that a subsequent tenant status request returns 404. The request may
|
||||||
still be in flight. It must be retried.
|
still be in flight. It must be retried.
|
||||||
|
|
||||||
|
The client SHOULD supply a `TenantConfig` for the tenant in the request body.
|
||||||
|
Settings specified in the config override the pageserver's defaults.
|
||||||
|
It is guaranteed that the config settings are applied before the pageserver
|
||||||
|
starts operating on the tenant. E.g., if the config specifies a specific
|
||||||
|
PITR interval for a tenant, then that setting will be in effect before the
|
||||||
|
pageserver starts the garbage collection loop. This enables a client to
|
||||||
|
guarantee a specific PITR setting across detach/attach cycles.
|
||||||
|
The pageserver will reject the request if it cannot parse the config, or
|
||||||
|
if there are any unknown fields in it.
|
||||||
|
|
||||||
|
If the client does not supply a config, the pageserver will use its defaults.
|
||||||
|
This behavior is deprecated: https://github.com/neondatabase/neon/issues/4282
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/TenantAttachRequest"
|
||||||
responses:
|
responses:
|
||||||
"202":
|
"202":
|
||||||
description: Tenant attaching scheduled
|
description: Tenant attaching scheduled
|
||||||
"400":
|
"400":
|
||||||
description: Error when no tenant id found in path parameters
|
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@@ -660,6 +678,8 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
|
required:
|
||||||
|
- new_timeline_id
|
||||||
properties:
|
properties:
|
||||||
new_timeline_id:
|
new_timeline_id:
|
||||||
type: string
|
type: string
|
||||||
@@ -918,10 +938,19 @@ components:
|
|||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/TenantConfig'
|
- $ref: '#/components/schemas/TenantConfig'
|
||||||
- type: object
|
- type: object
|
||||||
|
required:
|
||||||
|
- new_tenant_id
|
||||||
properties:
|
properties:
|
||||||
new_tenant_id:
|
new_tenant_id:
|
||||||
type: string
|
type: string
|
||||||
format: hex
|
format: hex
|
||||||
|
TenantAttachRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- config
|
||||||
|
properties:
|
||||||
|
config:
|
||||||
|
$ref: '#/components/schemas/TenantConfig'
|
||||||
TenantConfigRequest:
|
TenantConfigRequest:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/TenantConfig'
|
- $ref: '#/components/schemas/TenantConfig'
|
||||||
|
|||||||
+304
-125
@@ -1,3 +1,6 @@
|
|||||||
|
//!
|
||||||
|
//! Management HTTP API
|
||||||
|
//!
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -5,12 +8,14 @@ use anyhow::{anyhow, Context, Result};
|
|||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use hyper::{Body, Request, Response, Uri};
|
use hyper::{Body, Request, Response, Uri};
|
||||||
use metrics::launch_timestamp::LaunchTimestamp;
|
use metrics::launch_timestamp::LaunchTimestamp;
|
||||||
use pageserver_api::models::DownloadRemoteLayersTaskSpawnRequest;
|
use pageserver_api::models::{DownloadRemoteLayersTaskSpawnRequest, TenantAttachRequest};
|
||||||
use remote_storage::GenericRemoteStorage;
|
use remote_storage::GenericRemoteStorage;
|
||||||
|
use storage_broker::BrokerClientChannel;
|
||||||
use tenant_size_model::{SizeResult, StorageModel};
|
use tenant_size_model::{SizeResult, StorageModel};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
use utils::http::endpoint::RequestSpan;
|
use utils::http::endpoint::request_span;
|
||||||
|
use utils::http::json::json_request_or_empty_body;
|
||||||
use utils::http::request::{get_request_param, must_get_query_param, parse_query_param};
|
use utils::http::request::{get_request_param, must_get_query_param, parse_query_param};
|
||||||
|
|
||||||
use super::models::{
|
use super::models::{
|
||||||
@@ -23,7 +28,9 @@ use crate::metrics::{StorageTimeOperation, STORAGE_TIME_GLOBAL};
|
|||||||
use crate::pgdatadir_mapping::LsnForTimestamp;
|
use crate::pgdatadir_mapping::LsnForTimestamp;
|
||||||
use crate::task_mgr::TaskKind;
|
use crate::task_mgr::TaskKind;
|
||||||
use crate::tenant::config::TenantConfOpt;
|
use crate::tenant::config::TenantConfOpt;
|
||||||
use crate::tenant::mgr::{TenantMapInsertError, TenantStateError};
|
use crate::tenant::mgr::{
|
||||||
|
GetTenantError, SetNewTenantConfigError, TenantMapInsertError, TenantStateError,
|
||||||
|
};
|
||||||
use crate::tenant::size::ModelInputs;
|
use crate::tenant::size::ModelInputs;
|
||||||
use crate::tenant::storage_layer::LayerAccessStatsReset;
|
use crate::tenant::storage_layer::LayerAccessStatsReset;
|
||||||
use crate::tenant::{LogicalSizeCalculationCause, PageReconstructError, Timeline};
|
use crate::tenant::{LogicalSizeCalculationCause, PageReconstructError, Timeline};
|
||||||
@@ -42,7 +49,6 @@ use utils::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Imports only used for testing APIs
|
// Imports only used for testing APIs
|
||||||
#[cfg(feature = "testing")]
|
|
||||||
use super::models::ConfigureFailpointsRequest;
|
use super::models::ConfigureFailpointsRequest;
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
@@ -50,6 +56,7 @@ struct State {
|
|||||||
auth: Option<Arc<JwtAuth>>,
|
auth: Option<Arc<JwtAuth>>,
|
||||||
allowlist_routes: Vec<Uri>,
|
allowlist_routes: Vec<Uri>,
|
||||||
remote_storage: Option<GenericRemoteStorage>,
|
remote_storage: Option<GenericRemoteStorage>,
|
||||||
|
broker_client: storage_broker::BrokerClientChannel,
|
||||||
disk_usage_eviction_state: Arc<disk_usage_eviction_task::State>,
|
disk_usage_eviction_state: Arc<disk_usage_eviction_task::State>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +65,7 @@ impl State {
|
|||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
auth: Option<Arc<JwtAuth>>,
|
auth: Option<Arc<JwtAuth>>,
|
||||||
remote_storage: Option<GenericRemoteStorage>,
|
remote_storage: Option<GenericRemoteStorage>,
|
||||||
|
broker_client: storage_broker::BrokerClientChannel,
|
||||||
disk_usage_eviction_state: Arc<disk_usage_eviction_task::State>,
|
disk_usage_eviction_state: Arc<disk_usage_eviction_task::State>,
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
let allowlist_routes = ["/v1/status", "/v1/doc", "/swagger.yml"]
|
let allowlist_routes = ["/v1/status", "/v1/doc", "/swagger.yml"]
|
||||||
@@ -69,6 +77,7 @@ impl State {
|
|||||||
auth,
|
auth,
|
||||||
allowlist_routes,
|
allowlist_routes,
|
||||||
remote_storage,
|
remote_storage,
|
||||||
|
broker_client,
|
||||||
disk_usage_eviction_state,
|
disk_usage_eviction_state,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -139,6 +148,36 @@ impl From<TenantStateError> for ApiError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<GetTenantError> for ApiError {
|
||||||
|
fn from(tse: GetTenantError) -> ApiError {
|
||||||
|
match tse {
|
||||||
|
GetTenantError::NotFound(tid) => ApiError::NotFound(anyhow!("tenant {}", tid)),
|
||||||
|
e @ GetTenantError::NotActive(_) => {
|
||||||
|
// Why is this not `ApiError::NotFound`?
|
||||||
|
// Because we must be careful to never return 404 for a tenant if it does
|
||||||
|
// in fact exist locally. If we did, the caller could draw the conclusion
|
||||||
|
// that it can attach the tenant to another PS and we'd be in split-brain.
|
||||||
|
//
|
||||||
|
// (We can produce this variant only in `mgr::get_tenant(..., active=true)` calls).
|
||||||
|
ApiError::InternalServerError(anyhow::Error::new(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SetNewTenantConfigError> for ApiError {
|
||||||
|
fn from(e: SetNewTenantConfigError) -> ApiError {
|
||||||
|
match e {
|
||||||
|
SetNewTenantConfigError::GetTenant(tid) => {
|
||||||
|
ApiError::NotFound(anyhow!("tenant {}", tid))
|
||||||
|
}
|
||||||
|
e @ SetNewTenantConfigError::Persist(_) => {
|
||||||
|
ApiError::InternalServerError(anyhow::Error::new(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<crate::tenant::DeleteTimelineError> for ApiError {
|
impl From<crate::tenant::DeleteTimelineError> for ApiError {
|
||||||
fn from(value: crate::tenant::DeleteTimelineError) -> Self {
|
fn from(value: crate::tenant::DeleteTimelineError) -> Self {
|
||||||
use crate::tenant::DeleteTimelineError::*;
|
use crate::tenant::DeleteTimelineError::*;
|
||||||
@@ -158,7 +197,7 @@ impl From<crate::tenant::mgr::DeleteTimelineError> for ApiError {
|
|||||||
match value {
|
match value {
|
||||||
// Report Precondition failed so client can distinguish between
|
// Report Precondition failed so client can distinguish between
|
||||||
// "tenant is missing" case from "timeline is missing"
|
// "tenant is missing" case from "timeline is missing"
|
||||||
Tenant(TenantStateError::NotFound(..)) => {
|
Tenant(GetTenantError::NotFound(..)) => {
|
||||||
ApiError::PreconditionFailed("Requested tenant is missing")
|
ApiError::PreconditionFailed("Requested tenant is missing")
|
||||||
}
|
}
|
||||||
Tenant(t) => ApiError::from(t),
|
Tenant(t) => ApiError::from(t),
|
||||||
@@ -253,23 +292,29 @@ fn build_timeline_info_common(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// healthcheck handler
|
// healthcheck handler
|
||||||
async fn status_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn status_handler(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
check_permission(&request, None)?;
|
check_permission(&request, None)?;
|
||||||
let config = get_config(&request);
|
let config = get_config(&request);
|
||||||
json_response(StatusCode::OK, StatusResponse { id: config.id })
|
json_response(StatusCode::OK, StatusResponse { id: config.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn timeline_create_handler(mut request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn timeline_create_handler(
|
||||||
|
mut request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
let request_data: TimelineCreateRequest = json_request(&mut request).await?;
|
let request_data: TimelineCreateRequest = json_request(&mut request).await?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
|
|
||||||
let new_timeline_id = request_data
|
let new_timeline_id = request_data.new_timeline_id;
|
||||||
.new_timeline_id
|
|
||||||
.unwrap_or_else(TimelineId::generate);
|
|
||||||
|
|
||||||
let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Error);
|
let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Error);
|
||||||
|
|
||||||
|
let state = get_state(&request);
|
||||||
|
|
||||||
async {
|
async {
|
||||||
let tenant = mgr::get_tenant(tenant_id, true).await?;
|
let tenant = mgr::get_tenant(tenant_id, true).await?;
|
||||||
match tenant.create_timeline(
|
match tenant.create_timeline(
|
||||||
@@ -277,6 +322,7 @@ async fn timeline_create_handler(mut request: Request<Body>) -> Result<Response<
|
|||||||
request_data.ancestor_timeline_id.map(TimelineId::from),
|
request_data.ancestor_timeline_id.map(TimelineId::from),
|
||||||
request_data.ancestor_start_lsn,
|
request_data.ancestor_start_lsn,
|
||||||
request_data.pg_version.unwrap_or(crate::DEFAULT_PG_VERSION),
|
request_data.pg_version.unwrap_or(crate::DEFAULT_PG_VERSION),
|
||||||
|
state.broker_client.clone(),
|
||||||
&ctx,
|
&ctx,
|
||||||
)
|
)
|
||||||
.await {
|
.await {
|
||||||
@@ -290,11 +336,14 @@ async fn timeline_create_handler(mut request: Request<Body>) -> Result<Response<
|
|||||||
Err(err) => Err(ApiError::InternalServerError(err)),
|
Err(err) => Err(ApiError::InternalServerError(err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.instrument(info_span!("timeline_create", tenant = %tenant_id, new_timeline = ?request_data.new_timeline_id, timeline_id = %new_timeline_id, lsn=?request_data.ancestor_start_lsn, pg_version=?request_data.pg_version))
|
.instrument(info_span!("timeline_create", tenant = %tenant_id, timeline_id = %new_timeline_id, lsn=?request_data.ancestor_start_lsn, pg_version=?request_data.pg_version))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn timeline_list_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn timeline_list_handler(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
let include_non_incremental_logical_size: Option<bool> =
|
let include_non_incremental_logical_size: Option<bool> =
|
||||||
parse_query_param(&request, "include-non-incremental-logical-size")?;
|
parse_query_param(&request, "include-non-incremental-logical-size")?;
|
||||||
@@ -328,7 +377,10 @@ async fn timeline_list_handler(request: Request<Body>) -> Result<Response<Body>,
|
|||||||
json_response(StatusCode::OK, response_data)
|
json_response(StatusCode::OK, response_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn timeline_detail_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn timeline_detail_handler(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
||||||
let include_non_incremental_logical_size: Option<bool> =
|
let include_non_incremental_logical_size: Option<bool> =
|
||||||
@@ -362,7 +414,10 @@ async fn timeline_detail_handler(request: Request<Body>) -> Result<Response<Body
|
|||||||
json_response(StatusCode::OK, timeline_info)
|
json_response(StatusCode::OK, timeline_info)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_lsn_by_timestamp_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn get_lsn_by_timestamp_handler(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
|
|
||||||
@@ -386,11 +441,19 @@ async fn get_lsn_by_timestamp_handler(request: Request<Body>) -> Result<Response
|
|||||||
json_response(StatusCode::OK, result)
|
json_response(StatusCode::OK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO makes sense to provide tenant config right away the same way as it handled in tenant_create
|
async fn tenant_attach_handler(
|
||||||
async fn tenant_attach_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
mut request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
|
|
||||||
|
let maybe_body: Option<TenantAttachRequest> = json_request_or_empty_body(&mut request).await?;
|
||||||
|
let tenant_conf = match maybe_body {
|
||||||
|
Some(request) => TenantConfOpt::try_from(&*request.config).map_err(ApiError::BadRequest)?,
|
||||||
|
None => TenantConfOpt::default(),
|
||||||
|
};
|
||||||
|
|
||||||
let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Warn);
|
let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Warn);
|
||||||
|
|
||||||
info!("Handling tenant attach {tenant_id}");
|
info!("Handling tenant attach {tenant_id}");
|
||||||
@@ -401,9 +464,8 @@ async fn tenant_attach_handler(request: Request<Body>) -> Result<Response<Body>,
|
|||||||
mgr::attach_tenant(
|
mgr::attach_tenant(
|
||||||
state.conf,
|
state.conf,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
// XXX: Attach should provide the config, especially during tenant migration.
|
tenant_conf,
|
||||||
// See https://github.com/neondatabase/neon/issues/1555
|
state.broker_client.clone(),
|
||||||
TenantConfOpt::default(),
|
|
||||||
remote_storage.clone(),
|
remote_storage.clone(),
|
||||||
&ctx,
|
&ctx,
|
||||||
)
|
)
|
||||||
@@ -418,7 +480,10 @@ async fn tenant_attach_handler(request: Request<Body>) -> Result<Response<Body>,
|
|||||||
json_response(StatusCode::ACCEPTED, ())
|
json_response(StatusCode::ACCEPTED, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn timeline_delete_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn timeline_delete_handler(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
@@ -432,7 +497,10 @@ async fn timeline_delete_handler(request: Request<Body>) -> Result<Response<Body
|
|||||||
json_response(StatusCode::OK, ())
|
json_response(StatusCode::OK, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn tenant_detach_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn tenant_detach_handler(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
let detach_ignored: Option<bool> = parse_query_param(&request, "detach_ignored")?;
|
let detach_ignored: Option<bool> = parse_query_param(&request, "detach_ignored")?;
|
||||||
@@ -446,21 +514,33 @@ async fn tenant_detach_handler(request: Request<Body>) -> Result<Response<Body>,
|
|||||||
json_response(StatusCode::OK, ())
|
json_response(StatusCode::OK, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn tenant_load_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn tenant_load_handler(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
|
|
||||||
let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Warn);
|
let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Warn);
|
||||||
|
|
||||||
let state = get_state(&request);
|
let state = get_state(&request);
|
||||||
mgr::load_tenant(state.conf, tenant_id, state.remote_storage.clone(), &ctx)
|
mgr::load_tenant(
|
||||||
.instrument(info_span!("load", tenant = %tenant_id))
|
state.conf,
|
||||||
.await?;
|
tenant_id,
|
||||||
|
state.broker_client.clone(),
|
||||||
|
state.remote_storage.clone(),
|
||||||
|
&ctx,
|
||||||
|
)
|
||||||
|
.instrument(info_span!("load", tenant = %tenant_id))
|
||||||
|
.await?;
|
||||||
|
|
||||||
json_response(StatusCode::ACCEPTED, ())
|
json_response(StatusCode::ACCEPTED, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn tenant_ignore_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn tenant_ignore_handler(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
|
|
||||||
@@ -473,7 +553,10 @@ async fn tenant_ignore_handler(request: Request<Body>) -> Result<Response<Body>,
|
|||||||
json_response(StatusCode::OK, ())
|
json_response(StatusCode::OK, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn tenant_list_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn tenant_list_handler(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
check_permission(&request, None)?;
|
check_permission(&request, None)?;
|
||||||
|
|
||||||
let response_data = mgr::list_tenants()
|
let response_data = mgr::list_tenants()
|
||||||
@@ -493,7 +576,10 @@ async fn tenant_list_handler(request: Request<Body>) -> Result<Response<Body>, A
|
|||||||
json_response(StatusCode::OK, response_data)
|
json_response(StatusCode::OK, response_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn tenant_status(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn tenant_status(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
|
|
||||||
@@ -507,7 +593,7 @@ async fn tenant_status(request: Request<Body>) -> Result<Response<Body>, ApiErro
|
|||||||
}
|
}
|
||||||
|
|
||||||
let state = tenant.current_state();
|
let state = tenant.current_state();
|
||||||
Ok(TenantInfo {
|
Result::<_, ApiError>::Ok(TenantInfo {
|
||||||
id: tenant_id,
|
id: tenant_id,
|
||||||
state: state.clone(),
|
state: state.clone(),
|
||||||
current_physical_size: Some(current_physical_size),
|
current_physical_size: Some(current_physical_size),
|
||||||
@@ -515,8 +601,7 @@ async fn tenant_status(request: Request<Body>) -> Result<Response<Body>, ApiErro
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
.instrument(info_span!("tenant_status_handler", tenant = %tenant_id))
|
.instrument(info_span!("tenant_status_handler", tenant = %tenant_id))
|
||||||
.await
|
.await?;
|
||||||
.map_err(ApiError::InternalServerError)?;
|
|
||||||
|
|
||||||
json_response(StatusCode::OK, tenant_info)
|
json_response(StatusCode::OK, tenant_info)
|
||||||
}
|
}
|
||||||
@@ -534,7 +619,10 @@ async fn tenant_status(request: Request<Body>) -> Result<Response<Body>, ApiErro
|
|||||||
/// Note: we don't update the cached size and prometheus metric here.
|
/// Note: we don't update the cached size and prometheus metric here.
|
||||||
/// The retention period might be different, and it's nice to have a method to just calculate it
|
/// The retention period might be different, and it's nice to have a method to just calculate it
|
||||||
/// without modifying anything anyway.
|
/// without modifying anything anyway.
|
||||||
async fn tenant_size_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn tenant_size_handler(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
let inputs_only: Option<bool> = parse_query_param(&request, "inputs_only")?;
|
let inputs_only: Option<bool> = parse_query_param(&request, "inputs_only")?;
|
||||||
@@ -599,7 +687,10 @@ async fn tenant_size_handler(request: Request<Body>) -> Result<Response<Body>, A
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn layer_map_info_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn layer_map_info_handler(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
||||||
let reset: LayerAccessStatsReset =
|
let reset: LayerAccessStatsReset =
|
||||||
@@ -613,7 +704,10 @@ async fn layer_map_info_handler(request: Request<Body>) -> Result<Response<Body>
|
|||||||
json_response(StatusCode::OK, layer_map_info)
|
json_response(StatusCode::OK, layer_map_info)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn layer_download_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn layer_download_handler(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
||||||
@@ -636,7 +730,10 @@ async fn layer_download_handler(request: Request<Body>) -> Result<Response<Body>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn evict_timeline_layer_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn evict_timeline_layer_handler(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
||||||
@@ -714,7 +811,12 @@ pub fn html_response(status: StatusCode, data: String) -> Result<Response<Body>,
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn tenant_create_handler(mut request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn tenant_create_handler(
|
||||||
|
mut request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
|
let request_data: TenantCreateRequest = json_request(&mut request).await?;
|
||||||
|
let target_tenant_id = request_data.new_tenant_id;
|
||||||
check_permission(&request, None)?;
|
check_permission(&request, None)?;
|
||||||
|
|
||||||
let _timer = STORAGE_TIME_GLOBAL
|
let _timer = STORAGE_TIME_GLOBAL
|
||||||
@@ -722,17 +824,10 @@ async fn tenant_create_handler(mut request: Request<Body>) -> Result<Response<Bo
|
|||||||
.expect("bug")
|
.expect("bug")
|
||||||
.start_timer();
|
.start_timer();
|
||||||
|
|
||||||
let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Warn);
|
|
||||||
|
|
||||||
let request_data: TenantCreateRequest = json_request(&mut request).await?;
|
|
||||||
|
|
||||||
let tenant_conf =
|
let tenant_conf =
|
||||||
TenantConfOpt::try_from(&request_data.config).map_err(ApiError::BadRequest)?;
|
TenantConfOpt::try_from(&request_data.config).map_err(ApiError::BadRequest)?;
|
||||||
|
|
||||||
let target_tenant_id = request_data
|
let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Warn);
|
||||||
.new_tenant_id
|
|
||||||
.map(TenantId::from)
|
|
||||||
.unwrap_or_else(TenantId::generate);
|
|
||||||
|
|
||||||
let state = get_state(&request);
|
let state = get_state(&request);
|
||||||
|
|
||||||
@@ -740,6 +835,7 @@ async fn tenant_create_handler(mut request: Request<Body>) -> Result<Response<Bo
|
|||||||
state.conf,
|
state.conf,
|
||||||
tenant_conf,
|
tenant_conf,
|
||||||
target_tenant_id,
|
target_tenant_id,
|
||||||
|
state.broker_client.clone(),
|
||||||
state.remote_storage.clone(),
|
state.remote_storage.clone(),
|
||||||
&ctx,
|
&ctx,
|
||||||
)
|
)
|
||||||
@@ -765,7 +861,10 @@ async fn tenant_create_handler(mut request: Request<Body>) -> Result<Response<Bo
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_tenant_config_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn get_tenant_config_handler(
|
||||||
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
|
|
||||||
@@ -791,6 +890,7 @@ async fn get_tenant_config_handler(request: Request<Body>) -> Result<Response<Bo
|
|||||||
|
|
||||||
async fn update_tenant_config_handler(
|
async fn update_tenant_config_handler(
|
||||||
mut request: Request<Body>,
|
mut request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
) -> Result<Response<Body>, ApiError> {
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let request_data: TenantConfigRequest = json_request(&mut request).await?;
|
let request_data: TenantConfigRequest = json_request(&mut request).await?;
|
||||||
let tenant_id = request_data.tenant_id;
|
let tenant_id = request_data.tenant_id;
|
||||||
@@ -808,21 +908,25 @@ async fn update_tenant_config_handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Testing helper to transition a tenant to [`crate::tenant::TenantState::Broken`].
|
/// Testing helper to transition a tenant to [`crate::tenant::TenantState::Broken`].
|
||||||
#[cfg(feature = "testing")]
|
async fn handle_tenant_break(
|
||||||
async fn handle_tenant_break(r: Request<Body>) -> Result<Response<Body>, ApiError> {
|
r: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&r, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&r, "tenant_id")?;
|
||||||
|
|
||||||
let tenant = crate::tenant::mgr::get_tenant(tenant_id, true)
|
let tenant = crate::tenant::mgr::get_tenant(tenant_id, true)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ApiError::Conflict(String::from("no active tenant found")))?;
|
.map_err(|_| ApiError::Conflict(String::from("no active tenant found")))?;
|
||||||
|
|
||||||
tenant.set_broken("broken from test".to_owned());
|
tenant.set_broken("broken from test".to_owned()).await;
|
||||||
|
|
||||||
json_response(StatusCode::OK, ())
|
json_response(StatusCode::OK, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "testing")]
|
async fn failpoints_handler(
|
||||||
async fn failpoints_handler(mut request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
mut request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
if !fail::has_failpoints() {
|
if !fail::has_failpoints() {
|
||||||
return Err(ApiError::BadRequest(anyhow!(
|
return Err(ApiError::BadRequest(anyhow!(
|
||||||
"Cannot manage failpoints because pageserver was compiled without failpoints support"
|
"Cannot manage failpoints because pageserver was compiled without failpoints support"
|
||||||
@@ -855,7 +959,10 @@ async fn failpoints_handler(mut request: Request<Body>) -> Result<Response<Body>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run GC immediately on given timeline.
|
// Run GC immediately on given timeline.
|
||||||
async fn timeline_gc_handler(mut request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn timeline_gc_handler(
|
||||||
|
mut request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
@@ -874,8 +981,10 @@ async fn timeline_gc_handler(mut request: Request<Body>) -> Result<Response<Body
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run compaction immediately on given timeline.
|
// Run compaction immediately on given timeline.
|
||||||
#[cfg(feature = "testing")]
|
async fn timeline_compact_handler(
|
||||||
async fn timeline_compact_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
@@ -896,8 +1005,10 @@ async fn timeline_compact_handler(request: Request<Body>) -> Result<Response<Bod
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run checkpoint immediately on given timeline.
|
// Run checkpoint immediately on given timeline.
|
||||||
#[cfg(feature = "testing")]
|
async fn timeline_checkpoint_handler(
|
||||||
async fn timeline_checkpoint_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
@@ -921,6 +1032,7 @@ async fn timeline_checkpoint_handler(request: Request<Body>) -> Result<Response<
|
|||||||
|
|
||||||
async fn timeline_download_remote_layers_handler_post(
|
async fn timeline_download_remote_layers_handler_post(
|
||||||
mut request: Request<Body>,
|
mut request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
) -> Result<Response<Body>, ApiError> {
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
|
||||||
@@ -936,6 +1048,7 @@ async fn timeline_download_remote_layers_handler_post(
|
|||||||
|
|
||||||
async fn timeline_download_remote_layers_handler_get(
|
async fn timeline_download_remote_layers_handler_get(
|
||||||
request: Request<Body>,
|
request: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
) -> Result<Response<Body>, ApiError> {
|
) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
||||||
check_permission(&request, Some(tenant_id))?;
|
check_permission(&request, Some(tenant_id))?;
|
||||||
@@ -959,7 +1072,10 @@ async fn active_timeline_of_active_tenant(
|
|||||||
.map_err(ApiError::NotFound)
|
.map_err(ApiError::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn always_panic_handler(req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn always_panic_handler(
|
||||||
|
req: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
// Deliberately cause a panic to exercise the panic hook registered via std::panic::set_hook().
|
// Deliberately cause a panic to exercise the panic hook registered via std::panic::set_hook().
|
||||||
// For pageserver, the relevant panic hook is `tracing_panic_hook` , and the `sentry` crate's wrapper around it.
|
// For pageserver, the relevant panic hook is `tracing_panic_hook` , and the `sentry` crate's wrapper around it.
|
||||||
// Use catch_unwind to ensure that tokio nor hyper are distracted by our panic.
|
// Use catch_unwind to ensure that tokio nor hyper are distracted by our panic.
|
||||||
@@ -970,7 +1086,10 @@ async fn always_panic_handler(req: Request<Body>) -> Result<Response<Body>, ApiE
|
|||||||
json_response(StatusCode::NO_CONTENT, ())
|
json_response(StatusCode::NO_CONTENT, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn disk_usage_eviction_run(mut r: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn disk_usage_eviction_run(
|
||||||
|
mut r: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
check_permission(&r, None)?;
|
check_permission(&r, None)?;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -1060,8 +1179,10 @@ async fn handler_404(_: Request<Body>) -> Result<Response<Body>, ApiError> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "testing")]
|
async fn post_tracing_event_handler(
|
||||||
async fn post_tracing_event_handler(mut r: Request<Body>) -> Result<Response<Body>, ApiError> {
|
mut r: Request<Body>,
|
||||||
|
_cancel: CancellationToken,
|
||||||
|
) -> Result<Response<Body>, ApiError> {
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
enum Level {
|
enum Level {
|
||||||
@@ -1091,10 +1212,90 @@ async fn post_tracing_event_handler(mut r: Request<Body>) -> Result<Response<Bod
|
|||||||
json_response(StatusCode::OK, ())
|
json_response(StatusCode::OK, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Common functionality of all the HTTP API handlers.
|
||||||
|
///
|
||||||
|
/// - Adds a tracing span to each request (by `request_span`)
|
||||||
|
/// - Logs the request depending on the request method (by `request_span`)
|
||||||
|
/// - Logs the response if it was not successful (by `request_span`
|
||||||
|
/// - Shields the handler function from async cancellations. Hyper can drop the handler
|
||||||
|
/// Future if the connection to the client is lost, but most of the pageserver code is
|
||||||
|
/// not async cancellation safe. This converts the dropped future into a graceful cancellation
|
||||||
|
/// request with a CancellationToken.
|
||||||
|
async fn api_handler<R, H>(request: Request<Body>, handler: H) -> Result<Response<Body>, ApiError>
|
||||||
|
where
|
||||||
|
R: std::future::Future<Output = Result<Response<Body>, ApiError>> + Send + 'static,
|
||||||
|
H: FnOnce(Request<Body>, CancellationToken) -> R + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
// Spawn a new task to handle the request, to protect the handler from unexpected
|
||||||
|
// async cancellations. Most pageserver functions are not async cancellation safe.
|
||||||
|
// We arm a drop-guard, so that if Hyper drops the Future, we signal the task
|
||||||
|
// with the cancellation token.
|
||||||
|
let token = CancellationToken::new();
|
||||||
|
let cancel_guard = token.clone().drop_guard();
|
||||||
|
let result = request_span(request, move |r| async {
|
||||||
|
let handle = tokio::spawn(
|
||||||
|
async {
|
||||||
|
let token_cloned = token.clone();
|
||||||
|
let result = handler(r, token).await;
|
||||||
|
if token_cloned.is_cancelled() {
|
||||||
|
info!("Cancelled request finished");
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
.in_current_span(),
|
||||||
|
);
|
||||||
|
|
||||||
|
match handle.await {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(e) => {
|
||||||
|
// The handler task panicked. We have a global panic handler that logs the
|
||||||
|
// panic with its backtrace, so no need to log that here. Only log a brief
|
||||||
|
// message to make it clear that we returned the error to the client.
|
||||||
|
error!("HTTP request handler task panicked: {e:#}");
|
||||||
|
|
||||||
|
// Don't return an Error here, because then fallback error handler that was
|
||||||
|
// installed in make_router() will print the error. Instead, construct the
|
||||||
|
// HTTP error response and return that.
|
||||||
|
Ok(
|
||||||
|
ApiError::InternalServerError(anyhow!("HTTP request handler task panicked"))
|
||||||
|
.into_response(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cancel_guard.disarm();
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like api_handler, but returns an error response if the server is built without
|
||||||
|
/// the 'testing' feature.
|
||||||
|
async fn testing_api_handler<R, H>(
|
||||||
|
desc: &str,
|
||||||
|
request: Request<Body>,
|
||||||
|
handler: H,
|
||||||
|
) -> Result<Response<Body>, ApiError>
|
||||||
|
where
|
||||||
|
R: std::future::Future<Output = Result<Response<Body>, ApiError>> + Send + 'static,
|
||||||
|
H: FnOnce(Request<Body>, CancellationToken) -> R + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
if cfg!(feature = "testing") {
|
||||||
|
api_handler(request, handler).await
|
||||||
|
} else {
|
||||||
|
std::future::ready(Err(ApiError::BadRequest(anyhow!(
|
||||||
|
"Cannot {desc} because pageserver was compiled without testing APIs",
|
||||||
|
))))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn make_router(
|
pub fn make_router(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
launch_ts: &'static LaunchTimestamp,
|
launch_ts: &'static LaunchTimestamp,
|
||||||
auth: Option<Arc<JwtAuth>>,
|
auth: Option<Arc<JwtAuth>>,
|
||||||
|
broker_client: BrokerClientChannel,
|
||||||
remote_storage: Option<GenericRemoteStorage>,
|
remote_storage: Option<GenericRemoteStorage>,
|
||||||
disk_usage_eviction_state: Arc<disk_usage_eviction_task::State>,
|
disk_usage_eviction_state: Arc<disk_usage_eviction_task::State>,
|
||||||
) -> anyhow::Result<RouterBuilder<hyper::Body, ApiError>> {
|
) -> anyhow::Result<RouterBuilder<hyper::Body, ApiError>> {
|
||||||
@@ -1119,121 +1320,99 @@ pub fn make_router(
|
|||||||
.expect("construct launch timestamp header middleware"),
|
.expect("construct launch timestamp header middleware"),
|
||||||
);
|
);
|
||||||
|
|
||||||
macro_rules! testing_api {
|
|
||||||
($handler_desc:literal, $handler:path $(,)?) => {{
|
|
||||||
#[cfg(not(feature = "testing"))]
|
|
||||||
async fn cfg_disabled(_req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
|
||||||
Err(ApiError::BadRequest(anyhow!(concat!(
|
|
||||||
"Cannot ",
|
|
||||||
$handler_desc,
|
|
||||||
" because pageserver was compiled without testing APIs",
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "testing")]
|
|
||||||
let handler = $handler;
|
|
||||||
#[cfg(not(feature = "testing"))]
|
|
||||||
let handler = cfg_disabled;
|
|
||||||
|
|
||||||
move |r| RequestSpan(handler).handle(r)
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(router
|
Ok(router
|
||||||
.data(Arc::new(
|
.data(Arc::new(
|
||||||
State::new(conf, auth, remote_storage, disk_usage_eviction_state)
|
State::new(
|
||||||
.context("Failed to initialize router state")?,
|
conf,
|
||||||
|
auth,
|
||||||
|
remote_storage,
|
||||||
|
broker_client,
|
||||||
|
disk_usage_eviction_state,
|
||||||
|
)
|
||||||
|
.context("Failed to initialize router state")?,
|
||||||
))
|
))
|
||||||
.get("/v1/status", |r| RequestSpan(status_handler).handle(r))
|
.get("/v1/status", |r| api_handler(r, status_handler))
|
||||||
.put(
|
.put("/v1/failpoints", |r| {
|
||||||
"/v1/failpoints",
|
testing_api_handler("manage failpoints", r, failpoints_handler)
|
||||||
testing_api!("manage failpoints", failpoints_handler),
|
|
||||||
)
|
|
||||||
.get("/v1/tenant", |r| RequestSpan(tenant_list_handler).handle(r))
|
|
||||||
.post("/v1/tenant", |r| {
|
|
||||||
RequestSpan(tenant_create_handler).handle(r)
|
|
||||||
})
|
|
||||||
.get("/v1/tenant/:tenant_id", |r| {
|
|
||||||
RequestSpan(tenant_status).handle(r)
|
|
||||||
})
|
})
|
||||||
|
.get("/v1/tenant", |r| api_handler(r, tenant_list_handler))
|
||||||
|
.post("/v1/tenant", |r| api_handler(r, tenant_create_handler))
|
||||||
|
.get("/v1/tenant/:tenant_id", |r| api_handler(r, tenant_status))
|
||||||
.get("/v1/tenant/:tenant_id/synthetic_size", |r| {
|
.get("/v1/tenant/:tenant_id/synthetic_size", |r| {
|
||||||
RequestSpan(tenant_size_handler).handle(r)
|
api_handler(r, tenant_size_handler)
|
||||||
})
|
})
|
||||||
.put("/v1/tenant/config", |r| {
|
.put("/v1/tenant/config", |r| {
|
||||||
RequestSpan(update_tenant_config_handler).handle(r)
|
api_handler(r, update_tenant_config_handler)
|
||||||
})
|
})
|
||||||
.get("/v1/tenant/:tenant_id/config", |r| {
|
.get("/v1/tenant/:tenant_id/config", |r| {
|
||||||
RequestSpan(get_tenant_config_handler).handle(r)
|
api_handler(r, get_tenant_config_handler)
|
||||||
})
|
})
|
||||||
.get("/v1/tenant/:tenant_id/timeline", |r| {
|
.get("/v1/tenant/:tenant_id/timeline", |r| {
|
||||||
RequestSpan(timeline_list_handler).handle(r)
|
api_handler(r, timeline_list_handler)
|
||||||
})
|
})
|
||||||
.post("/v1/tenant/:tenant_id/timeline", |r| {
|
.post("/v1/tenant/:tenant_id/timeline", |r| {
|
||||||
RequestSpan(timeline_create_handler).handle(r)
|
api_handler(r, timeline_create_handler)
|
||||||
})
|
})
|
||||||
.post("/v1/tenant/:tenant_id/attach", |r| {
|
.post("/v1/tenant/:tenant_id/attach", |r| {
|
||||||
RequestSpan(tenant_attach_handler).handle(r)
|
api_handler(r, tenant_attach_handler)
|
||||||
})
|
})
|
||||||
.post("/v1/tenant/:tenant_id/detach", |r| {
|
.post("/v1/tenant/:tenant_id/detach", |r| {
|
||||||
RequestSpan(tenant_detach_handler).handle(r)
|
api_handler(r, tenant_detach_handler)
|
||||||
})
|
})
|
||||||
.post("/v1/tenant/:tenant_id/load", |r| {
|
.post("/v1/tenant/:tenant_id/load", |r| {
|
||||||
RequestSpan(tenant_load_handler).handle(r)
|
api_handler(r, tenant_load_handler)
|
||||||
})
|
})
|
||||||
.post("/v1/tenant/:tenant_id/ignore", |r| {
|
.post("/v1/tenant/:tenant_id/ignore", |r| {
|
||||||
RequestSpan(tenant_ignore_handler).handle(r)
|
api_handler(r, tenant_ignore_handler)
|
||||||
})
|
})
|
||||||
.get("/v1/tenant/:tenant_id/timeline/:timeline_id", |r| {
|
.get("/v1/tenant/:tenant_id/timeline/:timeline_id", |r| {
|
||||||
RequestSpan(timeline_detail_handler).handle(r)
|
api_handler(r, timeline_detail_handler)
|
||||||
})
|
})
|
||||||
.get(
|
.get(
|
||||||
"/v1/tenant/:tenant_id/timeline/:timeline_id/get_lsn_by_timestamp",
|
"/v1/tenant/:tenant_id/timeline/:timeline_id/get_lsn_by_timestamp",
|
||||||
|r| RequestSpan(get_lsn_by_timestamp_handler).handle(r),
|
|r| api_handler(r, get_lsn_by_timestamp_handler),
|
||||||
)
|
)
|
||||||
.put("/v1/tenant/:tenant_id/timeline/:timeline_id/do_gc", |r| {
|
.put("/v1/tenant/:tenant_id/timeline/:timeline_id/do_gc", |r| {
|
||||||
RequestSpan(timeline_gc_handler).handle(r)
|
api_handler(r, timeline_gc_handler)
|
||||||
|
})
|
||||||
|
.put("/v1/tenant/:tenant_id/timeline/:timeline_id/compact", |r| {
|
||||||
|
testing_api_handler("run timeline compaction", r, timeline_compact_handler)
|
||||||
})
|
})
|
||||||
.put(
|
|
||||||
"/v1/tenant/:tenant_id/timeline/:timeline_id/compact",
|
|
||||||
testing_api!("run timeline compaction", timeline_compact_handler),
|
|
||||||
)
|
|
||||||
.put(
|
.put(
|
||||||
"/v1/tenant/:tenant_id/timeline/:timeline_id/checkpoint",
|
"/v1/tenant/:tenant_id/timeline/:timeline_id/checkpoint",
|
||||||
testing_api!("run timeline checkpoint", timeline_checkpoint_handler),
|
|r| testing_api_handler("run timeline checkpoint", r, timeline_checkpoint_handler),
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/v1/tenant/:tenant_id/timeline/:timeline_id/download_remote_layers",
|
"/v1/tenant/:tenant_id/timeline/:timeline_id/download_remote_layers",
|
||||||
|r| RequestSpan(timeline_download_remote_layers_handler_post).handle(r),
|
|r| api_handler(r, timeline_download_remote_layers_handler_post),
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
"/v1/tenant/:tenant_id/timeline/:timeline_id/download_remote_layers",
|
"/v1/tenant/:tenant_id/timeline/:timeline_id/download_remote_layers",
|
||||||
|r| RequestSpan(timeline_download_remote_layers_handler_get).handle(r),
|
|r| api_handler(r, timeline_download_remote_layers_handler_get),
|
||||||
)
|
)
|
||||||
.delete("/v1/tenant/:tenant_id/timeline/:timeline_id", |r| {
|
.delete("/v1/tenant/:tenant_id/timeline/:timeline_id", |r| {
|
||||||
RequestSpan(timeline_delete_handler).handle(r)
|
api_handler(r, timeline_delete_handler)
|
||||||
})
|
})
|
||||||
.get("/v1/tenant/:tenant_id/timeline/:timeline_id/layer", |r| {
|
.get("/v1/tenant/:tenant_id/timeline/:timeline_id/layer", |r| {
|
||||||
RequestSpan(layer_map_info_handler).handle(r)
|
api_handler(r, layer_map_info_handler)
|
||||||
})
|
})
|
||||||
.get(
|
.get(
|
||||||
"/v1/tenant/:tenant_id/timeline/:timeline_id/layer/:layer_file_name",
|
"/v1/tenant/:tenant_id/timeline/:timeline_id/layer/:layer_file_name",
|
||||||
|r| RequestSpan(layer_download_handler).handle(r),
|
|r| api_handler(r, layer_download_handler),
|
||||||
)
|
)
|
||||||
.delete(
|
.delete(
|
||||||
"/v1/tenant/:tenant_id/timeline/:timeline_id/layer/:layer_file_name",
|
"/v1/tenant/:tenant_id/timeline/:timeline_id/layer/:layer_file_name",
|
||||||
|r| RequestSpan(evict_timeline_layer_handler).handle(r),
|
|r| api_handler(r, evict_timeline_layer_handler),
|
||||||
)
|
)
|
||||||
.put("/v1/disk_usage_eviction/run", |r| {
|
.put("/v1/disk_usage_eviction/run", |r| {
|
||||||
RequestSpan(disk_usage_eviction_run).handle(r)
|
api_handler(r, disk_usage_eviction_run)
|
||||||
|
})
|
||||||
|
.put("/v1/tenant/:tenant_id/break", |r| {
|
||||||
|
testing_api_handler("set tenant state to broken", r, handle_tenant_break)
|
||||||
|
})
|
||||||
|
.get("/v1/panic", |r| api_handler(r, always_panic_handler))
|
||||||
|
.post("/v1/tracing/event", |r| {
|
||||||
|
testing_api_handler("emit a tracing event", r, post_tracing_event_handler)
|
||||||
})
|
})
|
||||||
.put(
|
|
||||||
"/v1/tenant/:tenant_id/break",
|
|
||||||
testing_api!("set tenant state to broken", handle_tenant_break),
|
|
||||||
)
|
|
||||||
.get("/v1/panic", |r| RequestSpan(always_panic_handler).handle(r))
|
|
||||||
.post(
|
|
||||||
"/v1/tracing/event",
|
|
||||||
testing_api!("emit a tracing event", post_tracing_event_handler),
|
|
||||||
)
|
|
||||||
.any(handler_404))
|
.any(handler_404))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
mod auth;
|
mod auth;
|
||||||
pub mod basebackup;
|
pub mod basebackup;
|
||||||
pub mod broker_client;
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod consumption_metrics;
|
pub mod consumption_metrics;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
@@ -36,7 +35,7 @@ use tracing::info;
|
|||||||
/// backwards-compatible changes to the metadata format.
|
/// backwards-compatible changes to the metadata format.
|
||||||
pub const STORAGE_FORMAT_VERSION: u16 = 3;
|
pub const STORAGE_FORMAT_VERSION: u16 = 3;
|
||||||
|
|
||||||
pub const DEFAULT_PG_VERSION: u32 = 14;
|
pub const DEFAULT_PG_VERSION: u32 = 15;
|
||||||
|
|
||||||
// Magic constants used to identify different kinds of files
|
// Magic constants used to identify different kinds of files
|
||||||
pub const IMAGE_FILE_MAGIC: u16 = 0x5A60;
|
pub const IMAGE_FILE_MAGIC: u16 = 0x5A60;
|
||||||
@@ -46,6 +45,7 @@ static ZERO_PAGE: bytes::Bytes = bytes::Bytes::from_static(&[0u8; 8192]);
|
|||||||
|
|
||||||
pub use crate::metrics::preinitialize_metrics;
|
pub use crate::metrics::preinitialize_metrics;
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
pub async fn shutdown_pageserver(exit_code: i32) {
|
pub async fn shutdown_pageserver(exit_code: i32) {
|
||||||
// Shut down the libpq endpoint task. This prevents new connections from
|
// Shut down the libpq endpoint task. This prevents new connections from
|
||||||
// being accepted.
|
// being accepted.
|
||||||
|
|||||||
@@ -84,6 +84,16 @@ pub static STORAGE_TIME_GLOBAL: Lazy<HistogramVec> = Lazy::new(|| {
|
|||||||
.expect("failed to define a metric")
|
.expect("failed to define a metric")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static READ_NUM_FS_LAYERS: Lazy<HistogramVec> = Lazy::new(|| {
|
||||||
|
register_histogram_vec!(
|
||||||
|
"pageserver_read_num_fs_layers",
|
||||||
|
"Number of persistent layers accessed for processing a read request, including those in the cache",
|
||||||
|
&["tenant_id", "timeline_id"],
|
||||||
|
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 10.0, 20.0, 50.0, 100.0],
|
||||||
|
)
|
||||||
|
.expect("failed to define a metric")
|
||||||
|
});
|
||||||
|
|
||||||
// Metrics collected on operations on the storage repository.
|
// Metrics collected on operations on the storage repository.
|
||||||
static RECONSTRUCT_TIME: Lazy<HistogramVec> = Lazy::new(|| {
|
static RECONSTRUCT_TIME: Lazy<HistogramVec> = Lazy::new(|| {
|
||||||
register_histogram_vec!(
|
register_histogram_vec!(
|
||||||
@@ -95,6 +105,25 @@ static RECONSTRUCT_TIME: Lazy<HistogramVec> = Lazy::new(|| {
|
|||||||
.expect("failed to define a metric")
|
.expect("failed to define a metric")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static MATERIALIZED_PAGE_CACHE_HIT_DIRECT: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||||
|
register_int_counter_vec!(
|
||||||
|
"pageserver_materialized_cache_hits_direct_total",
|
||||||
|
"Number of cache hits from materialized page cache without redo",
|
||||||
|
&["tenant_id", "timeline_id"]
|
||||||
|
)
|
||||||
|
.expect("failed to define a metric")
|
||||||
|
});
|
||||||
|
|
||||||
|
static GET_RECONSTRUCT_DATA_TIME: Lazy<HistogramVec> = Lazy::new(|| {
|
||||||
|
register_histogram_vec!(
|
||||||
|
"pageserver_getpage_get_reconstruct_data_seconds",
|
||||||
|
"Time spent in get_reconstruct_value_data",
|
||||||
|
&["tenant_id", "timeline_id"],
|
||||||
|
CRITICAL_OP_BUCKETS.into(),
|
||||||
|
)
|
||||||
|
.expect("failed to define a metric")
|
||||||
|
});
|
||||||
|
|
||||||
static MATERIALIZED_PAGE_CACHE_HIT: Lazy<IntCounterVec> = Lazy::new(|| {
|
static MATERIALIZED_PAGE_CACHE_HIT: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||||
register_int_counter_vec!(
|
register_int_counter_vec!(
|
||||||
"pageserver_materialized_cache_hits_total",
|
"pageserver_materialized_cache_hits_total",
|
||||||
@@ -354,6 +383,7 @@ const STORAGE_IO_TIME_BUCKETS: &[f64] = &[
|
|||||||
0.001000, // 1000 usec
|
0.001000, // 1000 usec
|
||||||
0.030, // 30 ms
|
0.030, // 30 ms
|
||||||
1.000, // 1000 ms
|
1.000, // 1000 ms
|
||||||
|
30.000, // 30000 ms
|
||||||
];
|
];
|
||||||
|
|
||||||
const STORAGE_IO_TIME_OPERATIONS: &[&str] = &[
|
const STORAGE_IO_TIME_OPERATIONS: &[&str] = &[
|
||||||
@@ -622,7 +652,7 @@ pub static WAL_REDO_TIME: Lazy<Histogram> = Lazy::new(|| {
|
|||||||
pub static WAL_REDO_WAIT_TIME: Lazy<Histogram> = Lazy::new(|| {
|
pub static WAL_REDO_WAIT_TIME: Lazy<Histogram> = Lazy::new(|| {
|
||||||
register_histogram!(
|
register_histogram!(
|
||||||
"pageserver_wal_redo_wait_seconds",
|
"pageserver_wal_redo_wait_seconds",
|
||||||
"Time spent waiting for access to the WAL redo process",
|
"Time spent waiting for access to the Postgres WAL redo process",
|
||||||
redo_histogram_time_buckets!(),
|
redo_histogram_time_buckets!(),
|
||||||
)
|
)
|
||||||
.expect("failed to define a metric")
|
.expect("failed to define a metric")
|
||||||
@@ -631,7 +661,7 @@ pub static WAL_REDO_WAIT_TIME: Lazy<Histogram> = Lazy::new(|| {
|
|||||||
pub static WAL_REDO_RECORDS_HISTOGRAM: Lazy<Histogram> = Lazy::new(|| {
|
pub static WAL_REDO_RECORDS_HISTOGRAM: Lazy<Histogram> = Lazy::new(|| {
|
||||||
register_histogram!(
|
register_histogram!(
|
||||||
"pageserver_wal_redo_records_histogram",
|
"pageserver_wal_redo_records_histogram",
|
||||||
"Histogram of number of records replayed per redo",
|
"Histogram of number of records replayed per redo in the Postgres WAL redo process",
|
||||||
redo_histogram_count_buckets!(),
|
redo_histogram_count_buckets!(),
|
||||||
)
|
)
|
||||||
.expect("failed to define a metric")
|
.expect("failed to define a metric")
|
||||||
@@ -640,7 +670,7 @@ pub static WAL_REDO_RECORDS_HISTOGRAM: Lazy<Histogram> = Lazy::new(|| {
|
|||||||
pub static WAL_REDO_BYTES_HISTOGRAM: Lazy<Histogram> = Lazy::new(|| {
|
pub static WAL_REDO_BYTES_HISTOGRAM: Lazy<Histogram> = Lazy::new(|| {
|
||||||
register_histogram!(
|
register_histogram!(
|
||||||
"pageserver_wal_redo_bytes_histogram",
|
"pageserver_wal_redo_bytes_histogram",
|
||||||
"Histogram of number of records replayed per redo",
|
"Histogram of number of records replayed per redo sent to Postgres",
|
||||||
redo_bytes_histogram_count_buckets!(),
|
redo_bytes_histogram_count_buckets!(),
|
||||||
)
|
)
|
||||||
.expect("failed to define a metric")
|
.expect("failed to define a metric")
|
||||||
@@ -723,7 +753,9 @@ pub struct TimelineMetrics {
|
|||||||
tenant_id: String,
|
tenant_id: String,
|
||||||
timeline_id: String,
|
timeline_id: String,
|
||||||
pub reconstruct_time_histo: Histogram,
|
pub reconstruct_time_histo: Histogram,
|
||||||
|
pub get_reconstruct_data_time_histo: Histogram,
|
||||||
pub materialized_page_cache_hit_counter: GenericCounter<AtomicU64>,
|
pub materialized_page_cache_hit_counter: GenericCounter<AtomicU64>,
|
||||||
|
pub materialized_page_cache_hit_upon_request_counter: GenericCounter<AtomicU64>,
|
||||||
pub flush_time_histo: StorageTimeMetrics,
|
pub flush_time_histo: StorageTimeMetrics,
|
||||||
pub compact_time_histo: StorageTimeMetrics,
|
pub compact_time_histo: StorageTimeMetrics,
|
||||||
pub create_images_time_histo: StorageTimeMetrics,
|
pub create_images_time_histo: StorageTimeMetrics,
|
||||||
@@ -734,6 +766,7 @@ pub struct TimelineMetrics {
|
|||||||
pub last_record_gauge: IntGauge,
|
pub last_record_gauge: IntGauge,
|
||||||
pub wait_lsn_time_histo: Histogram,
|
pub wait_lsn_time_histo: Histogram,
|
||||||
pub resident_physical_size_gauge: UIntGauge,
|
pub resident_physical_size_gauge: UIntGauge,
|
||||||
|
pub read_num_fs_layers: Histogram,
|
||||||
/// copy of LayeredTimeline.current_logical_size
|
/// copy of LayeredTimeline.current_logical_size
|
||||||
pub current_logical_size_gauge: UIntGauge,
|
pub current_logical_size_gauge: UIntGauge,
|
||||||
pub num_persistent_files_created: IntCounter,
|
pub num_persistent_files_created: IntCounter,
|
||||||
@@ -753,6 +786,9 @@ impl TimelineMetrics {
|
|||||||
let reconstruct_time_histo = RECONSTRUCT_TIME
|
let reconstruct_time_histo = RECONSTRUCT_TIME
|
||||||
.get_metric_with_label_values(&[&tenant_id, &timeline_id])
|
.get_metric_with_label_values(&[&tenant_id, &timeline_id])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let get_reconstruct_data_time_histo = GET_RECONSTRUCT_DATA_TIME
|
||||||
|
.get_metric_with_label_values(&[&tenant_id, &timeline_id])
|
||||||
|
.unwrap();
|
||||||
let materialized_page_cache_hit_counter = MATERIALIZED_PAGE_CACHE_HIT
|
let materialized_page_cache_hit_counter = MATERIALIZED_PAGE_CACHE_HIT
|
||||||
.get_metric_with_label_values(&[&tenant_id, &timeline_id])
|
.get_metric_with_label_values(&[&tenant_id, &timeline_id])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -794,6 +830,12 @@ impl TimelineMetrics {
|
|||||||
let evictions = EVICTIONS
|
let evictions = EVICTIONS
|
||||||
.get_metric_with_label_values(&[&tenant_id, &timeline_id])
|
.get_metric_with_label_values(&[&tenant_id, &timeline_id])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let read_num_fs_layers = READ_NUM_FS_LAYERS
|
||||||
|
.get_metric_with_label_values(&[&tenant_id, &timeline_id])
|
||||||
|
.unwrap();
|
||||||
|
let materialized_page_cache_hit_upon_request_counter = MATERIALIZED_PAGE_CACHE_HIT_DIRECT
|
||||||
|
.get_metric_with_label_values(&[&tenant_id, &timeline_id])
|
||||||
|
.unwrap();
|
||||||
let evictions_with_low_residence_duration =
|
let evictions_with_low_residence_duration =
|
||||||
evictions_with_low_residence_duration_builder.build(&tenant_id, &timeline_id);
|
evictions_with_low_residence_duration_builder.build(&tenant_id, &timeline_id);
|
||||||
|
|
||||||
@@ -801,7 +843,9 @@ impl TimelineMetrics {
|
|||||||
tenant_id,
|
tenant_id,
|
||||||
timeline_id,
|
timeline_id,
|
||||||
reconstruct_time_histo,
|
reconstruct_time_histo,
|
||||||
|
get_reconstruct_data_time_histo,
|
||||||
materialized_page_cache_hit_counter,
|
materialized_page_cache_hit_counter,
|
||||||
|
materialized_page_cache_hit_upon_request_counter,
|
||||||
flush_time_histo,
|
flush_time_histo,
|
||||||
compact_time_histo,
|
compact_time_histo,
|
||||||
create_images_time_histo,
|
create_images_time_histo,
|
||||||
@@ -819,6 +863,7 @@ impl TimelineMetrics {
|
|||||||
evictions_with_low_residence_duration: std::sync::RwLock::new(
|
evictions_with_low_residence_duration: std::sync::RwLock::new(
|
||||||
evictions_with_low_residence_duration,
|
evictions_with_low_residence_duration,
|
||||||
),
|
),
|
||||||
|
read_num_fs_layers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -828,7 +873,9 @@ impl Drop for TimelineMetrics {
|
|||||||
let tenant_id = &self.tenant_id;
|
let tenant_id = &self.tenant_id;
|
||||||
let timeline_id = &self.timeline_id;
|
let timeline_id = &self.timeline_id;
|
||||||
let _ = RECONSTRUCT_TIME.remove_label_values(&[tenant_id, timeline_id]);
|
let _ = RECONSTRUCT_TIME.remove_label_values(&[tenant_id, timeline_id]);
|
||||||
|
let _ = GET_RECONSTRUCT_DATA_TIME.remove_label_values(&[tenant_id, timeline_id]);
|
||||||
let _ = MATERIALIZED_PAGE_CACHE_HIT.remove_label_values(&[tenant_id, timeline_id]);
|
let _ = MATERIALIZED_PAGE_CACHE_HIT.remove_label_values(&[tenant_id, timeline_id]);
|
||||||
|
let _ = MATERIALIZED_PAGE_CACHE_HIT_DIRECT.remove_label_values(&[tenant_id, timeline_id]);
|
||||||
let _ = LAST_RECORD_LSN.remove_label_values(&[tenant_id, timeline_id]);
|
let _ = LAST_RECORD_LSN.remove_label_values(&[tenant_id, timeline_id]);
|
||||||
let _ = WAIT_LSN_TIME.remove_label_values(&[tenant_id, timeline_id]);
|
let _ = WAIT_LSN_TIME.remove_label_values(&[tenant_id, timeline_id]);
|
||||||
let _ = RESIDENT_PHYSICAL_SIZE.remove_label_values(&[tenant_id, timeline_id]);
|
let _ = RESIDENT_PHYSICAL_SIZE.remove_label_values(&[tenant_id, timeline_id]);
|
||||||
@@ -836,6 +883,8 @@ impl Drop for TimelineMetrics {
|
|||||||
let _ = NUM_PERSISTENT_FILES_CREATED.remove_label_values(&[tenant_id, timeline_id]);
|
let _ = NUM_PERSISTENT_FILES_CREATED.remove_label_values(&[tenant_id, timeline_id]);
|
||||||
let _ = PERSISTENT_BYTES_WRITTEN.remove_label_values(&[tenant_id, timeline_id]);
|
let _ = PERSISTENT_BYTES_WRITTEN.remove_label_values(&[tenant_id, timeline_id]);
|
||||||
let _ = EVICTIONS.remove_label_values(&[tenant_id, timeline_id]);
|
let _ = EVICTIONS.remove_label_values(&[tenant_id, timeline_id]);
|
||||||
|
let _ = READ_NUM_FS_LAYERS.remove_label_values(&[tenant_id, timeline_id]);
|
||||||
|
|
||||||
self.evictions_with_low_residence_duration
|
self.evictions_with_low_residence_duration
|
||||||
.write()
|
.write()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ use crate::import_datadir::import_wal_from_tar;
|
|||||||
use crate::metrics::{LIVE_CONNECTIONS_COUNT, SMGR_QUERY_TIME};
|
use crate::metrics::{LIVE_CONNECTIONS_COUNT, SMGR_QUERY_TIME};
|
||||||
use crate::task_mgr;
|
use crate::task_mgr;
|
||||||
use crate::task_mgr::TaskKind;
|
use crate::task_mgr::TaskKind;
|
||||||
|
use crate::tenant;
|
||||||
use crate::tenant::mgr;
|
use crate::tenant::mgr;
|
||||||
|
use crate::tenant::mgr::GetTenantError;
|
||||||
use crate::tenant::{Tenant, Timeline};
|
use crate::tenant::{Tenant, Timeline};
|
||||||
use crate::trace::Tracer;
|
use crate::trace::Tracer;
|
||||||
|
|
||||||
@@ -172,6 +174,7 @@ async fn read_tar_eof(mut reader: (impl AsyncRead + Unpin)) -> anyhow::Result<()
|
|||||||
///
|
///
|
||||||
pub async fn libpq_listener_main(
|
pub async fn libpq_listener_main(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
|
broker_client: storage_broker::BrokerClientChannel,
|
||||||
auth: Option<Arc<JwtAuth>>,
|
auth: Option<Arc<JwtAuth>>,
|
||||||
listener: TcpListener,
|
listener: TcpListener,
|
||||||
auth_type: AuthType,
|
auth_type: AuthType,
|
||||||
@@ -213,7 +216,14 @@ pub async fn libpq_listener_main(
|
|||||||
None,
|
None,
|
||||||
"serving compute connection task",
|
"serving compute connection task",
|
||||||
false,
|
false,
|
||||||
page_service_conn_main(conf, local_auth, socket, auth_type, connection_ctx),
|
page_service_conn_main(
|
||||||
|
conf,
|
||||||
|
broker_client.clone(),
|
||||||
|
local_auth,
|
||||||
|
socket,
|
||||||
|
auth_type,
|
||||||
|
connection_ctx,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -230,6 +240,7 @@ pub async fn libpq_listener_main(
|
|||||||
|
|
||||||
async fn page_service_conn_main(
|
async fn page_service_conn_main(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
|
broker_client: storage_broker::BrokerClientChannel,
|
||||||
auth: Option<Arc<JwtAuth>>,
|
auth: Option<Arc<JwtAuth>>,
|
||||||
socket: tokio::net::TcpStream,
|
socket: tokio::net::TcpStream,
|
||||||
auth_type: AuthType,
|
auth_type: AuthType,
|
||||||
@@ -266,7 +277,7 @@ async fn page_service_conn_main(
|
|||||||
// and create a child per-query context when it invokes process_query.
|
// and create a child per-query context when it invokes process_query.
|
||||||
// But it's in a shared crate, so, we store connection_ctx inside PageServerHandler
|
// But it's in a shared crate, so, we store connection_ctx inside PageServerHandler
|
||||||
// and create the per-query context in process_query ourselves.
|
// and create the per-query context in process_query ourselves.
|
||||||
let mut conn_handler = PageServerHandler::new(conf, auth, connection_ctx);
|
let mut conn_handler = PageServerHandler::new(conf, broker_client, auth, connection_ctx);
|
||||||
let pgbackend = PostgresBackend::new_from_io(socket, peer_addr, auth_type, None)?;
|
let pgbackend = PostgresBackend::new_from_io(socket, peer_addr, auth_type, None)?;
|
||||||
|
|
||||||
match pgbackend
|
match pgbackend
|
||||||
@@ -324,6 +335,7 @@ impl PageRequestMetrics {
|
|||||||
|
|
||||||
struct PageServerHandler {
|
struct PageServerHandler {
|
||||||
_conf: &'static PageServerConf,
|
_conf: &'static PageServerConf,
|
||||||
|
broker_client: storage_broker::BrokerClientChannel,
|
||||||
auth: Option<Arc<JwtAuth>>,
|
auth: Option<Arc<JwtAuth>>,
|
||||||
claims: Option<Claims>,
|
claims: Option<Claims>,
|
||||||
|
|
||||||
@@ -337,11 +349,13 @@ struct PageServerHandler {
|
|||||||
impl PageServerHandler {
|
impl PageServerHandler {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
|
broker_client: storage_broker::BrokerClientChannel,
|
||||||
auth: Option<Arc<JwtAuth>>,
|
auth: Option<Arc<JwtAuth>>,
|
||||||
connection_ctx: RequestContext,
|
connection_ctx: RequestContext,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
PageServerHandler {
|
PageServerHandler {
|
||||||
_conf: conf,
|
_conf: conf,
|
||||||
|
broker_client,
|
||||||
auth,
|
auth,
|
||||||
claims: None,
|
claims: None,
|
||||||
connection_ctx,
|
connection_ctx,
|
||||||
@@ -494,7 +508,12 @@ impl PageServerHandler {
|
|||||||
|
|
||||||
let mut copyin_reader = pin!(StreamReader::new(copyin_stream(pgb)));
|
let mut copyin_reader = pin!(StreamReader::new(copyin_stream(pgb)));
|
||||||
timeline
|
timeline
|
||||||
.import_basebackup_from_tar(&mut copyin_reader, base_lsn, &ctx)
|
.import_basebackup_from_tar(
|
||||||
|
&mut copyin_reader,
|
||||||
|
base_lsn,
|
||||||
|
self.broker_client.clone(),
|
||||||
|
&ctx,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Read the end of the tar archive.
|
// Read the end of the tar archive.
|
||||||
@@ -1131,7 +1150,9 @@ enum GetActiveTenantError {
|
|||||||
wait_time: Duration,
|
wait_time: Duration,
|
||||||
},
|
},
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Other(#[from] anyhow::Error),
|
NotFound(GetTenantError),
|
||||||
|
#[error(transparent)]
|
||||||
|
WaitTenantActive(tenant::WaitToBecomeActiveError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<GetActiveTenantError> for QueryError {
|
impl From<GetActiveTenantError> for QueryError {
|
||||||
@@ -1140,7 +1161,8 @@ impl From<GetActiveTenantError> for QueryError {
|
|||||||
GetActiveTenantError::WaitForActiveTimeout { .. } => QueryError::Disconnected(
|
GetActiveTenantError::WaitForActiveTimeout { .. } => QueryError::Disconnected(
|
||||||
ConnectionError::Io(io::Error::new(io::ErrorKind::TimedOut, e.to_string())),
|
ConnectionError::Io(io::Error::new(io::ErrorKind::TimedOut, e.to_string())),
|
||||||
),
|
),
|
||||||
GetActiveTenantError::Other(e) => QueryError::Other(e),
|
GetActiveTenantError::WaitTenantActive(e) => QueryError::Other(anyhow::Error::new(e)),
|
||||||
|
GetActiveTenantError::NotFound(e) => QueryError::Other(anyhow::Error::new(e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1156,13 +1178,16 @@ async fn get_active_tenant_with_timeout(
|
|||||||
) -> Result<Arc<Tenant>, GetActiveTenantError> {
|
) -> Result<Arc<Tenant>, GetActiveTenantError> {
|
||||||
let tenant = match mgr::get_tenant(tenant_id, false).await {
|
let tenant = match mgr::get_tenant(tenant_id, false).await {
|
||||||
Ok(tenant) => tenant,
|
Ok(tenant) => tenant,
|
||||||
Err(e) => return Err(GetActiveTenantError::Other(e.into())),
|
Err(e @ GetTenantError::NotFound(_)) => return Err(GetActiveTenantError::NotFound(e)),
|
||||||
|
Err(GetTenantError::NotActive(_)) => {
|
||||||
|
unreachable!("we're calling get_tenant with active=false")
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let wait_time = Duration::from_secs(30);
|
let wait_time = Duration::from_secs(30);
|
||||||
match tokio::time::timeout(wait_time, tenant.wait_to_become_active()).await {
|
match tokio::time::timeout(wait_time, tenant.wait_to_become_active()).await {
|
||||||
Ok(Ok(())) => Ok(tenant),
|
Ok(Ok(())) => Ok(tenant),
|
||||||
// no .context(), the error message is good enough and some tests depend on it
|
// no .context(), the error message is good enough and some tests depend on it
|
||||||
Ok(Err(wait_error)) => Err(GetActiveTenantError::Other(wait_error)),
|
Ok(Err(e)) => Err(GetActiveTenantError::WaitTenantActive(e)),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let latest_state = tenant.current_state();
|
let latest_state = tenant.current_state();
|
||||||
if latest_state == TenantState::Active {
|
if latest_state == TenantState::Active {
|
||||||
@@ -1177,13 +1202,34 @@ async fn get_active_tenant_with_timeout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
enum GetActiveTimelineError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Tenant(GetActiveTenantError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Timeline(anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetActiveTimelineError> for QueryError {
|
||||||
|
fn from(e: GetActiveTimelineError) -> Self {
|
||||||
|
match e {
|
||||||
|
GetActiveTimelineError::Tenant(e) => e.into(),
|
||||||
|
GetActiveTimelineError::Timeline(e) => QueryError::Other(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Shorthand for getting a reference to a Timeline of an Active tenant.
|
/// Shorthand for getting a reference to a Timeline of an Active tenant.
|
||||||
async fn get_active_tenant_timeline(
|
async fn get_active_tenant_timeline(
|
||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
timeline_id: TimelineId,
|
timeline_id: TimelineId,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<Arc<Timeline>, GetActiveTenantError> {
|
) -> Result<Arc<Timeline>, GetActiveTimelineError> {
|
||||||
let tenant = get_active_tenant_with_timeout(tenant_id, ctx).await?;
|
let tenant = get_active_tenant_with_timeout(tenant_id, ctx)
|
||||||
let timeline = tenant.get_timeline(timeline_id, true)?;
|
.await
|
||||||
|
.map_err(GetActiveTimelineError::Tenant)?;
|
||||||
|
let timeline = tenant
|
||||||
|
.get_timeline(timeline_id, true)
|
||||||
|
.map_err(GetActiveTimelineError::Timeline)?;
|
||||||
Ok(timeline)
|
Ok(timeline)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1600,9 +1600,7 @@ pub fn create_test_timeline(
|
|||||||
pg_version: u32,
|
pg_version: u32,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<std::sync::Arc<Timeline>> {
|
) -> anyhow::Result<std::sync::Arc<Timeline>> {
|
||||||
let tline = tenant
|
let tline = tenant.create_test_timeline(timeline_id, Lsn(8), pg_version, ctx)?;
|
||||||
.create_empty_timeline(timeline_id, Lsn(8), pg_version, ctx)?
|
|
||||||
.initialize(ctx)?;
|
|
||||||
let mut m = tline.begin_modification(Lsn(8));
|
let mut m = tline.begin_modification(Lsn(8));
|
||||||
m.init_empty()?;
|
m.init_empty()?;
|
||||||
m.commit()?;
|
m.commit()?;
|
||||||
|
|||||||
+537
-365
File diff suppressed because it is too large
Load Diff
@@ -99,6 +99,7 @@ pub struct TenantConf {
|
|||||||
// See the corresponding metric's help string.
|
// See the corresponding metric's help string.
|
||||||
#[serde(with = "humantime_serde")]
|
#[serde(with = "humantime_serde")]
|
||||||
pub evictions_low_residence_duration_metric_threshold: Duration,
|
pub evictions_low_residence_duration_metric_threshold: Duration,
|
||||||
|
pub gc_feedback: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as TenantConf, but this struct preserves the information about
|
/// Same as TenantConf, but this struct preserves the information about
|
||||||
@@ -175,6 +176,10 @@ pub struct TenantConfOpt {
|
|||||||
#[serde(with = "humantime_serde")]
|
#[serde(with = "humantime_serde")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub evictions_low_residence_duration_metric_threshold: Option<Duration>,
|
pub evictions_low_residence_duration_metric_threshold: Option<Duration>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub gc_feedback: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -242,6 +247,7 @@ impl TenantConfOpt {
|
|||||||
evictions_low_residence_duration_metric_threshold: self
|
evictions_low_residence_duration_metric_threshold: self
|
||||||
.evictions_low_residence_duration_metric_threshold
|
.evictions_low_residence_duration_metric_threshold
|
||||||
.unwrap_or(global_conf.evictions_low_residence_duration_metric_threshold),
|
.unwrap_or(global_conf.evictions_low_residence_duration_metric_threshold),
|
||||||
|
gc_feedback: self.gc_feedback.unwrap_or(global_conf.gc_feedback),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,6 +284,7 @@ impl Default for TenantConf {
|
|||||||
DEFAULT_EVICTIONS_LOW_RESIDENCE_DURATION_METRIC_THRESHOLD,
|
DEFAULT_EVICTIONS_LOW_RESIDENCE_DURATION_METRIC_THRESHOLD,
|
||||||
)
|
)
|
||||||
.expect("cannot parse default evictions_low_residence_duration_metric_threshold"),
|
.expect("cannot parse default evictions_low_residence_duration_metric_threshold"),
|
||||||
|
gc_feedback: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,6 +379,7 @@ impl TryFrom<&'_ models::TenantConfig> for TenantConfOpt {
|
|||||||
))?,
|
))?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
tenant_conf.gc_feedback = request_data.gc_feedback;
|
||||||
|
|
||||||
Ok(tenant_conf)
|
Ok(tenant_conf)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,6 +204,35 @@ fn test_off_by_one() {
|
|||||||
assert_eq!(version.image_coverage.query(5), None);
|
assert_eq!(version.image_coverage.query(5), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// White-box regression test, checking for incorrect removal of node at key.end
|
||||||
|
#[test]
|
||||||
|
fn test_regression() {
|
||||||
|
let mut map = HistoricLayerCoverage::<String>::new();
|
||||||
|
map.insert(
|
||||||
|
LayerKey {
|
||||||
|
key: 0..5,
|
||||||
|
lsn: 0..5,
|
||||||
|
is_image: false,
|
||||||
|
},
|
||||||
|
"Layer 1".to_string(),
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
LayerKey {
|
||||||
|
key: 0..5,
|
||||||
|
lsn: 1..2,
|
||||||
|
is_image: false,
|
||||||
|
},
|
||||||
|
"Layer 2".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If an insertion operation improperly deletes the endpoint of a previous layer
|
||||||
|
// (which is more likely to happen with layers that collide on key.end), we will
|
||||||
|
// end up with an infinite layer, covering the entire keyspace. Here we assert
|
||||||
|
// that there's no layer at key 100 because we didn't insert any layer there.
|
||||||
|
let version = map.get_version(100).unwrap();
|
||||||
|
assert_eq!(version.delta_coverage.query(100), None);
|
||||||
|
}
|
||||||
|
|
||||||
/// Cover edge cases where layers begin or end on the same key
|
/// Cover edge cases where layers begin or end on the same key
|
||||||
#[test]
|
#[test]
|
||||||
fn test_key_collision() {
|
fn test_key_collision() {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
// TODO the `im` crate has 20x more downloads and also has
|
// NOTE the `im` crate has 20x more downloads and also has
|
||||||
// persistent/immutable BTree. It also runs a bit faster but
|
// persistent/immutable BTree. But it's bugged so rpds is a
|
||||||
// results are not the same on some tests.
|
// better choice https://github.com/neondatabase/neon/issues/3395
|
||||||
use rpds::RedBlackTreeMapSync;
|
use rpds::RedBlackTreeMapSync;
|
||||||
|
|
||||||
/// Data structure that can efficiently:
|
/// Data structure that can efficiently:
|
||||||
@@ -10,19 +10,22 @@ use rpds::RedBlackTreeMapSync;
|
|||||||
/// - iterate the latest layers in a key range
|
/// - iterate the latest layers in a key range
|
||||||
/// - insert layers in non-decreasing lsn.start order
|
/// - insert layers in non-decreasing lsn.start order
|
||||||
///
|
///
|
||||||
/// The struct is parameterized over Value for easier
|
/// For a detailed explanation and justification of this approach, see:
|
||||||
/// testing, but in practice it's some sort of layer.
|
/// https://neon.tech/blog/persistent-structures-in-neons-wal-indexing
|
||||||
|
///
|
||||||
|
/// NOTE The struct is parameterized over Value for easier
|
||||||
|
/// testing, but in practice it's some sort of layer.
|
||||||
pub struct LayerCoverage<Value> {
|
pub struct LayerCoverage<Value> {
|
||||||
/// For every change in coverage (as we sweep the key space)
|
/// For every change in coverage (as we sweep the key space)
|
||||||
/// we store (lsn.end, value).
|
/// we store (lsn.end, value).
|
||||||
///
|
///
|
||||||
/// We use an immutable/persistent tree so that we can keep historic
|
/// NOTE We use an immutable/persistent tree so that we can keep historic
|
||||||
/// versions of this coverage without cloning the whole thing and
|
/// versions of this coverage without cloning the whole thing and
|
||||||
/// incurring quadratic memory cost. See HistoricLayerCoverage.
|
/// incurring quadratic memory cost. See HistoricLayerCoverage.
|
||||||
///
|
///
|
||||||
/// We use the Sync version of the map because we want Self to
|
/// NOTE We use the Sync version of the map because we want Self to
|
||||||
/// be Sync. Using nonsync might be faster, if we can work with
|
/// be Sync. Using nonsync might be faster, if we can work with
|
||||||
/// that.
|
/// that.
|
||||||
nodes: RedBlackTreeMapSync<i128, Option<(u64, Value)>>,
|
nodes: RedBlackTreeMapSync<i128, Option<(u64, Value)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +44,13 @@ impl<Value: Clone> LayerCoverage<Value> {
|
|||||||
|
|
||||||
/// Helper function to subdivide the key range without changing any values
|
/// Helper function to subdivide the key range without changing any values
|
||||||
///
|
///
|
||||||
|
/// This operation has no semantic effect by itself. It only helps us pin in
|
||||||
|
/// place the part of the coverage we don't want to change when inserting.
|
||||||
|
///
|
||||||
|
/// As an analogy, think of a polygon. If you add a vertex along one of the
|
||||||
|
/// segments, the polygon is still the same, but it behaves differently when
|
||||||
|
/// we move or delete one of the other points.
|
||||||
|
///
|
||||||
/// Complexity: O(log N)
|
/// Complexity: O(log N)
|
||||||
fn add_node(&mut self, key: i128) {
|
fn add_node(&mut self, key: i128) {
|
||||||
let value = match self.nodes.range(..=key).last() {
|
let value = match self.nodes.range(..=key).last() {
|
||||||
@@ -74,7 +84,7 @@ impl<Value: Clone> LayerCoverage<Value> {
|
|||||||
let mut to_update = Vec::new();
|
let mut to_update = Vec::new();
|
||||||
let mut to_remove = Vec::new();
|
let mut to_remove = Vec::new();
|
||||||
let mut prev_covered = false;
|
let mut prev_covered = false;
|
||||||
for (k, node) in self.nodes.range(key.clone()) {
|
for (k, node) in self.nodes.range(key) {
|
||||||
let needs_cover = match node {
|
let needs_cover = match node {
|
||||||
None => true,
|
None => true,
|
||||||
Some((h, _)) => h < &lsn.end,
|
Some((h, _)) => h < &lsn.end,
|
||||||
@@ -87,9 +97,8 @@ impl<Value: Clone> LayerCoverage<Value> {
|
|||||||
}
|
}
|
||||||
prev_covered = needs_cover;
|
prev_covered = needs_cover;
|
||||||
}
|
}
|
||||||
if !prev_covered {
|
// TODO check if the nodes inserted at key.start and key.end are safe
|
||||||
to_remove.push(key.end);
|
// to remove. It's fine to keep them but they could be redundant.
|
||||||
}
|
|
||||||
for k in to_update {
|
for k in to_update {
|
||||||
self.nodes.insert_mut(k, Some((lsn.end, value.clone())));
|
self.nodes.insert_mut(k, Some((lsn.end, value.clone())));
|
||||||
}
|
}
|
||||||
|
|||||||
+145
-30
@@ -10,6 +10,7 @@ use tokio::fs;
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
use tokio::task::JoinSet;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
use remote_storage::GenericRemoteStorage;
|
use remote_storage::GenericRemoteStorage;
|
||||||
@@ -19,9 +20,12 @@ use crate::config::PageServerConf;
|
|||||||
use crate::context::{DownloadBehavior, RequestContext};
|
use crate::context::{DownloadBehavior, RequestContext};
|
||||||
use crate::task_mgr::{self, TaskKind};
|
use crate::task_mgr::{self, TaskKind};
|
||||||
use crate::tenant::config::TenantConfOpt;
|
use crate::tenant::config::TenantConfOpt;
|
||||||
use crate::tenant::{create_tenant_files, CreateTenantFilesMode, Tenant, TenantState};
|
use crate::tenant::{
|
||||||
|
create_tenant_files, CreateTenantFilesMode, SetStoppingError, Tenant, TenantState,
|
||||||
|
};
|
||||||
use crate::IGNORED_TENANT_FILE_NAME;
|
use crate::IGNORED_TENANT_FILE_NAME;
|
||||||
|
|
||||||
|
use utils::completion;
|
||||||
use utils::fs_ext::PathExt;
|
use utils::fs_ext::PathExt;
|
||||||
use utils::id::{TenantId, TimelineId};
|
use utils::id::{TenantId, TimelineId};
|
||||||
|
|
||||||
@@ -58,10 +62,12 @@ static TENANTS: Lazy<RwLock<TenantsMap>> = Lazy::new(|| RwLock::new(TenantsMap::
|
|||||||
/// Initialize repositories with locally available timelines.
|
/// Initialize repositories with locally available timelines.
|
||||||
/// Timelines that are only partially available locally (remote storage has more data than this pageserver)
|
/// Timelines that are only partially available locally (remote storage has more data than this pageserver)
|
||||||
/// are scheduled for download and added to the tenant once download is completed.
|
/// are scheduled for download and added to the tenant once download is completed.
|
||||||
#[instrument(skip(conf, remote_storage))]
|
#[instrument(skip_all)]
|
||||||
pub async fn init_tenant_mgr(
|
pub async fn init_tenant_mgr(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
|
broker_client: storage_broker::BrokerClientChannel,
|
||||||
remote_storage: Option<GenericRemoteStorage>,
|
remote_storage: Option<GenericRemoteStorage>,
|
||||||
|
init_done: (completion::Completion, completion::Barrier),
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// Scan local filesystem for attached tenants
|
// Scan local filesystem for attached tenants
|
||||||
let tenants_dir = conf.tenants_path();
|
let tenants_dir = conf.tenants_path();
|
||||||
@@ -116,7 +122,9 @@ pub async fn init_tenant_mgr(
|
|||||||
match schedule_local_tenant_processing(
|
match schedule_local_tenant_processing(
|
||||||
conf,
|
conf,
|
||||||
&tenant_dir_path,
|
&tenant_dir_path,
|
||||||
|
broker_client.clone(),
|
||||||
remote_storage.clone(),
|
remote_storage.clone(),
|
||||||
|
Some(init_done.clone()),
|
||||||
&ctx,
|
&ctx,
|
||||||
) {
|
) {
|
||||||
Ok(tenant) => {
|
Ok(tenant) => {
|
||||||
@@ -150,7 +158,9 @@ pub async fn init_tenant_mgr(
|
|||||||
pub fn schedule_local_tenant_processing(
|
pub fn schedule_local_tenant_processing(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
tenant_path: &Path,
|
tenant_path: &Path,
|
||||||
|
broker_client: storage_broker::BrokerClientChannel,
|
||||||
remote_storage: Option<GenericRemoteStorage>,
|
remote_storage: Option<GenericRemoteStorage>,
|
||||||
|
init_done: Option<(completion::Completion, completion::Barrier)>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<Arc<Tenant>> {
|
) -> anyhow::Result<Arc<Tenant>> {
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
@@ -186,7 +196,7 @@ pub fn schedule_local_tenant_processing(
|
|||||||
let tenant = if conf.tenant_attaching_mark_file_path(&tenant_id).exists() {
|
let tenant = if conf.tenant_attaching_mark_file_path(&tenant_id).exists() {
|
||||||
info!("tenant {tenant_id} has attaching mark file, resuming its attach operation");
|
info!("tenant {tenant_id} has attaching mark file, resuming its attach operation");
|
||||||
if let Some(remote_storage) = remote_storage {
|
if let Some(remote_storage) = remote_storage {
|
||||||
match Tenant::spawn_attach(conf, tenant_id, remote_storage, ctx) {
|
match Tenant::spawn_attach(conf, tenant_id, broker_client, remote_storage, ctx) {
|
||||||
Ok(tenant) => tenant,
|
Ok(tenant) => tenant,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to spawn_attach tenant {tenant_id}, reason: {e:#}");
|
error!("Failed to spawn_attach tenant {tenant_id}, reason: {e:#}");
|
||||||
@@ -204,7 +214,14 @@ pub fn schedule_local_tenant_processing(
|
|||||||
} else {
|
} else {
|
||||||
info!("tenant {tenant_id} is assumed to be loadable, starting load operation");
|
info!("tenant {tenant_id} is assumed to be loadable, starting load operation");
|
||||||
// Start loading the tenant into memory. It will initially be in Loading state.
|
// Start loading the tenant into memory. It will initially be in Loading state.
|
||||||
Tenant::spawn_load(conf, tenant_id, remote_storage, ctx)
|
Tenant::spawn_load(
|
||||||
|
conf,
|
||||||
|
tenant_id,
|
||||||
|
broker_client,
|
||||||
|
remote_storage,
|
||||||
|
init_done,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
Ok(tenant)
|
Ok(tenant)
|
||||||
}
|
}
|
||||||
@@ -219,6 +236,7 @@ pub fn schedule_local_tenant_processing(
|
|||||||
/// That could be easily misinterpreted by control plane, the consumer of the
|
/// That could be easily misinterpreted by control plane, the consumer of the
|
||||||
/// management API. For example, it could attach the tenant on a different pageserver.
|
/// management API. For example, it could attach the tenant on a different pageserver.
|
||||||
/// We would then be in split-brain once this pageserver restarts.
|
/// We would then be in split-brain once this pageserver restarts.
|
||||||
|
#[instrument]
|
||||||
pub async fn shutdown_all_tenants() {
|
pub async fn shutdown_all_tenants() {
|
||||||
// Prevent new tenants from being created.
|
// Prevent new tenants from being created.
|
||||||
let tenants_to_shut_down = {
|
let tenants_to_shut_down = {
|
||||||
@@ -241,15 +259,65 @@ pub async fn shutdown_all_tenants() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set tenant (and its timlines) to Stoppping state.
|
||||||
|
//
|
||||||
|
// Since we can only transition into Stopping state after activation is complete,
|
||||||
|
// run it in a JoinSet so all tenants have a chance to stop before we get SIGKILLed.
|
||||||
|
//
|
||||||
|
// Transitioning tenants to Stopping state has a couple of non-obvious side effects:
|
||||||
|
// 1. Lock out any new requests to the tenants.
|
||||||
|
// 2. Signal cancellation to WAL receivers (we wait on it below).
|
||||||
|
// 3. Signal cancellation for other tenant background loops.
|
||||||
|
// 4. ???
|
||||||
|
//
|
||||||
|
// The waiting for the cancellation is not done uniformly.
|
||||||
|
// We certainly wait for WAL receivers to shut down.
|
||||||
|
// That is necessary so that no new data comes in before the freeze_and_flush.
|
||||||
|
// But the tenant background loops are joined-on in our caller.
|
||||||
|
// It's mesed up.
|
||||||
|
let mut join_set = JoinSet::new();
|
||||||
let mut tenants_to_freeze_and_flush = Vec::with_capacity(tenants_to_shut_down.len());
|
let mut tenants_to_freeze_and_flush = Vec::with_capacity(tenants_to_shut_down.len());
|
||||||
for (_, tenant) in tenants_to_shut_down {
|
for (tenant_id, tenant) in tenants_to_shut_down {
|
||||||
if tenant.is_active() {
|
join_set.spawn(
|
||||||
// updates tenant state, forbidding new GC and compaction iterations from starting
|
async move {
|
||||||
tenant.set_stopping();
|
match tenant.set_stopping().await {
|
||||||
tenants_to_freeze_and_flush.push(tenant);
|
Ok(()) => debug!("tenant successfully stopped"),
|
||||||
|
Err(SetStoppingError::Broken) => {
|
||||||
|
info!("tenant is broken, so stopping failed, freeze_and_flush is likely going to make noise as well");
|
||||||
|
},
|
||||||
|
Err(SetStoppingError::AlreadyStopping) => {
|
||||||
|
// our task_mgr::shutdown_tasks are going to coalesce on that just fine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tenant
|
||||||
|
}
|
||||||
|
.instrument(info_span!("set_stopping", %tenant_id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut panicked = 0;
|
||||||
|
|
||||||
|
while let Some(res) = join_set.join_next().await {
|
||||||
|
match res {
|
||||||
|
Err(join_error) if join_error.is_cancelled() => {
|
||||||
|
unreachable!("we are not cancelling any of the futures");
|
||||||
|
}
|
||||||
|
Err(join_error) if join_error.is_panic() => {
|
||||||
|
// cannot really do anything, as this panic is likely a bug
|
||||||
|
panicked += 1;
|
||||||
|
}
|
||||||
|
Err(join_error) => {
|
||||||
|
warn!("unknown kind of JoinError: {join_error}");
|
||||||
|
}
|
||||||
|
Ok(tenant) => tenants_to_freeze_and_flush.push(tenant),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if panicked > 0 {
|
||||||
|
warn!(panicked, "observed panicks while stopping tenants");
|
||||||
|
}
|
||||||
|
|
||||||
// Shut down all existing walreceiver connections and stop accepting the new ones.
|
// Shut down all existing walreceiver connections and stop accepting the new ones.
|
||||||
task_mgr::shutdown_tasks(Some(TaskKind::WalReceiverManager), None, None).await;
|
task_mgr::shutdown_tasks(Some(TaskKind::WalReceiverManager), None, None).await;
|
||||||
|
|
||||||
@@ -261,12 +329,30 @@ pub async fn shutdown_all_tenants() {
|
|||||||
// should be no more activity in any of the repositories.
|
// should be no more activity in any of the repositories.
|
||||||
//
|
//
|
||||||
// On error, log it but continue with the shutdown for other tenants.
|
// On error, log it but continue with the shutdown for other tenants.
|
||||||
|
|
||||||
|
let mut join_set = tokio::task::JoinSet::new();
|
||||||
|
|
||||||
for tenant in tenants_to_freeze_and_flush {
|
for tenant in tenants_to_freeze_and_flush {
|
||||||
let tenant_id = tenant.tenant_id();
|
let tenant_id = tenant.tenant_id();
|
||||||
debug!("shutdown tenant {tenant_id}");
|
|
||||||
|
|
||||||
if let Err(err) = tenant.freeze_and_flush().await {
|
join_set.spawn(
|
||||||
error!("Could not checkpoint tenant {tenant_id} during shutdown: {err:?}");
|
async move {
|
||||||
|
if let Err(err) = tenant.freeze_and_flush().await {
|
||||||
|
warn!("Could not checkpoint tenant during shutdown: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.instrument(info_span!("freeze_and_flush", %tenant_id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(next) = join_set.join_next().await {
|
||||||
|
match next {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(join_error) if join_error.is_cancelled() => {
|
||||||
|
unreachable!("no cancelling")
|
||||||
|
}
|
||||||
|
Err(join_error) if join_error.is_panic() => { /* reported already */ }
|
||||||
|
Err(join_error) => warn!("unknown kind of JoinError: {join_error}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,6 +361,7 @@ pub async fn create_tenant(
|
|||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
tenant_conf: TenantConfOpt,
|
tenant_conf: TenantConfOpt,
|
||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
|
broker_client: storage_broker::BrokerClientChannel,
|
||||||
remote_storage: Option<GenericRemoteStorage>,
|
remote_storage: Option<GenericRemoteStorage>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<Arc<Tenant>, TenantMapInsertError> {
|
) -> Result<Arc<Tenant>, TenantMapInsertError> {
|
||||||
@@ -287,7 +374,7 @@ pub async fn create_tenant(
|
|||||||
// See https://github.com/neondatabase/neon/issues/4233
|
// See https://github.com/neondatabase/neon/issues/4233
|
||||||
|
|
||||||
let created_tenant =
|
let created_tenant =
|
||||||
schedule_local_tenant_processing(conf, &tenant_directory, remote_storage, ctx)?;
|
schedule_local_tenant_processing(conf, &tenant_directory, broker_client, remote_storage, None, ctx)?;
|
||||||
// TODO: tenant object & its background loops remain, untracked in tenant map, if we fail here.
|
// TODO: tenant object & its background loops remain, untracked in tenant map, if we fail here.
|
||||||
// See https://github.com/neondatabase/neon/issues/4233
|
// See https://github.com/neondatabase/neon/issues/4233
|
||||||
|
|
||||||
@@ -300,11 +387,19 @@ pub async fn create_tenant(
|
|||||||
}).await
|
}).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum SetNewTenantConfigError {
|
||||||
|
#[error(transparent)]
|
||||||
|
GetTenant(#[from] GetTenantError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Persist(anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn set_new_tenant_config(
|
pub async fn set_new_tenant_config(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
new_tenant_conf: TenantConfOpt,
|
new_tenant_conf: TenantConfOpt,
|
||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
) -> Result<(), TenantStateError> {
|
) -> Result<(), SetNewTenantConfigError> {
|
||||||
info!("configuring tenant {tenant_id}");
|
info!("configuring tenant {tenant_id}");
|
||||||
let tenant = get_tenant(tenant_id, true).await?;
|
let tenant = get_tenant(tenant_id, true).await?;
|
||||||
|
|
||||||
@@ -314,23 +409,32 @@ pub async fn set_new_tenant_config(
|
|||||||
&tenant_config_path,
|
&tenant_config_path,
|
||||||
new_tenant_conf,
|
new_tenant_conf,
|
||||||
false,
|
false,
|
||||||
)?;
|
)
|
||||||
|
.map_err(SetNewTenantConfigError::Persist)?;
|
||||||
tenant.set_new_tenant_config(new_tenant_conf);
|
tenant.set_new_tenant_config(new_tenant_conf);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum GetTenantError {
|
||||||
|
#[error("Tenant {0} not found")]
|
||||||
|
NotFound(TenantId),
|
||||||
|
#[error("Tenant {0} is not active")]
|
||||||
|
NotActive(TenantId),
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets the tenant from the in-memory data, erroring if it's absent or is not fitting to the query.
|
/// Gets the tenant from the in-memory data, erroring if it's absent or is not fitting to the query.
|
||||||
/// `active_only = true` allows to query only tenants that are ready for operations, erroring on other kinds of tenants.
|
/// `active_only = true` allows to query only tenants that are ready for operations, erroring on other kinds of tenants.
|
||||||
pub async fn get_tenant(
|
pub async fn get_tenant(
|
||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
active_only: bool,
|
active_only: bool,
|
||||||
) -> Result<Arc<Tenant>, TenantStateError> {
|
) -> Result<Arc<Tenant>, GetTenantError> {
|
||||||
let m = TENANTS.read().await;
|
let m = TENANTS.read().await;
|
||||||
let tenant = m
|
let tenant = m
|
||||||
.get(&tenant_id)
|
.get(&tenant_id)
|
||||||
.ok_or(TenantStateError::NotFound(tenant_id))?;
|
.ok_or(GetTenantError::NotFound(tenant_id))?;
|
||||||
if active_only && !tenant.is_active() {
|
if active_only && !tenant.is_active() {
|
||||||
Err(TenantStateError::NotActive(tenant_id))
|
Err(GetTenantError::NotActive(tenant_id))
|
||||||
} else {
|
} else {
|
||||||
Ok(Arc::clone(tenant))
|
Ok(Arc::clone(tenant))
|
||||||
}
|
}
|
||||||
@@ -339,7 +443,7 @@ pub async fn get_tenant(
|
|||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum DeleteTimelineError {
|
pub enum DeleteTimelineError {
|
||||||
#[error("Tenant {0}")]
|
#[error("Tenant {0}")]
|
||||||
Tenant(#[from] TenantStateError),
|
Tenant(#[from] GetTenantError),
|
||||||
|
|
||||||
#[error("Timeline {0}")]
|
#[error("Timeline {0}")]
|
||||||
Timeline(#[from] crate::tenant::DeleteTimelineError),
|
Timeline(#[from] crate::tenant::DeleteTimelineError),
|
||||||
@@ -404,6 +508,7 @@ pub async fn detach_tenant(
|
|||||||
pub async fn load_tenant(
|
pub async fn load_tenant(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
|
broker_client: storage_broker::BrokerClientChannel,
|
||||||
remote_storage: Option<GenericRemoteStorage>,
|
remote_storage: Option<GenericRemoteStorage>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<(), TenantMapInsertError> {
|
) -> Result<(), TenantMapInsertError> {
|
||||||
@@ -415,7 +520,7 @@ pub async fn load_tenant(
|
|||||||
.with_context(|| format!("Failed to remove tenant ignore mark {tenant_ignore_mark:?} during tenant loading"))?;
|
.with_context(|| format!("Failed to remove tenant ignore mark {tenant_ignore_mark:?} during tenant loading"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_tenant = schedule_local_tenant_processing(conf, &tenant_path, remote_storage, ctx)
|
let new_tenant = schedule_local_tenant_processing(conf, &tenant_path, broker_client, remote_storage, None, ctx)
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!("Failed to schedule tenant processing in path {tenant_path:?}")
|
format!("Failed to schedule tenant processing in path {tenant_path:?}")
|
||||||
})?;
|
})?;
|
||||||
@@ -472,6 +577,7 @@ pub async fn attach_tenant(
|
|||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
tenant_conf: TenantConfOpt,
|
tenant_conf: TenantConfOpt,
|
||||||
|
broker_client: storage_broker::BrokerClientChannel,
|
||||||
remote_storage: GenericRemoteStorage,
|
remote_storage: GenericRemoteStorage,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<(), TenantMapInsertError> {
|
) -> Result<(), TenantMapInsertError> {
|
||||||
@@ -487,7 +593,7 @@ pub async fn attach_tenant(
|
|||||||
.context("check for attach marker file existence")?;
|
.context("check for attach marker file existence")?;
|
||||||
anyhow::ensure!(marker_file_exists, "create_tenant_files should have created the attach marker file");
|
anyhow::ensure!(marker_file_exists, "create_tenant_files should have created the attach marker file");
|
||||||
|
|
||||||
let attached_tenant = schedule_local_tenant_processing(conf, &tenant_dir, Some(remote_storage), ctx)?;
|
let attached_tenant = schedule_local_tenant_processing(conf, &tenant_dir, broker_client, Some(remote_storage), None, ctx)?;
|
||||||
// TODO: tenant object & its background loops remain, untracked in tenant map, if we fail here.
|
// TODO: tenant object & its background loops remain, untracked in tenant map, if we fail here.
|
||||||
// See https://github.com/neondatabase/neon/issues/4233
|
// See https://github.com/neondatabase/neon/issues/4233
|
||||||
|
|
||||||
@@ -566,13 +672,23 @@ where
|
|||||||
{
|
{
|
||||||
let tenants_accessor = TENANTS.write().await;
|
let tenants_accessor = TENANTS.write().await;
|
||||||
match tenants_accessor.get(&tenant_id) {
|
match tenants_accessor.get(&tenant_id) {
|
||||||
Some(tenant) => match tenant.current_state() {
|
Some(tenant) => {
|
||||||
TenantState::Attaching
|
let tenant = Arc::clone(tenant);
|
||||||
| TenantState::Loading
|
// don't hold TENANTS lock while set_stopping waits for activation to finish
|
||||||
| TenantState::Broken { .. }
|
drop(tenants_accessor);
|
||||||
| TenantState::Active => tenant.set_stopping(),
|
match tenant.set_stopping().await {
|
||||||
TenantState::Stopping => return Err(TenantStateError::IsStopping(tenant_id)),
|
Ok(()) => {
|
||||||
},
|
// we won, continue stopping procedure
|
||||||
|
}
|
||||||
|
Err(SetStoppingError::Broken) => {
|
||||||
|
// continue the procedure, let's hope the closure can deal with broken tenants
|
||||||
|
}
|
||||||
|
Err(SetStoppingError::AlreadyStopping) => {
|
||||||
|
// the tenant is already stopping or broken, don't do anything
|
||||||
|
return Err(TenantStateError::IsStopping(tenant_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
None => return Err(TenantStateError::NotFound(tenant_id)),
|
None => return Err(TenantStateError::NotFound(tenant_id)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -597,7 +713,7 @@ where
|
|||||||
let tenants_accessor = TENANTS.read().await;
|
let tenants_accessor = TENANTS.read().await;
|
||||||
match tenants_accessor.get(&tenant_id) {
|
match tenants_accessor.get(&tenant_id) {
|
||||||
Some(tenant) => {
|
Some(tenant) => {
|
||||||
tenant.set_broken(e.to_string());
|
tenant.set_broken(e.to_string()).await;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
warn!("Tenant {tenant_id} got removed from memory");
|
warn!("Tenant {tenant_id} got removed from memory");
|
||||||
@@ -663,7 +779,6 @@ pub async fn immediate_gc(
|
|||||||
Ok(wait_task_done)
|
Ok(wait_task_done)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "testing")]
|
|
||||||
pub async fn immediate_compact(
|
pub async fn immediate_compact(
|
||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
timeline_id: TimelineId,
|
timeline_id: TimelineId,
|
||||||
|
|||||||
@@ -19,14 +19,8 @@ fn parallel_worker(paths: &[PathBuf], next_path_idx: &AtomicUsize) -> io::Result
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn par_fsync(paths: &[PathBuf]) -> io::Result<()> {
|
fn fsync_in_thread_pool(paths: &[PathBuf]) -> io::Result<()> {
|
||||||
const PARALLEL_PATH_THRESHOLD: usize = 1;
|
// TODO: remove this function in favor of `par_fsync_async` once we asyncify everything.
|
||||||
if paths.len() <= PARALLEL_PATH_THRESHOLD {
|
|
||||||
for path in paths {
|
|
||||||
fsync_path(path)?;
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use at most this number of threads.
|
/// Use at most this number of threads.
|
||||||
/// Increasing this limit will
|
/// Increasing this limit will
|
||||||
@@ -36,11 +30,11 @@ pub fn par_fsync(paths: &[PathBuf]) -> io::Result<()> {
|
|||||||
let num_threads = paths.len().min(MAX_NUM_THREADS);
|
let num_threads = paths.len().min(MAX_NUM_THREADS);
|
||||||
let next_path_idx = AtomicUsize::new(0);
|
let next_path_idx = AtomicUsize::new(0);
|
||||||
|
|
||||||
crossbeam_utils::thread::scope(|s| -> io::Result<()> {
|
std::thread::scope(|s| -> io::Result<()> {
|
||||||
let mut handles = vec![];
|
let mut handles = vec![];
|
||||||
// Spawn `num_threads - 1`, as the current thread is also a worker.
|
// Spawn `num_threads - 1`, as the current thread is also a worker.
|
||||||
for _ in 1..num_threads {
|
for _ in 1..num_threads {
|
||||||
handles.push(s.spawn(|_| parallel_worker(paths, &next_path_idx)));
|
handles.push(s.spawn(|| parallel_worker(paths, &next_path_idx)));
|
||||||
}
|
}
|
||||||
|
|
||||||
parallel_worker(paths, &next_path_idx)?;
|
parallel_worker(paths, &next_path_idx)?;
|
||||||
@@ -51,5 +45,41 @@ pub fn par_fsync(paths: &[PathBuf]) -> io::Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.unwrap()
|
}
|
||||||
|
|
||||||
|
/// Parallel fsync all files. Can be used in non-async context as it is using rayon thread pool.
|
||||||
|
pub fn par_fsync(paths: &[PathBuf]) -> io::Result<()> {
|
||||||
|
if paths.len() == 1 {
|
||||||
|
fsync_path(&paths[0])?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fsync_in_thread_pool(paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parallel fsync asynchronously. If number of files are less than PARALLEL_PATH_THRESHOLD, fsync is done in the current
|
||||||
|
/// execution thread. Otherwise, we will spawn_blocking and run it in tokio.
|
||||||
|
pub async fn par_fsync_async(paths: &[PathBuf]) -> io::Result<()> {
|
||||||
|
const MAX_CONCURRENT_FSYNC: usize = 64;
|
||||||
|
let mut next = paths.iter().peekable();
|
||||||
|
let mut js = tokio::task::JoinSet::new();
|
||||||
|
loop {
|
||||||
|
while js.len() < MAX_CONCURRENT_FSYNC && next.peek().is_some() {
|
||||||
|
let next = next.next().expect("just peeked");
|
||||||
|
let next = next.to_owned();
|
||||||
|
js.spawn_blocking(move || fsync_path(&next));
|
||||||
|
}
|
||||||
|
|
||||||
|
// now the joinset has been filled up, wait for next to complete
|
||||||
|
if let Some(res) = js.join_next().await {
|
||||||
|
res??;
|
||||||
|
} else {
|
||||||
|
// last item had already completed
|
||||||
|
assert!(
|
||||||
|
next.peek().is_none(),
|
||||||
|
"joinset emptied, we shouldn't have more work"
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1264,9 +1264,7 @@ mod tests {
|
|||||||
let harness = TenantHarness::create(test_name)?;
|
let harness = TenantHarness::create(test_name)?;
|
||||||
let (tenant, ctx) = runtime.block_on(harness.load());
|
let (tenant, ctx) = runtime.block_on(harness.load());
|
||||||
// create an empty timeline directory
|
// create an empty timeline directory
|
||||||
let timeline =
|
let _ = tenant.create_test_timeline(TIMELINE_ID, Lsn(0), DEFAULT_PG_VERSION, &ctx)?;
|
||||||
tenant.create_empty_timeline(TIMELINE_ID, Lsn(0), DEFAULT_PG_VERSION, &ctx)?;
|
|
||||||
let _ = timeline.initialize(&ctx).unwrap();
|
|
||||||
|
|
||||||
let remote_fs_dir = harness.conf.workdir.join("remote_fs");
|
let remote_fs_dir = harness.conf.workdir.join("remote_fs");
|
||||||
std::fs::create_dir_all(remote_fs_dir)?;
|
std::fs::create_dir_all(remote_fs_dir)?;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pub mod delta_layer;
|
|||||||
mod filename;
|
mod filename;
|
||||||
mod image_layer;
|
mod image_layer;
|
||||||
mod inmemory_layer;
|
mod inmemory_layer;
|
||||||
|
mod layer_desc;
|
||||||
mod remote_layer;
|
mod remote_layer;
|
||||||
|
|
||||||
use crate::config::PageServerConf;
|
use crate::config::PageServerConf;
|
||||||
@@ -37,6 +38,7 @@ pub use delta_layer::{DeltaLayer, DeltaLayerWriter};
|
|||||||
pub use filename::{DeltaFileName, ImageFileName, LayerFileName};
|
pub use filename::{DeltaFileName, ImageFileName, LayerFileName};
|
||||||
pub use image_layer::{ImageLayer, ImageLayerWriter};
|
pub use image_layer::{ImageLayer, ImageLayerWriter};
|
||||||
pub use inmemory_layer::InMemoryLayer;
|
pub use inmemory_layer::InMemoryLayer;
|
||||||
|
pub use layer_desc::PersistentLayerDesc;
|
||||||
pub use remote_layer::RemoteLayer;
|
pub use remote_layer::RemoteLayer;
|
||||||
|
|
||||||
use super::layer_map::BatchedUpdates;
|
use super::layer_map::BatchedUpdates;
|
||||||
@@ -406,14 +408,23 @@ pub type LayerKeyIter<'i> = Box<dyn Iterator<Item = (Key, Lsn, u64)> + 'i>;
|
|||||||
/// An image layer is a snapshot of all the data in a key-range, at a single
|
/// An image layer is a snapshot of all the data in a key-range, at a single
|
||||||
/// LSN.
|
/// LSN.
|
||||||
pub trait PersistentLayer: Layer {
|
pub trait PersistentLayer: Layer {
|
||||||
fn get_tenant_id(&self) -> TenantId;
|
/// Get the layer descriptor.
|
||||||
|
fn layer_desc(&self) -> &PersistentLayerDesc;
|
||||||
|
|
||||||
|
fn get_tenant_id(&self) -> TenantId {
|
||||||
|
self.layer_desc().tenant_id
|
||||||
|
}
|
||||||
|
|
||||||
/// Identify the timeline this layer belongs to
|
/// Identify the timeline this layer belongs to
|
||||||
fn get_timeline_id(&self) -> TimelineId;
|
fn get_timeline_id(&self) -> TimelineId {
|
||||||
|
self.layer_desc().timeline_id
|
||||||
|
}
|
||||||
|
|
||||||
/// File name used for this layer, both in the pageserver's local filesystem
|
/// File name used for this layer, both in the pageserver's local filesystem
|
||||||
/// state as well as in the remote storage.
|
/// state as well as in the remote storage.
|
||||||
fn filename(&self) -> LayerFileName;
|
fn filename(&self) -> LayerFileName {
|
||||||
|
self.layer_desc().filename()
|
||||||
|
}
|
||||||
|
|
||||||
// Path to the layer file in the local filesystem.
|
// Path to the layer file in the local filesystem.
|
||||||
// `None` for `RemoteLayer`.
|
// `None` for `RemoteLayer`.
|
||||||
@@ -542,7 +553,7 @@ impl From<LayerFileName> for LayerDescriptor {
|
|||||||
///
|
///
|
||||||
/// This is used by DeltaLayer and ImageLayer. Normally, this holds a reference to the
|
/// This is used by DeltaLayer and ImageLayer. Normally, this holds a reference to the
|
||||||
/// global config, and paths to layer files are constructed using the tenant/timeline
|
/// global config, and paths to layer files are constructed using the tenant/timeline
|
||||||
/// path from the config. But in the 'pageserver_binutils' binary, we need to construct a Layer
|
/// path from the config. But in the 'pagectl' binary, we need to construct a Layer
|
||||||
/// struct for a file on disk, without having a page server running, so that we have no
|
/// struct for a file on disk, without having a page server running, so that we have no
|
||||||
/// config. In that case, we use the Path variant to hold the full path to the file on
|
/// config. In that case, we use the Path variant to hold the full path to the file on
|
||||||
/// disk.
|
/// disk.
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ use utils::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
DeltaFileName, Layer, LayerAccessStats, LayerAccessStatsReset, LayerFileName, LayerIter,
|
DeltaFileName, Layer, LayerAccessStats, LayerAccessStatsReset, LayerIter, LayerKeyIter,
|
||||||
LayerKeyIter, PathOrConf,
|
PathOrConf, PersistentLayerDesc,
|
||||||
};
|
};
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -89,10 +89,10 @@ impl From<&DeltaLayer> for Summary {
|
|||||||
magic: DELTA_FILE_MAGIC,
|
magic: DELTA_FILE_MAGIC,
|
||||||
format_version: STORAGE_FORMAT_VERSION,
|
format_version: STORAGE_FORMAT_VERSION,
|
||||||
|
|
||||||
tenant_id: layer.tenant_id,
|
tenant_id: layer.desc.tenant_id,
|
||||||
timeline_id: layer.timeline_id,
|
timeline_id: layer.desc.timeline_id,
|
||||||
key_range: layer.key_range.clone(),
|
key_range: layer.desc.key_range.clone(),
|
||||||
lsn_range: layer.lsn_range.clone(),
|
lsn_range: layer.desc.lsn_range.clone(),
|
||||||
|
|
||||||
index_start_blk: 0,
|
index_start_blk: 0,
|
||||||
index_root_blk: 0,
|
index_root_blk: 0,
|
||||||
@@ -110,7 +110,7 @@ const WILL_INIT: u64 = 1;
|
|||||||
/// reading/deserializing records themselves.
|
/// reading/deserializing records themselves.
|
||||||
///
|
///
|
||||||
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
|
||||||
struct BlobRef(u64);
|
pub struct BlobRef(pub u64);
|
||||||
|
|
||||||
impl BlobRef {
|
impl BlobRef {
|
||||||
pub fn will_init(&self) -> bool {
|
pub fn will_init(&self) -> bool {
|
||||||
@@ -180,10 +180,7 @@ impl DeltaKey {
|
|||||||
pub struct DeltaLayer {
|
pub struct DeltaLayer {
|
||||||
path_or_conf: PathOrConf,
|
path_or_conf: PathOrConf,
|
||||||
|
|
||||||
pub tenant_id: TenantId,
|
pub desc: PersistentLayerDesc,
|
||||||
pub timeline_id: TimelineId,
|
|
||||||
pub key_range: Range<Key>,
|
|
||||||
pub lsn_range: Range<Lsn>,
|
|
||||||
|
|
||||||
pub file_size: u64,
|
pub file_size: u64,
|
||||||
|
|
||||||
@@ -197,8 +194,8 @@ impl std::fmt::Debug for DeltaLayer {
|
|||||||
use super::RangeDisplayDebug;
|
use super::RangeDisplayDebug;
|
||||||
|
|
||||||
f.debug_struct("DeltaLayer")
|
f.debug_struct("DeltaLayer")
|
||||||
.field("key_range", &RangeDisplayDebug(&self.key_range))
|
.field("key_range", &RangeDisplayDebug(&self.desc.key_range))
|
||||||
.field("lsn_range", &self.lsn_range)
|
.field("lsn_range", &self.desc.lsn_range)
|
||||||
.field("file_size", &self.file_size)
|
.field("file_size", &self.file_size)
|
||||||
.field("inner", &self.inner)
|
.field("inner", &self.inner)
|
||||||
.finish()
|
.finish()
|
||||||
@@ -228,30 +225,16 @@ impl std::fmt::Debug for DeltaLayerInner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Layer for DeltaLayer {
|
impl Layer for DeltaLayer {
|
||||||
fn get_key_range(&self) -> Range<Key> {
|
|
||||||
self.key_range.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_lsn_range(&self) -> Range<Lsn> {
|
|
||||||
self.lsn_range.clone()
|
|
||||||
}
|
|
||||||
fn is_incremental(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn short_id(&self) -> String {
|
|
||||||
self.filename().file_name()
|
|
||||||
}
|
|
||||||
/// debugging function to print out the contents of the layer
|
/// debugging function to print out the contents of the layer
|
||||||
fn dump(&self, verbose: bool, ctx: &RequestContext) -> Result<()> {
|
fn dump(&self, verbose: bool, ctx: &RequestContext) -> Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"----- delta layer for ten {} tli {} keys {}-{} lsn {}-{} ----",
|
"----- delta layer for ten {} tli {} keys {}-{} lsn {}-{} ----",
|
||||||
self.tenant_id,
|
self.desc.tenant_id,
|
||||||
self.timeline_id,
|
self.desc.timeline_id,
|
||||||
self.key_range.start,
|
self.desc.key_range.start,
|
||||||
self.key_range.end,
|
self.desc.key_range.end,
|
||||||
self.lsn_range.start,
|
self.desc.lsn_range.start,
|
||||||
self.lsn_range.end
|
self.desc.lsn_range.end
|
||||||
);
|
);
|
||||||
|
|
||||||
if !verbose {
|
if !verbose {
|
||||||
@@ -324,10 +307,10 @@ impl Layer for DeltaLayer {
|
|||||||
reconstruct_state: &mut ValueReconstructState,
|
reconstruct_state: &mut ValueReconstructState,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<ValueReconstructResult> {
|
) -> anyhow::Result<ValueReconstructResult> {
|
||||||
ensure!(lsn_range.start >= self.lsn_range.start);
|
ensure!(lsn_range.start >= self.desc.lsn_range.start);
|
||||||
let mut need_image = true;
|
let mut need_image = true;
|
||||||
|
|
||||||
ensure!(self.key_range.contains(&key));
|
ensure!(self.desc.key_range.contains(&key));
|
||||||
|
|
||||||
{
|
{
|
||||||
// Open the file and lock the metadata in memory
|
// Open the file and lock the metadata in memory
|
||||||
@@ -402,19 +385,31 @@ impl Layer for DeltaLayer {
|
|||||||
Ok(ValueReconstructResult::Complete)
|
Ok(ValueReconstructResult::Complete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn get_key_range(&self) -> Range<Key> {
|
||||||
|
self.layer_desc().key_range.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn get_lsn_range(&self) -> Range<Lsn> {
|
||||||
|
self.layer_desc().lsn_range.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn is_incremental(&self) -> bool {
|
||||||
|
self.layer_desc().is_incremental
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn short_id(&self) -> String {
|
||||||
|
self.layer_desc().short_id()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PersistentLayer for DeltaLayer {
|
impl PersistentLayer for DeltaLayer {
|
||||||
fn get_tenant_id(&self) -> TenantId {
|
fn layer_desc(&self) -> &PersistentLayerDesc {
|
||||||
self.tenant_id
|
&self.desc
|
||||||
}
|
|
||||||
|
|
||||||
fn get_timeline_id(&self) -> TimelineId {
|
|
||||||
self.timeline_id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn filename(&self) -> LayerFileName {
|
|
||||||
self.layer_name().into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_path(&self) -> Option<PathBuf> {
|
fn local_path(&self) -> Option<PathBuf> {
|
||||||
@@ -602,10 +597,12 @@ impl DeltaLayer {
|
|||||||
) -> DeltaLayer {
|
) -> DeltaLayer {
|
||||||
DeltaLayer {
|
DeltaLayer {
|
||||||
path_or_conf: PathOrConf::Conf(conf),
|
path_or_conf: PathOrConf::Conf(conf),
|
||||||
timeline_id,
|
desc: PersistentLayerDesc::new_delta(
|
||||||
tenant_id,
|
tenant_id,
|
||||||
key_range: filename.key_range.clone(),
|
timeline_id,
|
||||||
lsn_range: filename.lsn_range.clone(),
|
filename.key_range.clone(),
|
||||||
|
filename.lsn_range.clone(),
|
||||||
|
),
|
||||||
file_size,
|
file_size,
|
||||||
access_stats,
|
access_stats,
|
||||||
inner: RwLock::new(DeltaLayerInner {
|
inner: RwLock::new(DeltaLayerInner {
|
||||||
@@ -619,7 +616,7 @@ impl DeltaLayer {
|
|||||||
|
|
||||||
/// Create a DeltaLayer struct representing an existing file on disk.
|
/// Create a DeltaLayer struct representing an existing file on disk.
|
||||||
///
|
///
|
||||||
/// This variant is only used for debugging purposes, by the 'pageserver_binutils' binary.
|
/// This variant is only used for debugging purposes, by the 'pagectl' binary.
|
||||||
pub fn new_for_path(path: &Path, file: File) -> Result<Self> {
|
pub fn new_for_path(path: &Path, file: File) -> Result<Self> {
|
||||||
let mut summary_buf = Vec::new();
|
let mut summary_buf = Vec::new();
|
||||||
summary_buf.resize(PAGE_SZ, 0);
|
summary_buf.resize(PAGE_SZ, 0);
|
||||||
@@ -632,10 +629,12 @@ impl DeltaLayer {
|
|||||||
|
|
||||||
Ok(DeltaLayer {
|
Ok(DeltaLayer {
|
||||||
path_or_conf: PathOrConf::Path(path.to_path_buf()),
|
path_or_conf: PathOrConf::Path(path.to_path_buf()),
|
||||||
timeline_id: summary.timeline_id,
|
desc: PersistentLayerDesc::new_delta(
|
||||||
tenant_id: summary.tenant_id,
|
summary.tenant_id,
|
||||||
key_range: summary.key_range,
|
summary.timeline_id,
|
||||||
lsn_range: summary.lsn_range,
|
summary.key_range,
|
||||||
|
summary.lsn_range,
|
||||||
|
),
|
||||||
file_size: metadata.len(),
|
file_size: metadata.len(),
|
||||||
access_stats: LayerAccessStats::empty_will_record_residence_event_later(),
|
access_stats: LayerAccessStats::empty_will_record_residence_event_later(),
|
||||||
inner: RwLock::new(DeltaLayerInner {
|
inner: RwLock::new(DeltaLayerInner {
|
||||||
@@ -648,18 +647,14 @@ impl DeltaLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn layer_name(&self) -> DeltaFileName {
|
fn layer_name(&self) -> DeltaFileName {
|
||||||
DeltaFileName {
|
self.desc.delta_file_name()
|
||||||
key_range: self.key_range.clone(),
|
|
||||||
lsn_range: self.lsn_range.clone(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Path to the layer file in pageserver workdir.
|
/// Path to the layer file in pageserver workdir.
|
||||||
pub fn path(&self) -> PathBuf {
|
pub fn path(&self) -> PathBuf {
|
||||||
Self::path_for(
|
Self::path_for(
|
||||||
&self.path_or_conf,
|
&self.path_or_conf,
|
||||||
self.timeline_id,
|
self.desc.timeline_id,
|
||||||
self.tenant_id,
|
self.desc.tenant_id,
|
||||||
&self.layer_name(),
|
&self.layer_name(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -803,10 +798,12 @@ impl DeltaLayerWriterInner {
|
|||||||
// set inner.file here. The first read will have to re-open it.
|
// set inner.file here. The first read will have to re-open it.
|
||||||
let layer = DeltaLayer {
|
let layer = DeltaLayer {
|
||||||
path_or_conf: PathOrConf::Conf(self.conf),
|
path_or_conf: PathOrConf::Conf(self.conf),
|
||||||
tenant_id: self.tenant_id,
|
desc: PersistentLayerDesc::new_delta(
|
||||||
timeline_id: self.timeline_id,
|
self.tenant_id,
|
||||||
key_range: self.key_start..key_end,
|
self.timeline_id,
|
||||||
lsn_range: self.lsn_range.clone(),
|
self.key_start..key_end,
|
||||||
|
self.lsn_range.clone(),
|
||||||
|
),
|
||||||
file_size: metadata.len(),
|
file_size: metadata.len(),
|
||||||
access_stats: LayerAccessStats::empty_will_record_residence_event_later(),
|
access_stats: LayerAccessStats::empty_will_record_residence_event_later(),
|
||||||
inner: RwLock::new(DeltaLayerInner {
|
inner: RwLock::new(DeltaLayerInner {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
use utils::lsn::Lsn;
|
use utils::lsn::Lsn;
|
||||||
|
|
||||||
|
use super::PersistentLayerDesc;
|
||||||
|
|
||||||
// Note: Timeline::load_layer_map() relies on this sort order
|
// Note: Timeline::load_layer_map() relies on this sort order
|
||||||
#[derive(PartialEq, Eq, Clone, Hash)]
|
#[derive(PartialEq, Eq, Clone, Hash)]
|
||||||
pub struct DeltaFileName {
|
pub struct DeltaFileName {
|
||||||
@@ -153,7 +155,7 @@ impl Ord for ImageFileName {
|
|||||||
impl ImageFileName {
|
impl ImageFileName {
|
||||||
pub fn lsn_as_range(&self) -> Range<Lsn> {
|
pub fn lsn_as_range(&self) -> Range<Lsn> {
|
||||||
// Saves from having to copypaste this all over
|
// Saves from having to copypaste this all over
|
||||||
self.lsn..(self.lsn + 1)
|
PersistentLayerDesc::image_layer_lsn_range(self.lsn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ use utils::{
|
|||||||
lsn::Lsn,
|
lsn::Lsn,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::filename::{ImageFileName, LayerFileName};
|
use super::filename::ImageFileName;
|
||||||
use super::{Layer, LayerAccessStatsReset, LayerIter, PathOrConf};
|
use super::{Layer, LayerAccessStatsReset, LayerIter, PathOrConf, PersistentLayerDesc};
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Header stored in the beginning of the file
|
/// Header stored in the beginning of the file
|
||||||
@@ -84,9 +84,9 @@ impl From<&ImageLayer> for Summary {
|
|||||||
Self {
|
Self {
|
||||||
magic: IMAGE_FILE_MAGIC,
|
magic: IMAGE_FILE_MAGIC,
|
||||||
format_version: STORAGE_FORMAT_VERSION,
|
format_version: STORAGE_FORMAT_VERSION,
|
||||||
tenant_id: layer.tenant_id,
|
tenant_id: layer.desc.tenant_id,
|
||||||
timeline_id: layer.timeline_id,
|
timeline_id: layer.desc.timeline_id,
|
||||||
key_range: layer.key_range.clone(),
|
key_range: layer.desc.key_range.clone(),
|
||||||
lsn: layer.lsn,
|
lsn: layer.lsn,
|
||||||
|
|
||||||
index_start_blk: 0,
|
index_start_blk: 0,
|
||||||
@@ -104,14 +104,13 @@ impl From<&ImageLayer> for Summary {
|
|||||||
/// and it needs to be loaded before using it in queries.
|
/// and it needs to be loaded before using it in queries.
|
||||||
pub struct ImageLayer {
|
pub struct ImageLayer {
|
||||||
path_or_conf: PathOrConf,
|
path_or_conf: PathOrConf,
|
||||||
pub tenant_id: TenantId,
|
|
||||||
pub timeline_id: TimelineId,
|
|
||||||
pub key_range: Range<Key>,
|
|
||||||
pub file_size: u64,
|
|
||||||
|
|
||||||
// This entry contains an image of all pages as of this LSN
|
pub desc: PersistentLayerDesc,
|
||||||
|
// This entry contains an image of all pages as of this LSN, should be the same as desc.lsn
|
||||||
pub lsn: Lsn,
|
pub lsn: Lsn,
|
||||||
|
|
||||||
|
pub file_size: u64,
|
||||||
|
|
||||||
access_stats: LayerAccessStats,
|
access_stats: LayerAccessStats,
|
||||||
|
|
||||||
inner: RwLock<ImageLayerInner>,
|
inner: RwLock<ImageLayerInner>,
|
||||||
@@ -122,7 +121,7 @@ impl std::fmt::Debug for ImageLayer {
|
|||||||
use super::RangeDisplayDebug;
|
use super::RangeDisplayDebug;
|
||||||
|
|
||||||
f.debug_struct("ImageLayer")
|
f.debug_struct("ImageLayer")
|
||||||
.field("key_range", &RangeDisplayDebug(&self.key_range))
|
.field("key_range", &RangeDisplayDebug(&self.desc.key_range))
|
||||||
.field("file_size", &self.file_size)
|
.field("file_size", &self.file_size)
|
||||||
.field("lsn", &self.lsn)
|
.field("lsn", &self.lsn)
|
||||||
.field("inner", &self.inner)
|
.field("inner", &self.inner)
|
||||||
@@ -153,27 +152,15 @@ impl std::fmt::Debug for ImageLayerInner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Layer for ImageLayer {
|
impl Layer for ImageLayer {
|
||||||
fn get_key_range(&self) -> Range<Key> {
|
|
||||||
self.key_range.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_lsn_range(&self) -> Range<Lsn> {
|
|
||||||
// End-bound is exclusive
|
|
||||||
self.lsn..(self.lsn + 1)
|
|
||||||
}
|
|
||||||
fn is_incremental(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn short_id(&self) -> String {
|
|
||||||
self.filename().file_name()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// debugging function to print out the contents of the layer
|
/// debugging function to print out the contents of the layer
|
||||||
fn dump(&self, verbose: bool, ctx: &RequestContext) -> Result<()> {
|
fn dump(&self, verbose: bool, ctx: &RequestContext) -> Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"----- image layer for ten {} tli {} key {}-{} at {} ----",
|
"----- image layer for ten {} tli {} key {}-{} at {} ----",
|
||||||
self.tenant_id, self.timeline_id, self.key_range.start, self.key_range.end, self.lsn
|
self.desc.tenant_id,
|
||||||
|
self.desc.timeline_id,
|
||||||
|
self.desc.key_range.start,
|
||||||
|
self.desc.key_range.end,
|
||||||
|
self.lsn
|
||||||
);
|
);
|
||||||
|
|
||||||
if !verbose {
|
if !verbose {
|
||||||
@@ -203,7 +190,7 @@ impl Layer for ImageLayer {
|
|||||||
reconstruct_state: &mut ValueReconstructState,
|
reconstruct_state: &mut ValueReconstructState,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<ValueReconstructResult> {
|
) -> anyhow::Result<ValueReconstructResult> {
|
||||||
assert!(self.key_range.contains(&key));
|
assert!(self.desc.key_range.contains(&key));
|
||||||
assert!(lsn_range.start >= self.lsn);
|
assert!(lsn_range.start >= self.lsn);
|
||||||
assert!(lsn_range.end >= self.lsn);
|
assert!(lsn_range.end >= self.lsn);
|
||||||
|
|
||||||
@@ -230,24 +217,37 @@ impl Layer for ImageLayer {
|
|||||||
Ok(ValueReconstructResult::Missing)
|
Ok(ValueReconstructResult::Missing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn get_key_range(&self) -> Range<Key> {
|
||||||
|
self.layer_desc().key_range.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn get_lsn_range(&self) -> Range<Lsn> {
|
||||||
|
self.layer_desc().lsn_range.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn is_incremental(&self) -> bool {
|
||||||
|
self.layer_desc().is_incremental
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn short_id(&self) -> String {
|
||||||
|
self.layer_desc().short_id()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PersistentLayer for ImageLayer {
|
impl PersistentLayer for ImageLayer {
|
||||||
fn filename(&self) -> LayerFileName {
|
fn layer_desc(&self) -> &PersistentLayerDesc {
|
||||||
self.layer_name().into()
|
&self.desc
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_path(&self) -> Option<PathBuf> {
|
fn local_path(&self) -> Option<PathBuf> {
|
||||||
Some(self.path())
|
Some(self.path())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_tenant_id(&self) -> TenantId {
|
|
||||||
self.tenant_id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_timeline_id(&self) -> TimelineId {
|
|
||||||
self.timeline_id
|
|
||||||
}
|
|
||||||
fn iter(&self, _ctx: &RequestContext) -> Result<LayerIter<'_>> {
|
fn iter(&self, _ctx: &RequestContext) -> Result<LayerIter<'_>> {
|
||||||
unimplemented!();
|
unimplemented!();
|
||||||
}
|
}
|
||||||
@@ -405,9 +405,13 @@ impl ImageLayer {
|
|||||||
) -> ImageLayer {
|
) -> ImageLayer {
|
||||||
ImageLayer {
|
ImageLayer {
|
||||||
path_or_conf: PathOrConf::Conf(conf),
|
path_or_conf: PathOrConf::Conf(conf),
|
||||||
timeline_id,
|
desc: PersistentLayerDesc::new_img(
|
||||||
tenant_id,
|
tenant_id,
|
||||||
key_range: filename.key_range.clone(),
|
timeline_id,
|
||||||
|
filename.key_range.clone(),
|
||||||
|
filename.lsn,
|
||||||
|
false,
|
||||||
|
), // Now we assume image layer ALWAYS covers the full range. This may change in the future.
|
||||||
lsn: filename.lsn,
|
lsn: filename.lsn,
|
||||||
file_size,
|
file_size,
|
||||||
access_stats,
|
access_stats,
|
||||||
@@ -422,7 +426,7 @@ impl ImageLayer {
|
|||||||
|
|
||||||
/// Create an ImageLayer struct representing an existing file on disk.
|
/// Create an ImageLayer struct representing an existing file on disk.
|
||||||
///
|
///
|
||||||
/// This variant is only used for debugging purposes, by the 'pageserver_binutils' binary.
|
/// This variant is only used for debugging purposes, by the 'pagectl' binary.
|
||||||
pub fn new_for_path(path: &Path, file: File) -> Result<ImageLayer> {
|
pub fn new_for_path(path: &Path, file: File) -> Result<ImageLayer> {
|
||||||
let mut summary_buf = Vec::new();
|
let mut summary_buf = Vec::new();
|
||||||
summary_buf.resize(PAGE_SZ, 0);
|
summary_buf.resize(PAGE_SZ, 0);
|
||||||
@@ -433,9 +437,13 @@ impl ImageLayer {
|
|||||||
.context("get file metadata to determine size")?;
|
.context("get file metadata to determine size")?;
|
||||||
Ok(ImageLayer {
|
Ok(ImageLayer {
|
||||||
path_or_conf: PathOrConf::Path(path.to_path_buf()),
|
path_or_conf: PathOrConf::Path(path.to_path_buf()),
|
||||||
timeline_id: summary.timeline_id,
|
desc: PersistentLayerDesc::new_img(
|
||||||
tenant_id: summary.tenant_id,
|
summary.tenant_id,
|
||||||
key_range: summary.key_range,
|
summary.timeline_id,
|
||||||
|
summary.key_range,
|
||||||
|
summary.lsn,
|
||||||
|
false,
|
||||||
|
), // Now we assume image layer ALWAYS covers the full range. This may change in the future.
|
||||||
lsn: summary.lsn,
|
lsn: summary.lsn,
|
||||||
file_size: metadata.len(),
|
file_size: metadata.len(),
|
||||||
access_stats: LayerAccessStats::empty_will_record_residence_event_later(),
|
access_stats: LayerAccessStats::empty_will_record_residence_event_later(),
|
||||||
@@ -449,18 +457,15 @@ impl ImageLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn layer_name(&self) -> ImageFileName {
|
fn layer_name(&self) -> ImageFileName {
|
||||||
ImageFileName {
|
self.desc.image_file_name()
|
||||||
key_range: self.key_range.clone(),
|
|
||||||
lsn: self.lsn,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Path to the layer file in pageserver workdir.
|
/// Path to the layer file in pageserver workdir.
|
||||||
pub fn path(&self) -> PathBuf {
|
pub fn path(&self) -> PathBuf {
|
||||||
Self::path_for(
|
Self::path_for(
|
||||||
&self.path_or_conf,
|
&self.path_or_conf,
|
||||||
self.timeline_id,
|
self.desc.timeline_id,
|
||||||
self.tenant_id,
|
self.desc.tenant_id,
|
||||||
&self.layer_name(),
|
&self.layer_name(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -484,6 +489,7 @@ struct ImageLayerWriterInner {
|
|||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
key_range: Range<Key>,
|
key_range: Range<Key>,
|
||||||
lsn: Lsn,
|
lsn: Lsn,
|
||||||
|
is_incremental: bool,
|
||||||
|
|
||||||
blob_writer: WriteBlobWriter<VirtualFile>,
|
blob_writer: WriteBlobWriter<VirtualFile>,
|
||||||
tree: DiskBtreeBuilder<BlockBuf, KEY_SIZE>,
|
tree: DiskBtreeBuilder<BlockBuf, KEY_SIZE>,
|
||||||
@@ -499,6 +505,7 @@ impl ImageLayerWriterInner {
|
|||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
key_range: &Range<Key>,
|
key_range: &Range<Key>,
|
||||||
lsn: Lsn,
|
lsn: Lsn,
|
||||||
|
is_incremental: bool,
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
// Create the file initially with a temporary filename.
|
// Create the file initially with a temporary filename.
|
||||||
// We'll atomically rename it to the final name when we're done.
|
// We'll atomically rename it to the final name when we're done.
|
||||||
@@ -533,6 +540,7 @@ impl ImageLayerWriterInner {
|
|||||||
lsn,
|
lsn,
|
||||||
tree: tree_builder,
|
tree: tree_builder,
|
||||||
blob_writer,
|
blob_writer,
|
||||||
|
is_incremental,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(writer)
|
Ok(writer)
|
||||||
@@ -570,6 +578,14 @@ impl ImageLayerWriterInner {
|
|||||||
file.write_all(buf.as_ref())?;
|
file.write_all(buf.as_ref())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let desc = PersistentLayerDesc::new_img(
|
||||||
|
self.tenant_id,
|
||||||
|
self.timeline_id,
|
||||||
|
self.key_range.clone(),
|
||||||
|
self.lsn,
|
||||||
|
self.is_incremental, // for now, image layer ALWAYS covers the full range
|
||||||
|
);
|
||||||
|
|
||||||
// Fill in the summary on blk 0
|
// Fill in the summary on blk 0
|
||||||
let summary = Summary {
|
let summary = Summary {
|
||||||
magic: IMAGE_FILE_MAGIC,
|
magic: IMAGE_FILE_MAGIC,
|
||||||
@@ -593,9 +609,7 @@ impl ImageLayerWriterInner {
|
|||||||
// set inner.file here. The first read will have to re-open it.
|
// set inner.file here. The first read will have to re-open it.
|
||||||
let layer = ImageLayer {
|
let layer = ImageLayer {
|
||||||
path_or_conf: PathOrConf::Conf(self.conf),
|
path_or_conf: PathOrConf::Conf(self.conf),
|
||||||
timeline_id: self.timeline_id,
|
desc,
|
||||||
tenant_id: self.tenant_id,
|
|
||||||
key_range: self.key_range.clone(),
|
|
||||||
lsn: self.lsn,
|
lsn: self.lsn,
|
||||||
file_size: metadata.len(),
|
file_size: metadata.len(),
|
||||||
access_stats: LayerAccessStats::empty_will_record_residence_event_later(),
|
access_stats: LayerAccessStats::empty_will_record_residence_event_later(),
|
||||||
@@ -667,6 +681,7 @@ impl ImageLayerWriter {
|
|||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
key_range: &Range<Key>,
|
key_range: &Range<Key>,
|
||||||
lsn: Lsn,
|
lsn: Lsn,
|
||||||
|
is_incremental: bool,
|
||||||
) -> anyhow::Result<ImageLayerWriter> {
|
) -> anyhow::Result<ImageLayerWriter> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
inner: Some(ImageLayerWriterInner::new(
|
inner: Some(ImageLayerWriterInner::new(
|
||||||
@@ -675,6 +690,7 @@ impl ImageLayerWriter {
|
|||||||
tenant_id,
|
tenant_id,
|
||||||
key_range,
|
key_range,
|
||||||
lsn,
|
lsn,
|
||||||
|
is_incremental,
|
||||||
)?),
|
)?),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
use utils::{
|
||||||
|
id::{TenantId, TimelineId},
|
||||||
|
lsn::Lsn,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::repository::Key;
|
||||||
|
|
||||||
|
use super::{DeltaFileName, ImageFileName, LayerFileName};
|
||||||
|
|
||||||
|
/// A unique identifier of a persistent layer. This is different from `LayerDescriptor`, which is only used in the
|
||||||
|
/// benchmarks. This struct contains all necessary information to find the image / delta layer. It also provides
|
||||||
|
/// a unified way to generate layer information like file name.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub struct PersistentLayerDesc {
|
||||||
|
pub tenant_id: TenantId,
|
||||||
|
pub timeline_id: TimelineId,
|
||||||
|
pub key_range: Range<Key>,
|
||||||
|
/// For image layer, this is `[lsn, lsn+1)`.
|
||||||
|
pub lsn_range: Range<Lsn>,
|
||||||
|
/// Whether this is a delta layer.
|
||||||
|
pub is_delta: bool,
|
||||||
|
/// Whether this layer only contains page images for part of the keys in the range. In the current implementation, this should
|
||||||
|
/// always be equal to `is_delta`. If we land the partial image layer PR someday, image layer could also be
|
||||||
|
/// incremental.
|
||||||
|
pub is_incremental: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersistentLayerDesc {
|
||||||
|
pub fn short_id(&self) -> String {
|
||||||
|
self.filename().file_name()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_img(
|
||||||
|
tenant_id: TenantId,
|
||||||
|
timeline_id: TimelineId,
|
||||||
|
key_range: Range<Key>,
|
||||||
|
lsn: Lsn,
|
||||||
|
is_incremental: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
tenant_id,
|
||||||
|
timeline_id,
|
||||||
|
key_range,
|
||||||
|
lsn_range: Self::image_layer_lsn_range(lsn),
|
||||||
|
is_delta: false,
|
||||||
|
is_incremental,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_delta(
|
||||||
|
tenant_id: TenantId,
|
||||||
|
timeline_id: TimelineId,
|
||||||
|
key_range: Range<Key>,
|
||||||
|
lsn_range: Range<Lsn>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
tenant_id,
|
||||||
|
timeline_id,
|
||||||
|
key_range,
|
||||||
|
lsn_range,
|
||||||
|
is_delta: true,
|
||||||
|
is_incremental: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the LSN that the image layer covers.
|
||||||
|
pub fn image_layer_lsn(&self) -> Lsn {
|
||||||
|
assert!(!self.is_delta);
|
||||||
|
assert!(self.lsn_range.start + 1 == self.lsn_range.end);
|
||||||
|
self.lsn_range.start
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the LSN range corresponding to a single image layer LSN.
|
||||||
|
pub fn image_layer_lsn_range(lsn: Lsn) -> Range<Lsn> {
|
||||||
|
lsn..(lsn + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a delta file name for this layer.
|
||||||
|
///
|
||||||
|
/// Panic: if this is not a delta layer.
|
||||||
|
pub fn delta_file_name(&self) -> DeltaFileName {
|
||||||
|
assert!(self.is_delta);
|
||||||
|
DeltaFileName {
|
||||||
|
key_range: self.key_range.clone(),
|
||||||
|
lsn_range: self.lsn_range.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a delta file name for this layer.
|
||||||
|
///
|
||||||
|
/// Panic: if this is not an image layer, or the lsn range is invalid
|
||||||
|
pub fn image_file_name(&self) -> ImageFileName {
|
||||||
|
assert!(!self.is_delta);
|
||||||
|
assert!(self.lsn_range.start + 1 == self.lsn_range.end);
|
||||||
|
ImageFileName {
|
||||||
|
key_range: self.key_range.clone(),
|
||||||
|
lsn: self.lsn_range.start,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn filename(&self) -> LayerFileName {
|
||||||
|
if self.is_delta {
|
||||||
|
self.delta_file_name().into()
|
||||||
|
} else {
|
||||||
|
self.image_file_name().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,11 +18,10 @@ use utils::{
|
|||||||
lsn::Lsn,
|
lsn::Lsn,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::filename::{DeltaFileName, ImageFileName, LayerFileName};
|
use super::filename::{DeltaFileName, ImageFileName};
|
||||||
use super::image_layer::ImageLayer;
|
|
||||||
use super::{
|
use super::{
|
||||||
DeltaLayer, LayerAccessStats, LayerAccessStatsReset, LayerIter, LayerKeyIter,
|
DeltaLayer, ImageLayer, LayerAccessStats, LayerAccessStatsReset, LayerIter, LayerKeyIter,
|
||||||
LayerResidenceStatus, PersistentLayer,
|
LayerResidenceStatus, PersistentLayer, PersistentLayerDesc,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// RemoteLayer is a not yet downloaded [`ImageLayer`] or
|
/// RemoteLayer is a not yet downloaded [`ImageLayer`] or
|
||||||
@@ -34,19 +33,10 @@ use super::{
|
|||||||
///
|
///
|
||||||
/// See: [`crate::context::RequestContext`] for authorization to download
|
/// See: [`crate::context::RequestContext`] for authorization to download
|
||||||
pub struct RemoteLayer {
|
pub struct RemoteLayer {
|
||||||
tenantid: TenantId,
|
pub desc: PersistentLayerDesc,
|
||||||
timelineid: TimelineId,
|
|
||||||
key_range: Range<Key>,
|
|
||||||
lsn_range: Range<Lsn>,
|
|
||||||
|
|
||||||
pub file_name: LayerFileName,
|
|
||||||
|
|
||||||
pub layer_metadata: LayerFileMetadata,
|
pub layer_metadata: LayerFileMetadata,
|
||||||
|
|
||||||
is_delta: bool,
|
|
||||||
|
|
||||||
is_incremental: bool,
|
|
||||||
|
|
||||||
access_stats: LayerAccessStats,
|
access_stats: LayerAccessStats,
|
||||||
|
|
||||||
pub(crate) ongoing_download: Arc<tokio::sync::Semaphore>,
|
pub(crate) ongoing_download: Arc<tokio::sync::Semaphore>,
|
||||||
@@ -66,22 +56,14 @@ pub struct RemoteLayer {
|
|||||||
impl std::fmt::Debug for RemoteLayer {
|
impl std::fmt::Debug for RemoteLayer {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("RemoteLayer")
|
f.debug_struct("RemoteLayer")
|
||||||
.field("file_name", &self.file_name)
|
.field("file_name", &self.desc.filename())
|
||||||
.field("layer_metadata", &self.layer_metadata)
|
.field("layer_metadata", &self.layer_metadata)
|
||||||
.field("is_incremental", &self.is_incremental)
|
.field("is_incremental", &self.desc.is_incremental)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layer for RemoteLayer {
|
impl Layer for RemoteLayer {
|
||||||
fn get_key_range(&self) -> Range<Key> {
|
|
||||||
self.key_range.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_lsn_range(&self) -> Range<Lsn> {
|
|
||||||
self.lsn_range.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_value_reconstruct_data(
|
fn get_value_reconstruct_data(
|
||||||
&self,
|
&self,
|
||||||
_key: Key,
|
_key: Key,
|
||||||
@@ -95,53 +77,45 @@ impl Layer for RemoteLayer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_incremental(&self) -> bool {
|
|
||||||
self.is_incremental
|
|
||||||
}
|
|
||||||
|
|
||||||
/// debugging function to print out the contents of the layer
|
/// debugging function to print out the contents of the layer
|
||||||
fn dump(&self, _verbose: bool, _ctx: &RequestContext) -> Result<()> {
|
fn dump(&self, _verbose: bool, _ctx: &RequestContext) -> Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"----- remote layer for ten {} tli {} keys {}-{} lsn {}-{} ----",
|
"----- remote layer for ten {} tli {} keys {}-{} lsn {}-{} ----",
|
||||||
self.tenantid,
|
self.desc.tenant_id,
|
||||||
self.timelineid,
|
self.desc.timeline_id,
|
||||||
self.key_range.start,
|
self.desc.key_range.start,
|
||||||
self.key_range.end,
|
self.desc.key_range.end,
|
||||||
self.lsn_range.start,
|
self.desc.lsn_range.start,
|
||||||
self.lsn_range.end
|
self.desc.lsn_range.end
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn get_key_range(&self) -> Range<Key> {
|
||||||
|
self.layer_desc().key_range.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn get_lsn_range(&self) -> Range<Lsn> {
|
||||||
|
self.layer_desc().lsn_range.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn is_incremental(&self) -> bool {
|
||||||
|
self.layer_desc().is_incremental
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
fn short_id(&self) -> String {
|
fn short_id(&self) -> String {
|
||||||
self.filename().file_name()
|
self.layer_desc().short_id()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PersistentLayer for RemoteLayer {
|
impl PersistentLayer for RemoteLayer {
|
||||||
fn get_tenant_id(&self) -> TenantId {
|
fn layer_desc(&self) -> &PersistentLayerDesc {
|
||||||
self.tenantid
|
&self.desc
|
||||||
}
|
|
||||||
|
|
||||||
fn get_timeline_id(&self) -> TimelineId {
|
|
||||||
self.timelineid
|
|
||||||
}
|
|
||||||
|
|
||||||
fn filename(&self) -> LayerFileName {
|
|
||||||
if self.is_delta {
|
|
||||||
DeltaFileName {
|
|
||||||
key_range: self.key_range.clone(),
|
|
||||||
lsn_range: self.lsn_range.clone(),
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
ImageFileName {
|
|
||||||
key_range: self.key_range.clone(),
|
|
||||||
lsn: self.lsn_range.start,
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_path(&self) -> Option<PathBuf> {
|
fn local_path(&self) -> Option<PathBuf> {
|
||||||
@@ -176,7 +150,7 @@ impl PersistentLayer for RemoteLayer {
|
|||||||
let layer_file_name = self.filename().file_name();
|
let layer_file_name = self.filename().file_name();
|
||||||
let lsn_range = self.get_lsn_range();
|
let lsn_range = self.get_lsn_range();
|
||||||
|
|
||||||
if self.is_delta {
|
if self.desc.is_delta {
|
||||||
HistoricLayerInfo::Delta {
|
HistoricLayerInfo::Delta {
|
||||||
layer_file_name,
|
layer_file_name,
|
||||||
layer_file_size: self.layer_metadata.file_size(),
|
layer_file_size: self.layer_metadata.file_size(),
|
||||||
@@ -210,13 +184,13 @@ impl RemoteLayer {
|
|||||||
access_stats: LayerAccessStats,
|
access_stats: LayerAccessStats,
|
||||||
) -> RemoteLayer {
|
) -> RemoteLayer {
|
||||||
RemoteLayer {
|
RemoteLayer {
|
||||||
tenantid,
|
desc: PersistentLayerDesc::new_img(
|
||||||
timelineid,
|
tenantid,
|
||||||
key_range: fname.key_range.clone(),
|
timelineid,
|
||||||
lsn_range: fname.lsn_as_range(),
|
fname.key_range.clone(),
|
||||||
is_delta: false,
|
fname.lsn,
|
||||||
is_incremental: false,
|
false,
|
||||||
file_name: fname.to_owned().into(),
|
),
|
||||||
layer_metadata: layer_metadata.clone(),
|
layer_metadata: layer_metadata.clone(),
|
||||||
ongoing_download: Arc::new(tokio::sync::Semaphore::new(1)),
|
ongoing_download: Arc::new(tokio::sync::Semaphore::new(1)),
|
||||||
download_replacement_failure: std::sync::atomic::AtomicBool::default(),
|
download_replacement_failure: std::sync::atomic::AtomicBool::default(),
|
||||||
@@ -232,13 +206,12 @@ impl RemoteLayer {
|
|||||||
access_stats: LayerAccessStats,
|
access_stats: LayerAccessStats,
|
||||||
) -> RemoteLayer {
|
) -> RemoteLayer {
|
||||||
RemoteLayer {
|
RemoteLayer {
|
||||||
tenantid,
|
desc: PersistentLayerDesc::new_delta(
|
||||||
timelineid,
|
tenantid,
|
||||||
key_range: fname.key_range.clone(),
|
timelineid,
|
||||||
lsn_range: fname.lsn_range.clone(),
|
fname.key_range.clone(),
|
||||||
is_delta: true,
|
fname.lsn_range.clone(),
|
||||||
is_incremental: true,
|
),
|
||||||
file_name: fname.to_owned().into(),
|
|
||||||
layer_metadata: layer_metadata.clone(),
|
layer_metadata: layer_metadata.clone(),
|
||||||
ongoing_download: Arc::new(tokio::sync::Semaphore::new(1)),
|
ongoing_download: Arc::new(tokio::sync::Semaphore::new(1)),
|
||||||
download_replacement_failure: std::sync::atomic::AtomicBool::default(),
|
download_replacement_failure: std::sync::atomic::AtomicBool::default(),
|
||||||
@@ -256,15 +229,12 @@ impl RemoteLayer {
|
|||||||
where
|
where
|
||||||
L: ?Sized + Layer,
|
L: ?Sized + Layer,
|
||||||
{
|
{
|
||||||
if self.is_delta {
|
if self.desc.is_delta {
|
||||||
let fname = DeltaFileName {
|
let fname = self.desc.delta_file_name();
|
||||||
key_range: self.key_range.clone(),
|
|
||||||
lsn_range: self.lsn_range.clone(),
|
|
||||||
};
|
|
||||||
Arc::new(DeltaLayer::new(
|
Arc::new(DeltaLayer::new(
|
||||||
conf,
|
conf,
|
||||||
self.timelineid,
|
self.desc.timeline_id,
|
||||||
self.tenantid,
|
self.desc.tenant_id,
|
||||||
&fname,
|
&fname,
|
||||||
file_size,
|
file_size,
|
||||||
self.access_stats.clone_for_residence_change(
|
self.access_stats.clone_for_residence_change(
|
||||||
@@ -273,14 +243,11 @@ impl RemoteLayer {
|
|||||||
),
|
),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
let fname = ImageFileName {
|
let fname = self.desc.image_file_name();
|
||||||
key_range: self.key_range.clone(),
|
|
||||||
lsn: self.lsn_range.start,
|
|
||||||
};
|
|
||||||
Arc::new(ImageLayer::new(
|
Arc::new(ImageLayer::new(
|
||||||
conf,
|
conf,
|
||||||
self.timelineid,
|
self.desc.timeline_id,
|
||||||
self.tenantid,
|
self.desc.tenant_id,
|
||||||
&fname,
|
&fname,
|
||||||
file_size,
|
file_size,
|
||||||
self.access_stats.clone_for_residence_change(
|
self.access_stats.clone_for_residence_change(
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ use crate::context::{DownloadBehavior, RequestContext};
|
|||||||
use crate::metrics::TENANT_TASK_EVENTS;
|
use crate::metrics::TENANT_TASK_EVENTS;
|
||||||
use crate::task_mgr;
|
use crate::task_mgr;
|
||||||
use crate::task_mgr::{TaskKind, BACKGROUND_RUNTIME};
|
use crate::task_mgr::{TaskKind, BACKGROUND_RUNTIME};
|
||||||
use crate::tenant::mgr;
|
|
||||||
use crate::tenant::{Tenant, TenantState};
|
use crate::tenant::{Tenant, TenantState};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
use utils::id::TenantId;
|
use utils::completion;
|
||||||
|
|
||||||
pub fn start_background_loops(tenant_id: TenantId) {
|
pub fn start_background_loops(tenant: &Arc<Tenant>, init_done: Option<&completion::Barrier>) {
|
||||||
|
let tenant_id = tenant.tenant_id;
|
||||||
task_mgr::spawn(
|
task_mgr::spawn(
|
||||||
BACKGROUND_RUNTIME.handle(),
|
BACKGROUND_RUNTIME.handle(),
|
||||||
TaskKind::Compaction,
|
TaskKind::Compaction,
|
||||||
@@ -23,11 +23,16 @@ pub fn start_background_loops(tenant_id: TenantId) {
|
|||||||
None,
|
None,
|
||||||
&format!("compactor for tenant {tenant_id}"),
|
&format!("compactor for tenant {tenant_id}"),
|
||||||
false,
|
false,
|
||||||
async move {
|
{
|
||||||
compaction_loop(tenant_id)
|
let tenant = Arc::clone(tenant);
|
||||||
.instrument(info_span!("compaction_loop", tenant_id = %tenant_id))
|
let init_done = init_done.cloned();
|
||||||
.await;
|
async move {
|
||||||
Ok(())
|
completion::Barrier::maybe_wait(init_done).await;
|
||||||
|
compaction_loop(tenant)
|
||||||
|
.instrument(info_span!("compaction_loop", tenant_id = %tenant_id))
|
||||||
|
.await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
task_mgr::spawn(
|
task_mgr::spawn(
|
||||||
@@ -37,11 +42,16 @@ pub fn start_background_loops(tenant_id: TenantId) {
|
|||||||
None,
|
None,
|
||||||
&format!("garbage collector for tenant {tenant_id}"),
|
&format!("garbage collector for tenant {tenant_id}"),
|
||||||
false,
|
false,
|
||||||
async move {
|
{
|
||||||
gc_loop(tenant_id)
|
let tenant = Arc::clone(tenant);
|
||||||
.instrument(info_span!("gc_loop", tenant_id = %tenant_id))
|
let init_done = init_done.cloned();
|
||||||
.await;
|
async move {
|
||||||
Ok(())
|
completion::Barrier::maybe_wait(init_done).await;
|
||||||
|
gc_loop(tenant)
|
||||||
|
.instrument(info_span!("gc_loop", tenant_id = %tenant_id))
|
||||||
|
.await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -49,7 +59,7 @@ pub fn start_background_loops(tenant_id: TenantId) {
|
|||||||
///
|
///
|
||||||
/// Compaction task's main loop
|
/// Compaction task's main loop
|
||||||
///
|
///
|
||||||
async fn compaction_loop(tenant_id: TenantId) {
|
async fn compaction_loop(tenant: Arc<Tenant>) {
|
||||||
let wait_duration = Duration::from_secs(2);
|
let wait_duration = Duration::from_secs(2);
|
||||||
info!("starting");
|
info!("starting");
|
||||||
TENANT_TASK_EVENTS.with_label_values(&["start"]).inc();
|
TENANT_TASK_EVENTS.with_label_values(&["start"]).inc();
|
||||||
@@ -60,16 +70,16 @@ async fn compaction_loop(tenant_id: TenantId) {
|
|||||||
loop {
|
loop {
|
||||||
trace!("waking up");
|
trace!("waking up");
|
||||||
|
|
||||||
let tenant = tokio::select! {
|
tokio::select! {
|
||||||
_ = cancel.cancelled() => {
|
_ = cancel.cancelled() => {
|
||||||
info!("received cancellation request");
|
info!("received cancellation request");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
tenant_wait_result = wait_for_active_tenant(tenant_id, wait_duration) => match tenant_wait_result {
|
tenant_wait_result = wait_for_active_tenant(&tenant) => match tenant_wait_result {
|
||||||
ControlFlow::Break(()) => return,
|
ControlFlow::Break(()) => return,
|
||||||
ControlFlow::Continue(tenant) => tenant,
|
ControlFlow::Continue(()) => (),
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
let period = tenant.get_compaction_period();
|
let period = tenant.get_compaction_period();
|
||||||
|
|
||||||
@@ -119,7 +129,7 @@ async fn compaction_loop(tenant_id: TenantId) {
|
|||||||
///
|
///
|
||||||
/// GC task's main loop
|
/// GC task's main loop
|
||||||
///
|
///
|
||||||
async fn gc_loop(tenant_id: TenantId) {
|
async fn gc_loop(tenant: Arc<Tenant>) {
|
||||||
let wait_duration = Duration::from_secs(2);
|
let wait_duration = Duration::from_secs(2);
|
||||||
info!("starting");
|
info!("starting");
|
||||||
TENANT_TASK_EVENTS.with_label_values(&["start"]).inc();
|
TENANT_TASK_EVENTS.with_label_values(&["start"]).inc();
|
||||||
@@ -127,21 +137,22 @@ async fn gc_loop(tenant_id: TenantId) {
|
|||||||
let cancel = task_mgr::shutdown_token();
|
let cancel = task_mgr::shutdown_token();
|
||||||
// GC might require downloading, to find the cutoff LSN that corresponds to the
|
// GC might require downloading, to find the cutoff LSN that corresponds to the
|
||||||
// cutoff specified as time.
|
// cutoff specified as time.
|
||||||
let ctx = RequestContext::todo_child(TaskKind::GarbageCollector, DownloadBehavior::Download);
|
let ctx =
|
||||||
|
RequestContext::todo_child(TaskKind::GarbageCollector, DownloadBehavior::Download);
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
loop {
|
loop {
|
||||||
trace!("waking up");
|
trace!("waking up");
|
||||||
|
|
||||||
let tenant = tokio::select! {
|
tokio::select! {
|
||||||
_ = cancel.cancelled() => {
|
_ = cancel.cancelled() => {
|
||||||
info!("received cancellation request");
|
info!("received cancellation request");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
tenant_wait_result = wait_for_active_tenant(tenant_id, wait_duration) => match tenant_wait_result {
|
tenant_wait_result = wait_for_active_tenant(&tenant) => match tenant_wait_result {
|
||||||
ControlFlow::Break(()) => return,
|
ControlFlow::Break(()) => return,
|
||||||
ControlFlow::Continue(tenant) => tenant,
|
ControlFlow::Continue(()) => (),
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
let period = tenant.get_gc_period();
|
let period = tenant.get_gc_period();
|
||||||
|
|
||||||
@@ -161,7 +172,9 @@ async fn gc_loop(tenant_id: TenantId) {
|
|||||||
Duration::from_secs(10)
|
Duration::from_secs(10)
|
||||||
} else {
|
} else {
|
||||||
// Run gc
|
// Run gc
|
||||||
let res = tenant.gc_iteration(None, gc_horizon, tenant.get_pitr_interval(), &ctx).await;
|
let res = tenant
|
||||||
|
.gc_iteration(None, gc_horizon, tenant.get_pitr_interval(), &ctx)
|
||||||
|
.await;
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
error!("Gc failed, retrying in {:?}: {e:?}", wait_duration);
|
error!("Gc failed, retrying in {:?}: {e:?}", wait_duration);
|
||||||
wait_duration
|
wait_duration
|
||||||
@@ -187,23 +200,10 @@ async fn gc_loop(tenant_id: TenantId) {
|
|||||||
trace!("GC loop stopped.");
|
trace!("GC loop stopped.");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_active_tenant(
|
async fn wait_for_active_tenant(tenant: &Arc<Tenant>) -> ControlFlow<()> {
|
||||||
tenant_id: TenantId,
|
|
||||||
wait: Duration,
|
|
||||||
) -> ControlFlow<(), Arc<Tenant>> {
|
|
||||||
let tenant = loop {
|
|
||||||
match mgr::get_tenant(tenant_id, false).await {
|
|
||||||
Ok(tenant) => break tenant,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to get a tenant {tenant_id}: {e:#}");
|
|
||||||
tokio::time::sleep(wait).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// if the tenant has a proper status already, no need to wait for anything
|
// if the tenant has a proper status already, no need to wait for anything
|
||||||
if tenant.current_state() == TenantState::Active {
|
if tenant.current_state() == TenantState::Active {
|
||||||
ControlFlow::Continue(tenant)
|
ControlFlow::Continue(())
|
||||||
} else {
|
} else {
|
||||||
let mut tenant_state_updates = tenant.subscribe_for_state_updates();
|
let mut tenant_state_updates = tenant.subscribe_for_state_updates();
|
||||||
loop {
|
loop {
|
||||||
@@ -213,7 +213,7 @@ async fn wait_for_active_tenant(
|
|||||||
match new_state {
|
match new_state {
|
||||||
TenantState::Active => {
|
TenantState::Active => {
|
||||||
debug!("Tenant state changed to active, continuing the task loop");
|
debug!("Tenant state changed to active, continuing the task loop");
|
||||||
return ControlFlow::Continue(tenant);
|
return ControlFlow::Continue(());
|
||||||
}
|
}
|
||||||
state => {
|
state => {
|
||||||
debug!("Not running the task loop, tenant is not active: {state:?}");
|
debug!("Not running the task loop, tenant is not active: {state:?}");
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ use std::sync::atomic::{AtomicI64, Ordering as AtomicOrdering};
|
|||||||
use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak};
|
use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak};
|
||||||
use std::time::{Duration, Instant, SystemTime};
|
use std::time::{Duration, Instant, SystemTime};
|
||||||
|
|
||||||
use crate::broker_client::{get_broker_client, is_broker_client_initialized};
|
|
||||||
use crate::context::{DownloadBehavior, RequestContext};
|
use crate::context::{DownloadBehavior, RequestContext};
|
||||||
use crate::tenant::remote_timeline_client::{self, index::LayerFileMetadata};
|
use crate::tenant::remote_timeline_client::{self, index::LayerFileMetadata};
|
||||||
use crate::tenant::storage_layer::{
|
use crate::tenant::storage_layer::{
|
||||||
@@ -196,8 +195,9 @@ pub struct Timeline {
|
|||||||
/// Layer removal lock.
|
/// Layer removal lock.
|
||||||
/// A lock to ensure that no layer of the timeline is removed concurrently by other tasks.
|
/// A lock to ensure that no layer of the timeline is removed concurrently by other tasks.
|
||||||
/// This lock is acquired in [`Timeline::gc`], [`Timeline::compact`],
|
/// This lock is acquired in [`Timeline::gc`], [`Timeline::compact`],
|
||||||
/// and [`Tenant::delete_timeline`].
|
/// and [`Tenant::delete_timeline`]. This is an `Arc<Mutex>` lock because we need an owned
|
||||||
pub(super) layer_removal_cs: tokio::sync::Mutex<()>,
|
/// lock guard in functions that will be spawned to tokio I/O pool (which requires `'static`).
|
||||||
|
pub(super) layer_removal_cs: Arc<tokio::sync::Mutex<()>>,
|
||||||
|
|
||||||
// Needed to ensure that we can't create a branch at a point that was already garbage collected
|
// Needed to ensure that we can't create a branch at a point that was already garbage collected
|
||||||
pub latest_gc_cutoff_lsn: Rcu<Lsn>,
|
pub latest_gc_cutoff_lsn: Rcu<Lsn>,
|
||||||
@@ -227,7 +227,7 @@ pub struct Timeline {
|
|||||||
/// or None if WAL receiver has not received anything for this timeline
|
/// or None if WAL receiver has not received anything for this timeline
|
||||||
/// yet.
|
/// yet.
|
||||||
pub last_received_wal: Mutex<Option<WalReceiverInfo>>,
|
pub last_received_wal: Mutex<Option<WalReceiverInfo>>,
|
||||||
pub walreceiver: WalReceiver,
|
pub walreceiver: Mutex<Option<WalReceiver>>,
|
||||||
|
|
||||||
/// Relation size cache
|
/// Relation size cache
|
||||||
pub rel_size_cache: RwLock<HashMap<RelTag, (Lsn, BlockNumber)>>,
|
pub rel_size_cache: RwLock<HashMap<RelTag, (Lsn, BlockNumber)>>,
|
||||||
@@ -236,6 +236,10 @@ pub struct Timeline {
|
|||||||
|
|
||||||
state: watch::Sender<TimelineState>,
|
state: watch::Sender<TimelineState>,
|
||||||
|
|
||||||
|
/// Prevent two tasks from deleting the timeline at the same time. If held, the
|
||||||
|
/// timeline is being deleted. If 'true', the timeline has already been deleted.
|
||||||
|
pub delete_lock: tokio::sync::Mutex<bool>,
|
||||||
|
|
||||||
eviction_task_timeline_state: tokio::sync::Mutex<EvictionTaskTimelineState>,
|
eviction_task_timeline_state: tokio::sync::Mutex<EvictionTaskTimelineState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,7 +525,12 @@ impl Timeline {
|
|||||||
Some((cached_lsn, cached_img)) => {
|
Some((cached_lsn, cached_img)) => {
|
||||||
match cached_lsn.cmp(&lsn) {
|
match cached_lsn.cmp(&lsn) {
|
||||||
Ordering::Less => {} // there might be WAL between cached_lsn and lsn, we need to check
|
Ordering::Less => {} // there might be WAL between cached_lsn and lsn, we need to check
|
||||||
Ordering::Equal => return Ok(cached_img), // exact LSN match, return the image
|
Ordering::Equal => {
|
||||||
|
self.metrics
|
||||||
|
.materialized_page_cache_hit_upon_request_counter
|
||||||
|
.inc();
|
||||||
|
return Ok(cached_img); // exact LSN match, return the image
|
||||||
|
}
|
||||||
Ordering::Greater => {
|
Ordering::Greater => {
|
||||||
unreachable!("the returned lsn should never be after the requested lsn")
|
unreachable!("the returned lsn should never be after the requested lsn")
|
||||||
}
|
}
|
||||||
@@ -536,8 +545,10 @@ impl Timeline {
|
|||||||
img: cached_page_img,
|
img: cached_page_img,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let timer = self.metrics.get_reconstruct_data_time_histo.start_timer();
|
||||||
self.get_reconstruct_data(key, lsn, &mut reconstruct_state, ctx)
|
self.get_reconstruct_data(key, lsn, &mut reconstruct_state, ctx)
|
||||||
.await?;
|
.await?;
|
||||||
|
timer.stop_and_record();
|
||||||
|
|
||||||
self.metrics
|
self.metrics
|
||||||
.reconstruct_time_histo
|
.reconstruct_time_histo
|
||||||
@@ -622,17 +633,27 @@ impl Timeline {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(()),
|
||||||
seqwait_error => {
|
Err(e) => {
|
||||||
|
// don't count the time spent waiting for lock below, and also in walreceiver.status(), towards the wait_lsn_time_histo
|
||||||
drop(_timer);
|
drop(_timer);
|
||||||
let walreceiver_status = self.walreceiver.status().await;
|
let walreceiver_status = {
|
||||||
seqwait_error.with_context(|| format!(
|
match &*self.walreceiver.lock().unwrap() {
|
||||||
"Timed out while waiting for WAL record at LSN {} to arrive, last_record_lsn {} disk consistent LSN={}, {}",
|
None => "stopping or stopped".to_string(),
|
||||||
lsn,
|
Some(walreceiver) => match walreceiver.status() {
|
||||||
self.get_last_record_lsn(),
|
Some(status) => status.to_human_readable_string(),
|
||||||
self.get_disk_consistent_lsn(),
|
None => "Not active".to_string(),
|
||||||
walreceiver_status.map(|status| status.to_human_readable_string())
|
},
|
||||||
.unwrap_or_else(|| "WalReceiver status: Not active".to_string()),
|
}
|
||||||
))
|
};
|
||||||
|
Err(anyhow::Error::new(e).context({
|
||||||
|
format!(
|
||||||
|
"Timed out while waiting for WAL record at LSN {} to arrive, last_record_lsn {} disk consistent LSN={}, WalReceiver status: {}",
|
||||||
|
lsn,
|
||||||
|
self.get_last_record_lsn(),
|
||||||
|
self.get_disk_consistent_lsn(),
|
||||||
|
walreceiver_status,
|
||||||
|
)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -660,7 +681,7 @@ impl Timeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Outermost timeline compaction operation; downloads needed layers.
|
/// Outermost timeline compaction operation; downloads needed layers.
|
||||||
pub async fn compact(&self, ctx: &RequestContext) -> anyhow::Result<()> {
|
pub async fn compact(self: &Arc<Self>, ctx: &RequestContext) -> anyhow::Result<()> {
|
||||||
const ROUNDS: usize = 2;
|
const ROUNDS: usize = 2;
|
||||||
|
|
||||||
let last_record_lsn = self.get_last_record_lsn();
|
let last_record_lsn = self.get_last_record_lsn();
|
||||||
@@ -749,7 +770,7 @@ impl Timeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Compaction which might need to be retried after downloading remote layers.
|
/// Compaction which might need to be retried after downloading remote layers.
|
||||||
async fn compact_inner(&self, ctx: &RequestContext) -> Result<(), CompactionError> {
|
async fn compact_inner(self: &Arc<Self>, ctx: &RequestContext) -> Result<(), CompactionError> {
|
||||||
//
|
//
|
||||||
// High level strategy for compaction / image creation:
|
// High level strategy for compaction / image creation:
|
||||||
//
|
//
|
||||||
@@ -784,7 +805,7 @@ impl Timeline {
|
|||||||
// Below are functions compact_level0() and create_image_layers()
|
// Below are functions compact_level0() and create_image_layers()
|
||||||
// but they are a bit ad hoc and don't quite work like it's explained
|
// but they are a bit ad hoc and don't quite work like it's explained
|
||||||
// above. Rewrite it.
|
// above. Rewrite it.
|
||||||
let layer_removal_cs = self.layer_removal_cs.lock().await;
|
let layer_removal_cs = Arc::new(self.layer_removal_cs.clone().lock_owned().await);
|
||||||
// Is the timeline being deleted?
|
// Is the timeline being deleted?
|
||||||
let state = *self.state.borrow();
|
let state = *self.state.borrow();
|
||||||
if state == TimelineState::Stopping {
|
if state == TimelineState::Stopping {
|
||||||
@@ -818,7 +839,7 @@ impl Timeline {
|
|||||||
|
|
||||||
// 3. Compact
|
// 3. Compact
|
||||||
let timer = self.metrics.compact_time_histo.start_timer();
|
let timer = self.metrics.compact_time_histo.start_timer();
|
||||||
self.compact_level0(&layer_removal_cs, target_file_size, ctx)
|
self.compact_level0(layer_removal_cs.clone(), target_file_size, ctx)
|
||||||
.await?;
|
.await?;
|
||||||
timer.stop_and_record();
|
timer.stop_and_record();
|
||||||
}
|
}
|
||||||
@@ -907,18 +928,10 @@ impl Timeline {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activate(self: &Arc<Self>, ctx: &RequestContext) -> anyhow::Result<()> {
|
pub fn activate(self: &Arc<Self>, broker_client: BrokerClientChannel, ctx: &RequestContext) {
|
||||||
if is_broker_client_initialized() {
|
self.launch_wal_receiver(ctx, broker_client);
|
||||||
self.launch_wal_receiver(ctx, get_broker_client().clone())?;
|
|
||||||
} else if cfg!(test) {
|
|
||||||
info!("not launching WAL receiver because broker client hasn't been initialized");
|
|
||||||
} else {
|
|
||||||
anyhow::bail!("broker client not initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.set_state(TimelineState::Active);
|
self.set_state(TimelineState::Active);
|
||||||
self.launch_eviction_task();
|
self.launch_eviction_task();
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_state(&self, new_state: TimelineState) {
|
pub fn set_state(&self, new_state: TimelineState) {
|
||||||
@@ -1285,6 +1298,13 @@ impl Timeline {
|
|||||||
.unwrap_or(default_tenant_conf.evictions_low_residence_duration_metric_threshold)
|
.unwrap_or(default_tenant_conf.evictions_low_residence_duration_metric_threshold)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_gc_feedback(&self) -> bool {
|
||||||
|
let tenant_conf = self.tenant_conf.read().unwrap();
|
||||||
|
tenant_conf
|
||||||
|
.gc_feedback
|
||||||
|
.unwrap_or(self.conf.default_tenant_conf.gc_feedback)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn tenant_conf_updated(&self) {
|
pub(super) fn tenant_conf_updated(&self) {
|
||||||
// NB: Most tenant conf options are read by background loops, so,
|
// NB: Most tenant conf options are read by background loops, so,
|
||||||
// changes will automatically be picked up.
|
// changes will automatically be picked up.
|
||||||
@@ -1327,15 +1347,7 @@ impl Timeline {
|
|||||||
let (layer_flush_done_tx, _) = tokio::sync::watch::channel((0, Ok(())));
|
let (layer_flush_done_tx, _) = tokio::sync::watch::channel((0, Ok(())));
|
||||||
|
|
||||||
let tenant_conf_guard = tenant_conf.read().unwrap();
|
let tenant_conf_guard = tenant_conf.read().unwrap();
|
||||||
let wal_connect_timeout = tenant_conf_guard
|
|
||||||
.walreceiver_connect_timeout
|
|
||||||
.unwrap_or(conf.default_tenant_conf.walreceiver_connect_timeout);
|
|
||||||
let lagging_wal_timeout = tenant_conf_guard
|
|
||||||
.lagging_wal_timeout
|
|
||||||
.unwrap_or(conf.default_tenant_conf.lagging_wal_timeout);
|
|
||||||
let max_lsn_wal_lag = tenant_conf_guard
|
|
||||||
.max_lsn_wal_lag
|
|
||||||
.unwrap_or(conf.default_tenant_conf.max_lsn_wal_lag);
|
|
||||||
let evictions_low_residence_duration_metric_threshold =
|
let evictions_low_residence_duration_metric_threshold =
|
||||||
Self::get_evictions_low_residence_duration_metric_threshold(
|
Self::get_evictions_low_residence_duration_metric_threshold(
|
||||||
&tenant_conf_guard,
|
&tenant_conf_guard,
|
||||||
@@ -1344,18 +1356,6 @@ impl Timeline {
|
|||||||
drop(tenant_conf_guard);
|
drop(tenant_conf_guard);
|
||||||
|
|
||||||
Arc::new_cyclic(|myself| {
|
Arc::new_cyclic(|myself| {
|
||||||
let walreceiver = WalReceiver::new(
|
|
||||||
TenantTimelineId::new(tenant_id, timeline_id),
|
|
||||||
Weak::clone(myself),
|
|
||||||
WalReceiverConf {
|
|
||||||
wal_connect_timeout,
|
|
||||||
lagging_wal_timeout,
|
|
||||||
max_lsn_wal_lag,
|
|
||||||
auth_token: crate::config::SAFEKEEPER_AUTH_TOKEN.get().cloned(),
|
|
||||||
availability_zone: conf.availability_zone.clone(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut result = Timeline {
|
let mut result = Timeline {
|
||||||
conf,
|
conf,
|
||||||
tenant_conf,
|
tenant_conf,
|
||||||
@@ -1367,7 +1367,7 @@ impl Timeline {
|
|||||||
wanted_image_layers: Mutex::new(None),
|
wanted_image_layers: Mutex::new(None),
|
||||||
|
|
||||||
walredo_mgr,
|
walredo_mgr,
|
||||||
walreceiver,
|
walreceiver: Mutex::new(None),
|
||||||
|
|
||||||
remote_client: remote_client.map(Arc::new),
|
remote_client: remote_client.map(Arc::new),
|
||||||
|
|
||||||
@@ -1432,6 +1432,7 @@ impl Timeline {
|
|||||||
eviction_task_timeline_state: tokio::sync::Mutex::new(
|
eviction_task_timeline_state: tokio::sync::Mutex::new(
|
||||||
EvictionTaskTimelineState::default(),
|
EvictionTaskTimelineState::default(),
|
||||||
),
|
),
|
||||||
|
delete_lock: tokio::sync::Mutex::new(false),
|
||||||
};
|
};
|
||||||
result.repartition_threshold = result.get_checkpoint_distance() / 10;
|
result.repartition_threshold = result.get_checkpoint_distance() / 10;
|
||||||
result
|
result
|
||||||
@@ -1487,17 +1488,49 @@ impl Timeline {
|
|||||||
*flush_loop_state = FlushLoopState::Running;
|
*flush_loop_state = FlushLoopState::Running;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn launch_wal_receiver(
|
/// Creates and starts the wal receiver.
|
||||||
&self,
|
///
|
||||||
|
/// This function is expected to be called at most once per Timeline's lifecycle
|
||||||
|
/// when the timeline is activated.
|
||||||
|
fn launch_wal_receiver(
|
||||||
|
self: &Arc<Self>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
broker_client: BrokerClientChannel,
|
broker_client: BrokerClientChannel,
|
||||||
) -> anyhow::Result<()> {
|
) {
|
||||||
info!(
|
info!(
|
||||||
"launching WAL receiver for timeline {} of tenant {}",
|
"launching WAL receiver for timeline {} of tenant {}",
|
||||||
self.timeline_id, self.tenant_id
|
self.timeline_id, self.tenant_id
|
||||||
);
|
);
|
||||||
self.walreceiver.start(ctx, broker_client)?;
|
|
||||||
Ok(())
|
let tenant_conf_guard = self.tenant_conf.read().unwrap();
|
||||||
|
let wal_connect_timeout = tenant_conf_guard
|
||||||
|
.walreceiver_connect_timeout
|
||||||
|
.unwrap_or(self.conf.default_tenant_conf.walreceiver_connect_timeout);
|
||||||
|
let lagging_wal_timeout = tenant_conf_guard
|
||||||
|
.lagging_wal_timeout
|
||||||
|
.unwrap_or(self.conf.default_tenant_conf.lagging_wal_timeout);
|
||||||
|
let max_lsn_wal_lag = tenant_conf_guard
|
||||||
|
.max_lsn_wal_lag
|
||||||
|
.unwrap_or(self.conf.default_tenant_conf.max_lsn_wal_lag);
|
||||||
|
drop(tenant_conf_guard);
|
||||||
|
|
||||||
|
let mut guard = self.walreceiver.lock().unwrap();
|
||||||
|
assert!(
|
||||||
|
guard.is_none(),
|
||||||
|
"multiple launches / re-launches of WAL receiver are not supported"
|
||||||
|
);
|
||||||
|
*guard = Some(WalReceiver::start(
|
||||||
|
Arc::clone(self),
|
||||||
|
WalReceiverConf {
|
||||||
|
wal_connect_timeout,
|
||||||
|
lagging_wal_timeout,
|
||||||
|
max_lsn_wal_lag,
|
||||||
|
auth_token: crate::config::SAFEKEEPER_AUTH_TOKEN.get().cloned(),
|
||||||
|
availability_zone: self.conf.availability_zone.clone(),
|
||||||
|
},
|
||||||
|
broker_client,
|
||||||
|
ctx,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -1891,6 +1924,7 @@ impl Timeline {
|
|||||||
// no cancellation here, because nothing really waits for this to complete compared
|
// no cancellation here, because nothing really waits for this to complete compared
|
||||||
// to spawn_ondemand_logical_size_calculation.
|
// to spawn_ondemand_logical_size_calculation.
|
||||||
let cancel = CancellationToken::new();
|
let cancel = CancellationToken::new();
|
||||||
|
|
||||||
let calculated_size = match self_clone
|
let calculated_size = match self_clone
|
||||||
.logical_size_calculation_task(lsn, LogicalSizeCalculationCause::Initial, &background_ctx, cancel)
|
.logical_size_calculation_task(lsn, LogicalSizeCalculationCause::Initial, &background_ctx, cancel)
|
||||||
.await
|
.await
|
||||||
@@ -2155,7 +2189,7 @@ impl Timeline {
|
|||||||
fn delete_historic_layer(
|
fn delete_historic_layer(
|
||||||
&self,
|
&self,
|
||||||
// we cannot remove layers otherwise, since gc and compaction will race
|
// we cannot remove layers otherwise, since gc and compaction will race
|
||||||
_layer_removal_cs: &tokio::sync::MutexGuard<'_, ()>,
|
_layer_removal_cs: Arc<tokio::sync::OwnedMutexGuard<()>>,
|
||||||
layer: Arc<dyn PersistentLayer>,
|
layer: Arc<dyn PersistentLayer>,
|
||||||
updates: &mut BatchedUpdates<'_, dyn PersistentLayer>,
|
updates: &mut BatchedUpdates<'_, dyn PersistentLayer>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
@@ -2234,6 +2268,9 @@ impl Timeline {
|
|||||||
let mut timeline_owned;
|
let mut timeline_owned;
|
||||||
let mut timeline = self;
|
let mut timeline = self;
|
||||||
|
|
||||||
|
let mut read_count =
|
||||||
|
scopeguard::guard(0, |cnt| self.metrics.read_num_fs_layers.observe(cnt as f64));
|
||||||
|
|
||||||
// For debugging purposes, collect the path of layers that we traversed
|
// For debugging purposes, collect the path of layers that we traversed
|
||||||
// through. It's included in the error message if we fail to find the key.
|
// through. It's included in the error message if we fail to find the key.
|
||||||
let mut traversal_path = Vec::<TraversalPathItem>::new();
|
let mut traversal_path = Vec::<TraversalPathItem>::new();
|
||||||
@@ -2368,6 +2405,7 @@ impl Timeline {
|
|||||||
Err(e) => return Err(PageReconstructError::from(e)),
|
Err(e) => return Err(PageReconstructError::from(e)),
|
||||||
};
|
};
|
||||||
cont_lsn = lsn_floor;
|
cont_lsn = lsn_floor;
|
||||||
|
// metrics: open_layer does not count as fs access, so we are not updating `read_count`
|
||||||
traversal_path.push((
|
traversal_path.push((
|
||||||
result,
|
result,
|
||||||
cont_lsn,
|
cont_lsn,
|
||||||
@@ -2394,6 +2432,7 @@ impl Timeline {
|
|||||||
Err(e) => return Err(PageReconstructError::from(e)),
|
Err(e) => return Err(PageReconstructError::from(e)),
|
||||||
};
|
};
|
||||||
cont_lsn = lsn_floor;
|
cont_lsn = lsn_floor;
|
||||||
|
// metrics: open_layer does not count as fs access, so we are not updating `read_count`
|
||||||
traversal_path.push((
|
traversal_path.push((
|
||||||
result,
|
result,
|
||||||
cont_lsn,
|
cont_lsn,
|
||||||
@@ -2428,6 +2467,7 @@ impl Timeline {
|
|||||||
Err(e) => return Err(PageReconstructError::from(e)),
|
Err(e) => return Err(PageReconstructError::from(e)),
|
||||||
};
|
};
|
||||||
cont_lsn = lsn_floor;
|
cont_lsn = lsn_floor;
|
||||||
|
*read_count += 1;
|
||||||
traversal_path.push((
|
traversal_path.push((
|
||||||
result,
|
result,
|
||||||
cont_lsn,
|
cont_lsn,
|
||||||
@@ -2493,7 +2533,7 @@ impl Timeline {
|
|||||||
(DownloadBehavior::Error, false) => {
|
(DownloadBehavior::Error, false) => {
|
||||||
return Err(PageReconstructError::NeedsDownload(
|
return Err(PageReconstructError::NeedsDownload(
|
||||||
TenantTimelineId::new(self.tenant_id, self.timeline_id),
|
TenantTimelineId::new(self.tenant_id, self.timeline_id),
|
||||||
remote_layer.file_name.clone(),
|
remote_layer.filename(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2619,7 +2659,7 @@ impl Timeline {
|
|||||||
|
|
||||||
/// Layer flusher task's main loop.
|
/// Layer flusher task's main loop.
|
||||||
async fn flush_loop(
|
async fn flush_loop(
|
||||||
&self,
|
self: &Arc<Self>,
|
||||||
mut layer_flush_start_rx: tokio::sync::watch::Receiver<u64>,
|
mut layer_flush_start_rx: tokio::sync::watch::Receiver<u64>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) {
|
) {
|
||||||
@@ -2708,9 +2748,9 @@ impl Timeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Flush one frozen in-memory layer to disk, as a new delta layer.
|
/// Flush one frozen in-memory layer to disk, as a new delta layer.
|
||||||
#[instrument(skip(self, frozen_layer, ctx), fields(tenant_id=%self.tenant_id, timeline_id=%self.timeline_id, layer=%frozen_layer.short_id()))]
|
#[instrument(skip_all, fields(tenant_id=%self.tenant_id, timeline_id=%self.timeline_id, layer=%frozen_layer.short_id()))]
|
||||||
async fn flush_frozen_layer(
|
async fn flush_frozen_layer(
|
||||||
&self,
|
self: &Arc<Self>,
|
||||||
frozen_layer: Arc<InMemoryLayer>,
|
frozen_layer: Arc<InMemoryLayer>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
@@ -2730,7 +2770,16 @@ impl Timeline {
|
|||||||
.await?
|
.await?
|
||||||
} else {
|
} else {
|
||||||
// normal case, write out a L0 delta layer file.
|
// normal case, write out a L0 delta layer file.
|
||||||
let (delta_path, metadata) = self.create_delta_layer(&frozen_layer)?;
|
let this = self.clone();
|
||||||
|
let frozen_layer = frozen_layer.clone();
|
||||||
|
let span = tracing::info_span!("blocking");
|
||||||
|
let (delta_path, metadata) = tokio::task::spawn_blocking(move || {
|
||||||
|
let _g = span.entered();
|
||||||
|
this.create_delta_layer(&frozen_layer)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("create_delta_layer spawn_blocking")
|
||||||
|
.and_then(|res| res)?;
|
||||||
HashMap::from([(delta_path, metadata)])
|
HashMap::from([(delta_path, metadata)])
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2834,7 +2883,7 @@ impl Timeline {
|
|||||||
|
|
||||||
// Write out the given frozen in-memory layer as a new L0 delta file
|
// Write out the given frozen in-memory layer as a new L0 delta file
|
||||||
fn create_delta_layer(
|
fn create_delta_layer(
|
||||||
&self,
|
self: &Arc<Self>,
|
||||||
frozen_layer: &InMemoryLayer,
|
frozen_layer: &InMemoryLayer,
|
||||||
) -> anyhow::Result<(LayerFileName, LayerFileMetadata)> {
|
) -> anyhow::Result<(LayerFileName, LayerFileMetadata)> {
|
||||||
// Write it out
|
// Write it out
|
||||||
@@ -2850,10 +2899,13 @@ impl Timeline {
|
|||||||
// TODO: If we're running inside 'flush_frozen_layers' and there are multiple
|
// TODO: If we're running inside 'flush_frozen_layers' and there are multiple
|
||||||
// files to flush, it might be better to first write them all, and then fsync
|
// files to flush, it might be better to first write them all, and then fsync
|
||||||
// them all in parallel.
|
// them all in parallel.
|
||||||
par_fsync::par_fsync(&[
|
|
||||||
new_delta_path.clone(),
|
// First sync the delta layer. We still use par_fsync here to keep everything consistent. Feel free to replace
|
||||||
self.conf.timeline_path(&self.timeline_id, &self.tenant_id),
|
// this with a single fsync in future refactors.
|
||||||
])?;
|
par_fsync::par_fsync(&[new_delta_path.clone()]).context("fsync of delta layer")?;
|
||||||
|
// Then sync the parent directory.
|
||||||
|
par_fsync::par_fsync(&[self.conf.timeline_path(&self.timeline_id, &self.tenant_id)])
|
||||||
|
.context("fsync of timeline dir")?;
|
||||||
|
|
||||||
// Add it to the layer map
|
// Add it to the layer map
|
||||||
let l = Arc::new(new_delta);
|
let l = Arc::new(new_delta);
|
||||||
@@ -3014,6 +3066,7 @@ impl Timeline {
|
|||||||
self.tenant_id,
|
self.tenant_id,
|
||||||
&img_range,
|
&img_range,
|
||||||
lsn,
|
lsn,
|
||||||
|
false, // image layer always covers the full range
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
fail_point!("image-layer-writer-fail-before-finish", |_| {
|
fail_point!("image-layer-writer-fail-before-finish", |_| {
|
||||||
@@ -3077,17 +3130,22 @@ impl Timeline {
|
|||||||
let all_paths = image_layers
|
let all_paths = image_layers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|layer| layer.path())
|
.map(|layer| layer.path())
|
||||||
.chain(std::iter::once(
|
|
||||||
self.conf.timeline_path(&self.timeline_id, &self.tenant_id),
|
|
||||||
))
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
par_fsync::par_fsync(&all_paths).context("fsync of newly created layer files")?;
|
|
||||||
|
par_fsync::par_fsync_async(&all_paths)
|
||||||
|
.await
|
||||||
|
.context("fsync of newly created layer files")?;
|
||||||
|
|
||||||
|
par_fsync::par_fsync_async(&[self.conf.timeline_path(&self.timeline_id, &self.tenant_id)])
|
||||||
|
.await
|
||||||
|
.context("fsync of timeline dir")?;
|
||||||
|
|
||||||
let mut layer_paths_to_upload = HashMap::with_capacity(image_layers.len());
|
let mut layer_paths_to_upload = HashMap::with_capacity(image_layers.len());
|
||||||
|
|
||||||
let mut layers = self.layers.write().unwrap();
|
let mut layers = self.layers.write().unwrap();
|
||||||
let mut updates = layers.batch_update();
|
let mut updates = layers.batch_update();
|
||||||
let timeline_path = self.conf.timeline_path(&self.timeline_id, &self.tenant_id);
|
let timeline_path = self.conf.timeline_path(&self.timeline_id, &self.tenant_id);
|
||||||
|
|
||||||
for l in image_layers {
|
for l in image_layers {
|
||||||
let path = l.filename();
|
let path = l.filename();
|
||||||
let metadata = timeline_path
|
let metadata = timeline_path
|
||||||
@@ -3146,9 +3204,9 @@ impl Timeline {
|
|||||||
/// This method takes the `_layer_removal_cs` guard to highlight it required downloads are
|
/// This method takes the `_layer_removal_cs` guard to highlight it required downloads are
|
||||||
/// returned as an error. If the `layer_removal_cs` boundary is changed not to be taken in the
|
/// returned as an error. If the `layer_removal_cs` boundary is changed not to be taken in the
|
||||||
/// start of level0 files compaction, the on-demand download should be revisited as well.
|
/// start of level0 files compaction, the on-demand download should be revisited as well.
|
||||||
async fn compact_level0_phase1(
|
fn compact_level0_phase1(
|
||||||
&self,
|
&self,
|
||||||
_layer_removal_cs: &tokio::sync::MutexGuard<'_, ()>,
|
_layer_removal_cs: Arc<tokio::sync::OwnedMutexGuard<()>>,
|
||||||
target_file_size: u64,
|
target_file_size: u64,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<CompactLevel0Phase1Result, CompactionError> {
|
) -> Result<CompactLevel0Phase1Result, CompactionError> {
|
||||||
@@ -3461,13 +3519,13 @@ impl Timeline {
|
|||||||
if !new_layers.is_empty() {
|
if !new_layers.is_empty() {
|
||||||
let mut layer_paths: Vec<PathBuf> = new_layers.iter().map(|l| l.path()).collect();
|
let mut layer_paths: Vec<PathBuf> = new_layers.iter().map(|l| l.path()).collect();
|
||||||
|
|
||||||
// also sync the directory
|
|
||||||
layer_paths.push(self.conf.timeline_path(&self.timeline_id, &self.tenant_id));
|
|
||||||
|
|
||||||
// Fsync all the layer files and directory using multiple threads to
|
// Fsync all the layer files and directory using multiple threads to
|
||||||
// minimize latency.
|
// minimize latency.
|
||||||
par_fsync::par_fsync(&layer_paths).context("fsync all new layers")?;
|
par_fsync::par_fsync(&layer_paths).context("fsync all new layers")?;
|
||||||
|
|
||||||
|
par_fsync::par_fsync(&[self.conf.timeline_path(&self.timeline_id, &self.tenant_id)])
|
||||||
|
.context("fsync of timeline dir")?;
|
||||||
|
|
||||||
layer_paths.pop().unwrap();
|
layer_paths.pop().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3484,17 +3542,26 @@ impl Timeline {
|
|||||||
/// as Level 1 files.
|
/// as Level 1 files.
|
||||||
///
|
///
|
||||||
async fn compact_level0(
|
async fn compact_level0(
|
||||||
&self,
|
self: &Arc<Self>,
|
||||||
layer_removal_cs: &tokio::sync::MutexGuard<'_, ()>,
|
layer_removal_cs: Arc<tokio::sync::OwnedMutexGuard<()>>,
|
||||||
target_file_size: u64,
|
target_file_size: u64,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<(), CompactionError> {
|
) -> Result<(), CompactionError> {
|
||||||
|
let this = self.clone();
|
||||||
|
let ctx_inner = ctx.clone();
|
||||||
|
let layer_removal_cs_inner = layer_removal_cs.clone();
|
||||||
|
let span = tracing::info_span!("blocking");
|
||||||
let CompactLevel0Phase1Result {
|
let CompactLevel0Phase1Result {
|
||||||
new_layers,
|
new_layers,
|
||||||
deltas_to_compact,
|
deltas_to_compact,
|
||||||
} = self
|
} = tokio::task::spawn_blocking(move || {
|
||||||
.compact_level0_phase1(layer_removal_cs, target_file_size, ctx)
|
let _g = span.entered();
|
||||||
.await?;
|
this.compact_level0_phase1(layer_removal_cs_inner, target_file_size, &ctx_inner)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("compact_level0_phase1 spawn_blocking")
|
||||||
|
.map_err(CompactionError::Other)
|
||||||
|
.and_then(|res| res)?;
|
||||||
|
|
||||||
if new_layers.is_empty() && deltas_to_compact.is_empty() {
|
if new_layers.is_empty() && deltas_to_compact.is_empty() {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
@@ -3552,7 +3619,7 @@ impl Timeline {
|
|||||||
let mut layer_names_to_delete = Vec::with_capacity(deltas_to_compact.len());
|
let mut layer_names_to_delete = Vec::with_capacity(deltas_to_compact.len());
|
||||||
for l in deltas_to_compact {
|
for l in deltas_to_compact {
|
||||||
layer_names_to_delete.push(l.filename());
|
layer_names_to_delete.push(l.filename());
|
||||||
self.delete_historic_layer(layer_removal_cs, l, &mut updates)?;
|
self.delete_historic_layer(layer_removal_cs.clone(), l, &mut updates)?;
|
||||||
}
|
}
|
||||||
updates.flush();
|
updates.flush();
|
||||||
drop(layers);
|
drop(layers);
|
||||||
@@ -3672,7 +3739,7 @@ impl Timeline {
|
|||||||
|
|
||||||
fail_point!("before-timeline-gc");
|
fail_point!("before-timeline-gc");
|
||||||
|
|
||||||
let layer_removal_cs = self.layer_removal_cs.lock().await;
|
let layer_removal_cs = Arc::new(self.layer_removal_cs.clone().lock_owned().await);
|
||||||
// Is the timeline being deleted?
|
// Is the timeline being deleted?
|
||||||
let state = *self.state.borrow();
|
let state = *self.state.borrow();
|
||||||
if state == TimelineState::Stopping {
|
if state == TimelineState::Stopping {
|
||||||
@@ -3692,7 +3759,7 @@ impl Timeline {
|
|||||||
|
|
||||||
let res = self
|
let res = self
|
||||||
.gc_timeline(
|
.gc_timeline(
|
||||||
&layer_removal_cs,
|
layer_removal_cs.clone(),
|
||||||
horizon_cutoff,
|
horizon_cutoff,
|
||||||
pitr_cutoff,
|
pitr_cutoff,
|
||||||
retain_lsns,
|
retain_lsns,
|
||||||
@@ -3711,7 +3778,7 @@ impl Timeline {
|
|||||||
|
|
||||||
async fn gc_timeline(
|
async fn gc_timeline(
|
||||||
&self,
|
&self,
|
||||||
layer_removal_cs: &tokio::sync::MutexGuard<'_, ()>,
|
layer_removal_cs: Arc<tokio::sync::OwnedMutexGuard<()>>,
|
||||||
horizon_cutoff: Lsn,
|
horizon_cutoff: Lsn,
|
||||||
pitr_cutoff: Lsn,
|
pitr_cutoff: Lsn,
|
||||||
retain_lsns: Vec<Lsn>,
|
retain_lsns: Vec<Lsn>,
|
||||||
@@ -3851,7 +3918,7 @@ impl Timeline {
|
|||||||
// delta layers. Image layers can form "stairs" preventing old image from been deleted.
|
// delta layers. Image layers can form "stairs" preventing old image from been deleted.
|
||||||
// But image layers are in any case less sparse than delta layers. Also we need some
|
// But image layers are in any case less sparse than delta layers. Also we need some
|
||||||
// protection from replacing recent image layers with new one after each GC iteration.
|
// protection from replacing recent image layers with new one after each GC iteration.
|
||||||
if l.is_incremental() && !LayerMap::is_l0(&*l) {
|
if self.get_gc_feedback() && l.is_incremental() && !LayerMap::is_l0(&*l) {
|
||||||
wanted_image_layers.add_range(l.get_key_range());
|
wanted_image_layers.add_range(l.get_key_range());
|
||||||
}
|
}
|
||||||
result.layers_not_updated += 1;
|
result.layers_not_updated += 1;
|
||||||
@@ -3884,7 +3951,11 @@ impl Timeline {
|
|||||||
{
|
{
|
||||||
for doomed_layer in layers_to_remove {
|
for doomed_layer in layers_to_remove {
|
||||||
layer_names_to_delete.push(doomed_layer.filename());
|
layer_names_to_delete.push(doomed_layer.filename());
|
||||||
self.delete_historic_layer(layer_removal_cs, doomed_layer, &mut updates)?; // FIXME: schedule succeeded deletions before returning?
|
self.delete_historic_layer(
|
||||||
|
layer_removal_cs.clone(),
|
||||||
|
doomed_layer,
|
||||||
|
&mut updates,
|
||||||
|
)?; // FIXME: schedule succeeded deletions before returning?
|
||||||
result.layers_removed += 1;
|
result.layers_removed += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4056,7 +4127,7 @@ impl Timeline {
|
|||||||
// Does retries + exponential back-off internally.
|
// Does retries + exponential back-off internally.
|
||||||
// When this fails, don't layer further retry attempts here.
|
// When this fails, don't layer further retry attempts here.
|
||||||
let result = remote_client
|
let result = remote_client
|
||||||
.download_layer_file(&remote_layer.file_name, &remote_layer.layer_metadata)
|
.download_layer_file(&remote_layer.filename(), &remote_layer.layer_metadata)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Ok(size) = &result {
|
if let Ok(size) = &result {
|
||||||
|
|||||||
@@ -29,16 +29,14 @@ use crate::tenant::timeline::walreceiver::connection_manager::{
|
|||||||
connection_manager_loop_step, ConnectionManagerState,
|
connection_manager_loop_step, ConnectionManagerState,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::num::NonZeroU64;
|
use std::num::NonZeroU64;
|
||||||
use std::ops::ControlFlow;
|
use std::ops::ControlFlow;
|
||||||
use std::sync::atomic::{self, AtomicBool};
|
use std::sync::Arc;
|
||||||
use std::sync::{Arc, Weak};
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use storage_broker::BrokerClientChannel;
|
use storage_broker::BrokerClientChannel;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tokio::sync::{watch, RwLock};
|
use tokio::sync::watch;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
@@ -62,46 +60,23 @@ pub struct WalReceiverConf {
|
|||||||
|
|
||||||
pub struct WalReceiver {
|
pub struct WalReceiver {
|
||||||
timeline: TenantTimelineId,
|
timeline: TenantTimelineId,
|
||||||
timeline_ref: Weak<Timeline>,
|
manager_status: Arc<std::sync::RwLock<Option<ConnectionManagerStatus>>>,
|
||||||
conf: WalReceiverConf,
|
|
||||||
started: AtomicBool,
|
|
||||||
manager_status: Arc<RwLock<Option<ConnectionManagerStatus>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WalReceiver {
|
impl WalReceiver {
|
||||||
pub fn new(
|
|
||||||
timeline: TenantTimelineId,
|
|
||||||
timeline_ref: Weak<Timeline>,
|
|
||||||
conf: WalReceiverConf,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
timeline,
|
|
||||||
timeline_ref,
|
|
||||||
conf,
|
|
||||||
started: AtomicBool::new(false),
|
|
||||||
manager_status: Arc::new(RwLock::new(None)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start(
|
pub fn start(
|
||||||
&self,
|
timeline: Arc<Timeline>,
|
||||||
ctx: &RequestContext,
|
conf: WalReceiverConf,
|
||||||
mut broker_client: BrokerClientChannel,
|
mut broker_client: BrokerClientChannel,
|
||||||
) -> anyhow::Result<()> {
|
ctx: &RequestContext,
|
||||||
if self.started.load(atomic::Ordering::Acquire) {
|
) -> Self {
|
||||||
anyhow::bail!("Wal receiver is already started");
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeline = self.timeline_ref.upgrade().with_context(|| {
|
|
||||||
format!("walreceiver start on a dropped timeline {}", self.timeline)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let tenant_id = timeline.tenant_id;
|
let tenant_id = timeline.tenant_id;
|
||||||
let timeline_id = timeline.timeline_id;
|
let timeline_id = timeline.timeline_id;
|
||||||
let walreceiver_ctx =
|
let walreceiver_ctx =
|
||||||
ctx.detached_child(TaskKind::WalReceiverManager, DownloadBehavior::Error);
|
ctx.detached_child(TaskKind::WalReceiverManager, DownloadBehavior::Error);
|
||||||
let wal_receiver_conf = self.conf.clone();
|
|
||||||
let loop_status = Arc::clone(&self.manager_status);
|
let loop_status = Arc::new(std::sync::RwLock::new(None));
|
||||||
|
let manager_status = Arc::clone(&loop_status);
|
||||||
task_mgr::spawn(
|
task_mgr::spawn(
|
||||||
WALRECEIVER_RUNTIME.handle(),
|
WALRECEIVER_RUNTIME.handle(),
|
||||||
TaskKind::WalReceiverManager,
|
TaskKind::WalReceiverManager,
|
||||||
@@ -113,7 +88,7 @@ impl WalReceiver {
|
|||||||
info!("WAL receiver manager started, connecting to broker");
|
info!("WAL receiver manager started, connecting to broker");
|
||||||
let mut connection_manager_state = ConnectionManagerState::new(
|
let mut connection_manager_state = ConnectionManagerState::new(
|
||||||
timeline,
|
timeline,
|
||||||
wal_receiver_conf,
|
conf,
|
||||||
);
|
);
|
||||||
loop {
|
loop {
|
||||||
select! {
|
select! {
|
||||||
@@ -137,29 +112,29 @@ impl WalReceiver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connection_manager_state.shutdown().await;
|
connection_manager_state.shutdown().await;
|
||||||
*loop_status.write().await = None;
|
*loop_status.write().unwrap() = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
.instrument(info_span!(parent: None, "wal_connection_manager", tenant = %tenant_id, timeline = %timeline_id))
|
.instrument(info_span!(parent: None, "wal_connection_manager", tenant = %tenant_id, timeline = %timeline_id))
|
||||||
);
|
);
|
||||||
|
|
||||||
self.started.store(true, atomic::Ordering::Release);
|
Self {
|
||||||
|
timeline: TenantTimelineId::new(tenant_id, timeline_id),
|
||||||
Ok(())
|
manager_status,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stop(&self) {
|
pub async fn stop(self) {
|
||||||
task_mgr::shutdown_tasks(
|
task_mgr::shutdown_tasks(
|
||||||
Some(TaskKind::WalReceiverManager),
|
Some(TaskKind::WalReceiverManager),
|
||||||
Some(self.timeline.tenant_id),
|
Some(self.timeline.tenant_id),
|
||||||
Some(self.timeline.timeline_id),
|
Some(self.timeline.timeline_id),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
self.started.store(false, atomic::Ordering::Release);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn status(&self) -> Option<ConnectionManagerStatus> {
|
pub(super) fn status(&self) -> Option<ConnectionManagerStatus> {
|
||||||
self.manager_status.read().await.clone()
|
self.manager_status.read().unwrap().clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ use storage_broker::proto::TenantTimelineId as ProtoTenantTimelineId;
|
|||||||
use storage_broker::BrokerClientChannel;
|
use storage_broker::BrokerClientChannel;
|
||||||
use storage_broker::Streaming;
|
use storage_broker::Streaming;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
use crate::{exponential_backoff, DEFAULT_BASE_BACKOFF_SECONDS, DEFAULT_MAX_BACKOFF_SECONDS};
|
use crate::{exponential_backoff, DEFAULT_BASE_BACKOFF_SECONDS, DEFAULT_MAX_BACKOFF_SECONDS};
|
||||||
@@ -48,7 +47,7 @@ pub(super) async fn connection_manager_loop_step(
|
|||||||
broker_client: &mut BrokerClientChannel,
|
broker_client: &mut BrokerClientChannel,
|
||||||
connection_manager_state: &mut ConnectionManagerState,
|
connection_manager_state: &mut ConnectionManagerState,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
manager_status: &RwLock<Option<ConnectionManagerStatus>>,
|
manager_status: &std::sync::RwLock<Option<ConnectionManagerStatus>>,
|
||||||
) -> ControlFlow<(), ()> {
|
) -> ControlFlow<(), ()> {
|
||||||
match connection_manager_state
|
match connection_manager_state
|
||||||
.timeline
|
.timeline
|
||||||
@@ -195,7 +194,7 @@ pub(super) async fn connection_manager_loop_step(
|
|||||||
.change_connection(new_candidate, ctx)
|
.change_connection(new_candidate, ctx)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
*manager_status.write().await = Some(connection_manager_state.manager_status());
|
*manager_status.write().unwrap() = Some(connection_manager_state.manager_status());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1309,9 +1308,8 @@ mod tests {
|
|||||||
async fn dummy_state(harness: &TenantHarness<'_>) -> ConnectionManagerState {
|
async fn dummy_state(harness: &TenantHarness<'_>) -> ConnectionManagerState {
|
||||||
let (tenant, ctx) = harness.load().await;
|
let (tenant, ctx) = harness.load().await;
|
||||||
let timeline = tenant
|
let timeline = tenant
|
||||||
.create_empty_timeline(TIMELINE_ID, Lsn(0), crate::DEFAULT_PG_VERSION, &ctx)
|
.create_test_timeline(TIMELINE_ID, Lsn(0), crate::DEFAULT_PG_VERSION, &ctx)
|
||||||
.expect("Failed to create an empty timeline for dummy wal connection manager");
|
.expect("Failed to create an empty timeline for dummy wal connection manager");
|
||||||
let timeline = timeline.initialize(&ctx).unwrap();
|
|
||||||
|
|
||||||
ConnectionManagerState {
|
ConnectionManagerState {
|
||||||
id: TenantTimelineId {
|
id: TenantTimelineId {
|
||||||
|
|||||||
+3
-1
@@ -11,10 +11,12 @@ OBJS = \
|
|||||||
pagestore_smgr.o \
|
pagestore_smgr.o \
|
||||||
relsize_cache.o \
|
relsize_cache.o \
|
||||||
walproposer.o \
|
walproposer.o \
|
||||||
walproposer_utils.o
|
walproposer_utils.o \
|
||||||
|
control_plane_connector.o
|
||||||
|
|
||||||
PG_CPPFLAGS = -I$(libpq_srcdir)
|
PG_CPPFLAGS = -I$(libpq_srcdir)
|
||||||
SHLIB_LINK_INTERNAL = $(libpq)
|
SHLIB_LINK_INTERNAL = $(libpq)
|
||||||
|
SHLIB_LINK = -lcurl
|
||||||
|
|
||||||
EXTENSION = neon
|
EXTENSION = neon
|
||||||
DATA = neon--1.0.sql
|
DATA = neon--1.0.sql
|
||||||
|
|||||||
@@ -0,0 +1,830 @@
|
|||||||
|
/*-------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* control_plane_connector.c
|
||||||
|
* Captures updates to roles/databases using ProcessUtility_hook and
|
||||||
|
* sends them to the control ProcessUtility_hook. The changes are sent
|
||||||
|
* via HTTP to the URL specified by the GUC neon.console_url when the
|
||||||
|
* transaction commits. Forwarding may be disabled temporarily by
|
||||||
|
* setting neon.forward_ddl to false.
|
||||||
|
*
|
||||||
|
* Currently, the transaction may abort AFTER
|
||||||
|
* changes have already been forwarded, and that case is not handled.
|
||||||
|
* Subtransactions are handled using a stack of hash tables, which
|
||||||
|
* accumulate changes. On subtransaction commit, the top of the stack
|
||||||
|
* is merged with the table below it.
|
||||||
|
*
|
||||||
|
* IDENTIFICATION
|
||||||
|
* contrib/neon/control_plane_connector.c
|
||||||
|
*
|
||||||
|
*-------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
#include "postgres.h"
|
||||||
|
#include "tcop/pquery.h"
|
||||||
|
#include "tcop/utility.h"
|
||||||
|
#include "access/xact.h"
|
||||||
|
#include "utils/hsearch.h"
|
||||||
|
#include "utils/memutils.h"
|
||||||
|
#include "commands/defrem.h"
|
||||||
|
#include "miscadmin.h"
|
||||||
|
#include "utils/acl.h"
|
||||||
|
#include "fmgr.h"
|
||||||
|
#include "utils/guc.h"
|
||||||
|
#include "port.h"
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include "utils/jsonb.h"
|
||||||
|
|
||||||
|
static ProcessUtility_hook_type PreviousProcessUtilityHook = NULL;
|
||||||
|
|
||||||
|
/* GUCs */
|
||||||
|
static char *ConsoleURL = NULL;
|
||||||
|
static bool ForwardDDL = true;
|
||||||
|
|
||||||
|
/* Curl structures for sending the HTTP requests */
|
||||||
|
static CURL * CurlHandle;
|
||||||
|
static struct curl_slist *ContentHeader = NULL;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* CURL docs say that this buffer must exist until we call curl_easy_cleanup
|
||||||
|
* (which we never do), so we make this a static
|
||||||
|
*/
|
||||||
|
static char CurlErrorBuf[CURL_ERROR_SIZE];
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
Op_Set, /* An upsert: Either a creation or an alter */
|
||||||
|
Op_Delete,
|
||||||
|
} OpType;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
char name[NAMEDATALEN];
|
||||||
|
Oid owner;
|
||||||
|
char old_name[NAMEDATALEN];
|
||||||
|
OpType type;
|
||||||
|
} DbEntry;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
char name[NAMEDATALEN];
|
||||||
|
char old_name[NAMEDATALEN];
|
||||||
|
const char *password;
|
||||||
|
OpType type;
|
||||||
|
} RoleEntry;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We keep one of these for each subtransaction in a stack. When a subtransaction
|
||||||
|
* commits, we merge the top of the stack into the table below it. It is allocated in the
|
||||||
|
* subtransaction's context.
|
||||||
|
*/
|
||||||
|
typedef struct DdlHashTable
|
||||||
|
{
|
||||||
|
struct DdlHashTable *prev_table;
|
||||||
|
HTAB *db_table;
|
||||||
|
HTAB *role_table;
|
||||||
|
} DdlHashTable;
|
||||||
|
|
||||||
|
static DdlHashTable RootTable;
|
||||||
|
static DdlHashTable * CurrentDdlTable = &RootTable;
|
||||||
|
|
||||||
|
static void
|
||||||
|
PushKeyValue(JsonbParseState **state, char *key, char *value)
|
||||||
|
{
|
||||||
|
JsonbValue k,
|
||||||
|
v;
|
||||||
|
|
||||||
|
k.type = jbvString;
|
||||||
|
k.val.string.len = strlen(key);
|
||||||
|
k.val.string.val = key;
|
||||||
|
v.type = jbvString;
|
||||||
|
v.val.string.len = strlen(value);
|
||||||
|
v.val.string.val = value;
|
||||||
|
pushJsonbValue(state, WJB_KEY, &k);
|
||||||
|
pushJsonbValue(state, WJB_VALUE, &v);
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *
|
||||||
|
ConstructDeltaMessage()
|
||||||
|
{
|
||||||
|
JsonbParseState *state = NULL;
|
||||||
|
|
||||||
|
pushJsonbValue(&state, WJB_BEGIN_OBJECT, NULL);
|
||||||
|
if (RootTable.db_table)
|
||||||
|
{
|
||||||
|
JsonbValue dbs;
|
||||||
|
|
||||||
|
dbs.type = jbvString;
|
||||||
|
dbs.val.string.val = "dbs";
|
||||||
|
dbs.val.string.len = strlen(dbs.val.string.val);
|
||||||
|
pushJsonbValue(&state, WJB_KEY, &dbs);
|
||||||
|
pushJsonbValue(&state, WJB_BEGIN_ARRAY, NULL);
|
||||||
|
|
||||||
|
HASH_SEQ_STATUS status;
|
||||||
|
DbEntry *entry;
|
||||||
|
|
||||||
|
hash_seq_init(&status, RootTable.db_table);
|
||||||
|
while ((entry = hash_seq_search(&status)) != NULL)
|
||||||
|
{
|
||||||
|
pushJsonbValue(&state, WJB_BEGIN_OBJECT, NULL);
|
||||||
|
PushKeyValue(&state, "op", entry->type == Op_Set ? "set" : "del");
|
||||||
|
PushKeyValue(&state, "name", entry->name);
|
||||||
|
if (entry->owner != InvalidOid)
|
||||||
|
{
|
||||||
|
PushKeyValue(&state, "owner", GetUserNameFromId(entry->owner, false));
|
||||||
|
}
|
||||||
|
if (entry->old_name[0] != '\0')
|
||||||
|
{
|
||||||
|
PushKeyValue(&state, "old_name", entry->old_name);
|
||||||
|
}
|
||||||
|
pushJsonbValue(&state, WJB_END_OBJECT, NULL);
|
||||||
|
}
|
||||||
|
pushJsonbValue(&state, WJB_END_ARRAY, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RootTable.role_table)
|
||||||
|
{
|
||||||
|
JsonbValue roles;
|
||||||
|
|
||||||
|
roles.type = jbvString;
|
||||||
|
roles.val.string.val = "roles";
|
||||||
|
roles.val.string.len = strlen(roles.val.string.val);
|
||||||
|
pushJsonbValue(&state, WJB_KEY, &roles);
|
||||||
|
pushJsonbValue(&state, WJB_BEGIN_ARRAY, NULL);
|
||||||
|
|
||||||
|
HASH_SEQ_STATUS status;
|
||||||
|
RoleEntry *entry;
|
||||||
|
|
||||||
|
hash_seq_init(&status, RootTable.role_table);
|
||||||
|
while ((entry = hash_seq_search(&status)) != NULL)
|
||||||
|
{
|
||||||
|
pushJsonbValue(&state, WJB_BEGIN_OBJECT, NULL);
|
||||||
|
PushKeyValue(&state, "op", entry->type == Op_Set ? "set" : "del");
|
||||||
|
PushKeyValue(&state, "name", entry->name);
|
||||||
|
if (entry->password)
|
||||||
|
{
|
||||||
|
PushKeyValue(&state, "password", (char *) entry->password);
|
||||||
|
}
|
||||||
|
if (entry->old_name[0] != '\0')
|
||||||
|
{
|
||||||
|
PushKeyValue(&state, "old_name", entry->old_name);
|
||||||
|
}
|
||||||
|
pushJsonbValue(&state, WJB_END_OBJECT, NULL);
|
||||||
|
}
|
||||||
|
pushJsonbValue(&state, WJB_END_ARRAY, NULL);
|
||||||
|
}
|
||||||
|
JsonbValue *result = pushJsonbValue(&state, WJB_END_OBJECT, NULL);
|
||||||
|
Jsonb *jsonb = JsonbValueToJsonb(result);
|
||||||
|
|
||||||
|
return JsonbToCString(NULL, &jsonb->root, 0 /* estimated_len */ );
|
||||||
|
}
|
||||||
|
|
||||||
|
#define ERROR_SIZE 1024
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
char str[ERROR_SIZE];
|
||||||
|
size_t size;
|
||||||
|
} ErrorString;
|
||||||
|
|
||||||
|
static size_t
|
||||||
|
ErrorWriteCallback(char *ptr, size_t size, size_t nmemb, void *userdata)
|
||||||
|
{
|
||||||
|
/* Docs say size is always 1 */
|
||||||
|
ErrorString *str = userdata;
|
||||||
|
|
||||||
|
size_t to_write = nmemb;
|
||||||
|
|
||||||
|
/* +1 for null terminator */
|
||||||
|
if (str->size + nmemb + 1 >= ERROR_SIZE)
|
||||||
|
to_write = ERROR_SIZE - str->size - 1;
|
||||||
|
|
||||||
|
/* Ignore everyrthing past the first ERROR_SIZE bytes */
|
||||||
|
if (to_write == 0)
|
||||||
|
return nmemb;
|
||||||
|
memcpy(str->str + str->size, ptr, to_write);
|
||||||
|
str->size += to_write;
|
||||||
|
str->str[str->size] = '\0';
|
||||||
|
return nmemb;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
SendDeltasToControlPlane()
|
||||||
|
{
|
||||||
|
if (!RootTable.db_table && !RootTable.role_table)
|
||||||
|
return;
|
||||||
|
if (!ConsoleURL)
|
||||||
|
{
|
||||||
|
elog(LOG, "ConsoleURL not set, skipping forwarding");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ForwardDDL)
|
||||||
|
return;
|
||||||
|
|
||||||
|
char *message = ConstructDeltaMessage();
|
||||||
|
ErrorString str = {};
|
||||||
|
|
||||||
|
curl_easy_setopt(CurlHandle, CURLOPT_CUSTOMREQUEST, "PATCH");
|
||||||
|
curl_easy_setopt(CurlHandle, CURLOPT_HTTPHEADER, ContentHeader);
|
||||||
|
curl_easy_setopt(CurlHandle, CURLOPT_POSTFIELDS, message);
|
||||||
|
curl_easy_setopt(CurlHandle, CURLOPT_URL, ConsoleURL);
|
||||||
|
curl_easy_setopt(CurlHandle, CURLOPT_ERRORBUFFER, CurlErrorBuf);
|
||||||
|
curl_easy_setopt(CurlHandle, CURLOPT_TIMEOUT, 3L /* seconds */ );
|
||||||
|
curl_easy_setopt(CurlHandle, CURLOPT_WRITEDATA, &str);
|
||||||
|
curl_easy_setopt(CurlHandle, CURLOPT_WRITEFUNCTION, ErrorWriteCallback);
|
||||||
|
|
||||||
|
const int num_retries = 5;
|
||||||
|
int curl_status;
|
||||||
|
|
||||||
|
for (int i = 0; i < num_retries; i++)
|
||||||
|
{
|
||||||
|
if ((curl_status = curl_easy_perform(CurlHandle)) == 0)
|
||||||
|
break;
|
||||||
|
elog(LOG, "Curl request failed on attempt %d: %s", i, CurlErrorBuf);
|
||||||
|
pg_usleep(1000 * 1000);
|
||||||
|
}
|
||||||
|
if (curl_status != 0)
|
||||||
|
{
|
||||||
|
elog(ERROR, "Failed to perform curl request: %s", CurlErrorBuf);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
long response_code;
|
||||||
|
|
||||||
|
if (curl_easy_getinfo(CurlHandle, CURLINFO_RESPONSE_CODE, &response_code) != CURLE_UNKNOWN_OPTION)
|
||||||
|
{
|
||||||
|
bool error_exists = str.size != 0;
|
||||||
|
|
||||||
|
if (response_code != 200)
|
||||||
|
{
|
||||||
|
if (error_exists)
|
||||||
|
{
|
||||||
|
elog(ERROR,
|
||||||
|
"Received HTTP code %ld from control plane: %s",
|
||||||
|
response_code,
|
||||||
|
str.str);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
elog(ERROR,
|
||||||
|
"Received HTTP code %ld from control plane",
|
||||||
|
response_code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
InitDbTableIfNeeded()
|
||||||
|
{
|
||||||
|
if (!CurrentDdlTable->db_table)
|
||||||
|
{
|
||||||
|
HASHCTL db_ctl = {};
|
||||||
|
|
||||||
|
db_ctl.keysize = NAMEDATALEN;
|
||||||
|
db_ctl.entrysize = sizeof(DbEntry);
|
||||||
|
db_ctl.hcxt = CurTransactionContext;
|
||||||
|
CurrentDdlTable->db_table = hash_create(
|
||||||
|
"Dbs Created",
|
||||||
|
4,
|
||||||
|
&db_ctl,
|
||||||
|
HASH_ELEM | HASH_STRINGS | HASH_CONTEXT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
InitRoleTableIfNeeded()
|
||||||
|
{
|
||||||
|
if (!CurrentDdlTable->role_table)
|
||||||
|
{
|
||||||
|
HASHCTL role_ctl = {};
|
||||||
|
|
||||||
|
role_ctl.keysize = NAMEDATALEN;
|
||||||
|
role_ctl.entrysize = sizeof(RoleEntry);
|
||||||
|
role_ctl.hcxt = CurTransactionContext;
|
||||||
|
CurrentDdlTable->role_table = hash_create(
|
||||||
|
"Roles Created",
|
||||||
|
4,
|
||||||
|
&role_ctl,
|
||||||
|
HASH_ELEM | HASH_STRINGS | HASH_CONTEXT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
PushTable()
|
||||||
|
{
|
||||||
|
DdlHashTable *new_table = MemoryContextAlloc(CurTransactionContext, sizeof(DdlHashTable));
|
||||||
|
|
||||||
|
new_table->prev_table = CurrentDdlTable;
|
||||||
|
new_table->role_table = NULL;
|
||||||
|
new_table->db_table = NULL;
|
||||||
|
CurrentDdlTable = new_table;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
MergeTable()
|
||||||
|
{
|
||||||
|
DdlHashTable *old_table = CurrentDdlTable;
|
||||||
|
|
||||||
|
CurrentDdlTable = old_table->prev_table;
|
||||||
|
|
||||||
|
if (old_table->db_table)
|
||||||
|
{
|
||||||
|
InitDbTableIfNeeded();
|
||||||
|
DbEntry *entry;
|
||||||
|
HASH_SEQ_STATUS status;
|
||||||
|
|
||||||
|
hash_seq_init(&status, old_table->db_table);
|
||||||
|
while ((entry = hash_seq_search(&status)) != NULL)
|
||||||
|
{
|
||||||
|
DbEntry *to_write = hash_search(
|
||||||
|
CurrentDdlTable->db_table,
|
||||||
|
entry->name,
|
||||||
|
HASH_ENTER,
|
||||||
|
NULL);
|
||||||
|
|
||||||
|
to_write->type = entry->type;
|
||||||
|
if (entry->owner != InvalidOid)
|
||||||
|
to_write->owner = entry->owner;
|
||||||
|
strlcpy(to_write->old_name, entry->old_name, NAMEDATALEN);
|
||||||
|
if (entry->old_name[0] != '\0')
|
||||||
|
{
|
||||||
|
bool found_old = false;
|
||||||
|
DbEntry *old = hash_search(
|
||||||
|
CurrentDdlTable->db_table,
|
||||||
|
entry->old_name,
|
||||||
|
HASH_FIND,
|
||||||
|
&found_old);
|
||||||
|
|
||||||
|
if (found_old)
|
||||||
|
{
|
||||||
|
if (old->old_name[0] != '\0')
|
||||||
|
strlcpy(to_write->old_name, old->old_name, NAMEDATALEN);
|
||||||
|
else
|
||||||
|
strlcpy(to_write->old_name, entry->old_name, NAMEDATALEN);
|
||||||
|
hash_search(
|
||||||
|
CurrentDdlTable->db_table,
|
||||||
|
entry->old_name,
|
||||||
|
HASH_REMOVE,
|
||||||
|
NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hash_destroy(old_table->db_table);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (old_table->role_table)
|
||||||
|
{
|
||||||
|
InitRoleTableIfNeeded();
|
||||||
|
RoleEntry *entry;
|
||||||
|
HASH_SEQ_STATUS status;
|
||||||
|
|
||||||
|
hash_seq_init(&status, old_table->role_table);
|
||||||
|
while ((entry = hash_seq_search(&status)) != NULL)
|
||||||
|
{
|
||||||
|
RoleEntry *to_write = hash_search(
|
||||||
|
CurrentDdlTable->role_table,
|
||||||
|
entry->name,
|
||||||
|
HASH_ENTER,
|
||||||
|
NULL);
|
||||||
|
|
||||||
|
to_write->type = entry->type;
|
||||||
|
if (entry->password)
|
||||||
|
to_write->password = entry->password;
|
||||||
|
strlcpy(to_write->old_name, entry->old_name, NAMEDATALEN);
|
||||||
|
if (entry->old_name[0] != '\0')
|
||||||
|
{
|
||||||
|
bool found_old = false;
|
||||||
|
RoleEntry *old = hash_search(
|
||||||
|
CurrentDdlTable->role_table,
|
||||||
|
entry->old_name,
|
||||||
|
HASH_FIND,
|
||||||
|
&found_old);
|
||||||
|
|
||||||
|
if (found_old)
|
||||||
|
{
|
||||||
|
if (old->old_name[0] != '\0')
|
||||||
|
strlcpy(to_write->old_name, old->old_name, NAMEDATALEN);
|
||||||
|
else
|
||||||
|
strlcpy(to_write->old_name, entry->old_name, NAMEDATALEN);
|
||||||
|
hash_search(CurrentDdlTable->role_table,
|
||||||
|
entry->old_name,
|
||||||
|
HASH_REMOVE,
|
||||||
|
NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hash_destroy(old_table->role_table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
PopTable()
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Current table gets freed because it is allocated in aborted
|
||||||
|
* subtransaction's memory context.
|
||||||
|
*/
|
||||||
|
CurrentDdlTable = CurrentDdlTable->prev_table;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
NeonSubXactCallback(
|
||||||
|
SubXactEvent event,
|
||||||
|
SubTransactionId mySubid,
|
||||||
|
SubTransactionId parentSubid,
|
||||||
|
void *arg)
|
||||||
|
{
|
||||||
|
switch (event)
|
||||||
|
{
|
||||||
|
case SUBXACT_EVENT_START_SUB:
|
||||||
|
return PushTable();
|
||||||
|
case SUBXACT_EVENT_COMMIT_SUB:
|
||||||
|
return MergeTable();
|
||||||
|
case SUBXACT_EVENT_ABORT_SUB:
|
||||||
|
return PopTable();
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
NeonXactCallback(XactEvent event, void *arg)
|
||||||
|
{
|
||||||
|
if (event == XACT_EVENT_PRE_COMMIT || event == XACT_EVENT_PARALLEL_PRE_COMMIT)
|
||||||
|
{
|
||||||
|
SendDeltasToControlPlane();
|
||||||
|
}
|
||||||
|
RootTable.role_table = NULL;
|
||||||
|
RootTable.db_table = NULL;
|
||||||
|
Assert(CurrentDdlTable == &RootTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
HandleCreateDb(CreatedbStmt *stmt)
|
||||||
|
{
|
||||||
|
InitDbTableIfNeeded();
|
||||||
|
DefElem *downer = NULL;
|
||||||
|
ListCell *option;
|
||||||
|
|
||||||
|
foreach(option, stmt->options)
|
||||||
|
{
|
||||||
|
DefElem *defel = lfirst(option);
|
||||||
|
|
||||||
|
if (strcmp(defel->defname, "owner") == 0)
|
||||||
|
downer = defel;
|
||||||
|
}
|
||||||
|
bool found = false;
|
||||||
|
DbEntry *entry = hash_search(
|
||||||
|
CurrentDdlTable->db_table,
|
||||||
|
stmt->dbname,
|
||||||
|
HASH_ENTER,
|
||||||
|
&found);
|
||||||
|
|
||||||
|
if (!found)
|
||||||
|
memset(entry->old_name, 0, sizeof(entry->old_name));
|
||||||
|
|
||||||
|
entry->type = Op_Set;
|
||||||
|
if (downer && downer->arg)
|
||||||
|
entry->owner = get_role_oid(defGetString(downer), false);
|
||||||
|
else
|
||||||
|
entry->owner = GetUserId();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
HandleAlterOwner(AlterOwnerStmt *stmt)
|
||||||
|
{
|
||||||
|
if (stmt->objectType != OBJECT_DATABASE)
|
||||||
|
return;
|
||||||
|
InitDbTableIfNeeded();
|
||||||
|
const char *name = strVal(stmt->object);
|
||||||
|
bool found = false;
|
||||||
|
DbEntry *entry = hash_search(
|
||||||
|
CurrentDdlTable->db_table,
|
||||||
|
name,
|
||||||
|
HASH_ENTER,
|
||||||
|
&found);
|
||||||
|
|
||||||
|
if (!found)
|
||||||
|
memset(entry->old_name, 0, sizeof(entry->old_name));
|
||||||
|
|
||||||
|
entry->owner = get_role_oid(get_rolespec_name(stmt->newowner), false);
|
||||||
|
entry->type = Op_Set;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
HandleDbRename(RenameStmt *stmt)
|
||||||
|
{
|
||||||
|
Assert(stmt->renameType == OBJECT_DATABASE);
|
||||||
|
InitDbTableIfNeeded();
|
||||||
|
bool found = false;
|
||||||
|
DbEntry *entry = hash_search(
|
||||||
|
CurrentDdlTable->db_table,
|
||||||
|
stmt->subname,
|
||||||
|
HASH_FIND,
|
||||||
|
&found);
|
||||||
|
DbEntry *entry_for_new_name = hash_search(
|
||||||
|
CurrentDdlTable->db_table,
|
||||||
|
stmt->newname,
|
||||||
|
HASH_ENTER,
|
||||||
|
NULL);
|
||||||
|
|
||||||
|
entry_for_new_name->type = Op_Set;
|
||||||
|
if (found)
|
||||||
|
{
|
||||||
|
if (entry->old_name[0] != '\0')
|
||||||
|
strlcpy(entry_for_new_name->old_name, entry->old_name, NAMEDATALEN);
|
||||||
|
else
|
||||||
|
strlcpy(entry_for_new_name->old_name, entry->name, NAMEDATALEN);
|
||||||
|
entry_for_new_name->owner = entry->owner;
|
||||||
|
hash_search(
|
||||||
|
CurrentDdlTable->db_table,
|
||||||
|
stmt->subname,
|
||||||
|
HASH_REMOVE,
|
||||||
|
NULL);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
strlcpy(entry_for_new_name->old_name, stmt->subname, NAMEDATALEN);
|
||||||
|
entry_for_new_name->owner = InvalidOid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
HandleDropDb(DropdbStmt *stmt)
|
||||||
|
{
|
||||||
|
InitDbTableIfNeeded();
|
||||||
|
bool found = false;
|
||||||
|
DbEntry *entry = hash_search(
|
||||||
|
CurrentDdlTable->db_table,
|
||||||
|
stmt->dbname,
|
||||||
|
HASH_ENTER,
|
||||||
|
&found);
|
||||||
|
|
||||||
|
entry->type = Op_Delete;
|
||||||
|
entry->owner = InvalidOid;
|
||||||
|
if (!found)
|
||||||
|
memset(entry->old_name, 0, sizeof(entry->old_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
HandleCreateRole(CreateRoleStmt *stmt)
|
||||||
|
{
|
||||||
|
InitRoleTableIfNeeded();
|
||||||
|
bool found = false;
|
||||||
|
RoleEntry *entry = hash_search(
|
||||||
|
CurrentDdlTable->role_table,
|
||||||
|
stmt->role,
|
||||||
|
HASH_ENTER,
|
||||||
|
&found);
|
||||||
|
DefElem *dpass = NULL;
|
||||||
|
ListCell *option;
|
||||||
|
|
||||||
|
foreach(option, stmt->options)
|
||||||
|
{
|
||||||
|
DefElem *defel = lfirst(option);
|
||||||
|
|
||||||
|
if (strcmp(defel->defname, "password") == 0)
|
||||||
|
dpass = defel;
|
||||||
|
}
|
||||||
|
if (!found)
|
||||||
|
memset(entry->old_name, 0, sizeof(entry->old_name));
|
||||||
|
if (dpass && dpass->arg)
|
||||||
|
entry->password = MemoryContextStrdup(CurTransactionContext, strVal(dpass->arg));
|
||||||
|
else
|
||||||
|
entry->password = NULL;
|
||||||
|
entry->type = Op_Set;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
HandleAlterRole(AlterRoleStmt *stmt)
|
||||||
|
{
|
||||||
|
InitRoleTableIfNeeded();
|
||||||
|
DefElem *dpass = NULL;
|
||||||
|
ListCell *option;
|
||||||
|
|
||||||
|
foreach(option, stmt->options)
|
||||||
|
{
|
||||||
|
DefElem *defel = lfirst(option);
|
||||||
|
|
||||||
|
if (strcmp(defel->defname, "password") == 0)
|
||||||
|
dpass = defel;
|
||||||
|
}
|
||||||
|
/* We only care about updates to the password */
|
||||||
|
if (!dpass)
|
||||||
|
return;
|
||||||
|
bool found = false;
|
||||||
|
RoleEntry *entry = hash_search(
|
||||||
|
CurrentDdlTable->role_table,
|
||||||
|
stmt->role->rolename,
|
||||||
|
HASH_ENTER,
|
||||||
|
&found);
|
||||||
|
|
||||||
|
if (!found)
|
||||||
|
memset(entry->old_name, 0, sizeof(entry->old_name));
|
||||||
|
if (dpass->arg)
|
||||||
|
entry->password = MemoryContextStrdup(CurTransactionContext, strVal(dpass->arg));
|
||||||
|
else
|
||||||
|
entry->password = NULL;
|
||||||
|
entry->type = Op_Set;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
HandleRoleRename(RenameStmt *stmt)
|
||||||
|
{
|
||||||
|
InitRoleTableIfNeeded();
|
||||||
|
Assert(stmt->renameType == OBJECT_ROLE);
|
||||||
|
bool found = false;
|
||||||
|
RoleEntry *entry = hash_search(
|
||||||
|
CurrentDdlTable->role_table,
|
||||||
|
stmt->subname,
|
||||||
|
HASH_FIND,
|
||||||
|
&found);
|
||||||
|
|
||||||
|
RoleEntry *entry_for_new_name = hash_search(
|
||||||
|
CurrentDdlTable->role_table,
|
||||||
|
stmt->newname,
|
||||||
|
HASH_ENTER,
|
||||||
|
NULL);
|
||||||
|
|
||||||
|
entry_for_new_name->type = Op_Set;
|
||||||
|
if (found)
|
||||||
|
{
|
||||||
|
if (entry->old_name[0] != '\0')
|
||||||
|
strlcpy(entry_for_new_name->old_name, entry->old_name, NAMEDATALEN);
|
||||||
|
else
|
||||||
|
strlcpy(entry_for_new_name->old_name, entry->name, NAMEDATALEN);
|
||||||
|
entry_for_new_name->password = entry->password;
|
||||||
|
hash_search(
|
||||||
|
CurrentDdlTable->role_table,
|
||||||
|
entry->name,
|
||||||
|
HASH_REMOVE,
|
||||||
|
NULL);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
strlcpy(entry_for_new_name->old_name, stmt->subname, NAMEDATALEN);
|
||||||
|
entry_for_new_name->password = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
HandleDropRole(DropRoleStmt *stmt)
|
||||||
|
{
|
||||||
|
InitRoleTableIfNeeded();
|
||||||
|
ListCell *item;
|
||||||
|
|
||||||
|
foreach(item, stmt->roles)
|
||||||
|
{
|
||||||
|
RoleSpec *spec = lfirst(item);
|
||||||
|
bool found = false;
|
||||||
|
RoleEntry *entry = hash_search(
|
||||||
|
CurrentDdlTable->role_table,
|
||||||
|
spec->rolename,
|
||||||
|
HASH_ENTER,
|
||||||
|
&found);
|
||||||
|
|
||||||
|
entry->type = Op_Delete;
|
||||||
|
entry->password = NULL;
|
||||||
|
if (!found)
|
||||||
|
memset(entry->old_name, 0, sizeof(entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
HandleRename(RenameStmt *stmt)
|
||||||
|
{
|
||||||
|
if (stmt->renameType == OBJECT_DATABASE)
|
||||||
|
return HandleDbRename(stmt);
|
||||||
|
else if (stmt->renameType == OBJECT_ROLE)
|
||||||
|
return HandleRoleRename(stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
NeonProcessUtility(
|
||||||
|
PlannedStmt *pstmt,
|
||||||
|
const char *queryString,
|
||||||
|
bool readOnlyTree,
|
||||||
|
ProcessUtilityContext context,
|
||||||
|
ParamListInfo params,
|
||||||
|
QueryEnvironment *queryEnv,
|
||||||
|
DestReceiver *dest,
|
||||||
|
QueryCompletion *qc)
|
||||||
|
{
|
||||||
|
Node *parseTree = pstmt->utilityStmt;
|
||||||
|
|
||||||
|
switch (nodeTag(parseTree))
|
||||||
|
{
|
||||||
|
case T_CreatedbStmt:
|
||||||
|
HandleCreateDb(castNode(CreatedbStmt, parseTree));
|
||||||
|
break;
|
||||||
|
case T_AlterOwnerStmt:
|
||||||
|
HandleAlterOwner(castNode(AlterOwnerStmt, parseTree));
|
||||||
|
break;
|
||||||
|
case T_RenameStmt:
|
||||||
|
HandleRename(castNode(RenameStmt, parseTree));
|
||||||
|
break;
|
||||||
|
case T_DropdbStmt:
|
||||||
|
HandleDropDb(castNode(DropdbStmt, parseTree));
|
||||||
|
break;
|
||||||
|
case T_CreateRoleStmt:
|
||||||
|
HandleCreateRole(castNode(CreateRoleStmt, parseTree));
|
||||||
|
break;
|
||||||
|
case T_AlterRoleStmt:
|
||||||
|
HandleAlterRole(castNode(AlterRoleStmt, parseTree));
|
||||||
|
break;
|
||||||
|
case T_DropRoleStmt:
|
||||||
|
HandleDropRole(castNode(DropRoleStmt, parseTree));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PreviousProcessUtilityHook)
|
||||||
|
{
|
||||||
|
PreviousProcessUtilityHook(
|
||||||
|
pstmt,
|
||||||
|
queryString,
|
||||||
|
readOnlyTree,
|
||||||
|
context,
|
||||||
|
params,
|
||||||
|
queryEnv,
|
||||||
|
dest,
|
||||||
|
qc);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
standard_ProcessUtility(
|
||||||
|
pstmt,
|
||||||
|
queryString,
|
||||||
|
readOnlyTree,
|
||||||
|
context,
|
||||||
|
params,
|
||||||
|
queryEnv,
|
||||||
|
dest,
|
||||||
|
qc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern void
|
||||||
|
InitControlPlaneConnector()
|
||||||
|
{
|
||||||
|
PreviousProcessUtilityHook = ProcessUtility_hook;
|
||||||
|
ProcessUtility_hook = NeonProcessUtility;
|
||||||
|
RegisterXactCallback(NeonXactCallback, NULL);
|
||||||
|
RegisterSubXactCallback(NeonSubXactCallback, NULL);
|
||||||
|
|
||||||
|
DefineCustomStringVariable(
|
||||||
|
"neon.console_url",
|
||||||
|
"URL of the Neon Console, which will be forwarded changes to dbs and roles",
|
||||||
|
NULL,
|
||||||
|
&ConsoleURL,
|
||||||
|
NULL,
|
||||||
|
PGC_POSTMASTER,
|
||||||
|
0,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL);
|
||||||
|
|
||||||
|
DefineCustomBoolVariable(
|
||||||
|
"neon.forward_ddl",
|
||||||
|
"Controls whether to forward DDL to the control plane",
|
||||||
|
NULL,
|
||||||
|
&ForwardDDL,
|
||||||
|
true,
|
||||||
|
PGC_SUSET,
|
||||||
|
0,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL);
|
||||||
|
|
||||||
|
const char *jwt_token = getenv("NEON_CONTROL_PLANE_TOKEN");
|
||||||
|
|
||||||
|
if (!jwt_token)
|
||||||
|
{
|
||||||
|
elog(LOG, "Missing NEON_CONTROL_PLANE_TOKEN environment variable, forwarding will not be authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curl_global_init(CURL_GLOBAL_DEFAULT))
|
||||||
|
{
|
||||||
|
elog(ERROR, "Failed to initialize curl");
|
||||||
|
}
|
||||||
|
if ((CurlHandle = curl_easy_init()) == NULL)
|
||||||
|
{
|
||||||
|
elog(ERROR, "Failed to initialize curl handle");
|
||||||
|
}
|
||||||
|
if ((ContentHeader = curl_slist_append(ContentHeader, "Content-Type: application/json")) == NULL)
|
||||||
|
{
|
||||||
|
elog(ERROR, "Failed to initialize content header");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jwt_token)
|
||||||
|
{
|
||||||
|
char auth_header[8192];
|
||||||
|
|
||||||
|
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", jwt_token);
|
||||||
|
if ((ContentHeader = curl_slist_append(ContentHeader, auth_header)) == NULL)
|
||||||
|
{
|
||||||
|
elog(ERROR, "Failed to initialize authorization header");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef CONTROL_PLANE_CONNECTOR_H
|
||||||
|
#define CONTROL_PLANE_CONNECTOR_H
|
||||||
|
|
||||||
|
void InitControlPlaneConnector();
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
#include "neon.h"
|
#include "neon.h"
|
||||||
#include "walproposer.h"
|
#include "walproposer.h"
|
||||||
#include "pagestore_client.h"
|
#include "pagestore_client.h"
|
||||||
|
#include "control_plane_connector.h"
|
||||||
|
|
||||||
PG_MODULE_MAGIC;
|
PG_MODULE_MAGIC;
|
||||||
void _PG_init(void);
|
void _PG_init(void);
|
||||||
@@ -34,7 +35,11 @@ _PG_init(void)
|
|||||||
{
|
{
|
||||||
pg_init_libpagestore();
|
pg_init_libpagestore();
|
||||||
pg_init_walproposer();
|
pg_init_walproposer();
|
||||||
|
InitControlPlaneConnector();
|
||||||
|
|
||||||
|
// Important: This must happen after other parts of the extension
|
||||||
|
// are loaded, otherwise any settings to GUCs that were set before
|
||||||
|
// the extension was loaded will be removed.
|
||||||
EmitWarningsOnPlaceholders("neon");
|
EmitWarningsOnPlaceholders("neon");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+152
-149
@@ -1,4 +1,4 @@
|
|||||||
# This file is automatically @generated by Poetry and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiohttp"
|
name = "aiohttp"
|
||||||
@@ -79,30 +79,30 @@ sa = ["sqlalchemy[postgresql-psycopg2binary] (>=1.3,<1.5)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "allure-pytest"
|
name = "allure-pytest"
|
||||||
version = "2.13.1"
|
version = "2.13.2"
|
||||||
description = "Allure pytest integration"
|
description = "Allure pytest integration"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "allure-pytest-2.13.1.tar.gz", hash = "sha256:68d69456eeb65af4061ec06a80bc941163b0616e8216554d36b070a6bf070e08"},
|
{file = "allure-pytest-2.13.2.tar.gz", hash = "sha256:22243159e8ec81ce2b5254b4013802198821b1b42f118f69d4a289396607c7b3"},
|
||||||
{file = "allure_pytest-2.13.1-py3-none-any.whl", hash = "sha256:a8de2fc3b3effe2d8f98801646920de3f055b779710f4c806dbee7c613c24633"},
|
{file = "allure_pytest-2.13.2-py3-none-any.whl", hash = "sha256:17de9dbee7f61c8e66a5b5e818b00e419dbcea44cb55c24319401ba813220690"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
allure-python-commons = "2.13.1"
|
allure-python-commons = "2.13.2"
|
||||||
pytest = ">=4.5.0"
|
pytest = ">=4.5.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "allure-python-commons"
|
name = "allure-python-commons"
|
||||||
version = "2.13.1"
|
version = "2.13.2"
|
||||||
description = "Common module for integrate allure with python-based frameworks"
|
description = "Common module for integrate allure with python-based frameworks"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
files = [
|
files = [
|
||||||
{file = "allure-python-commons-2.13.1.tar.gz", hash = "sha256:3fc13e1da8ebb23f9ab5c9c72ad04595023cdd5078dbb8604939997faebed5cb"},
|
{file = "allure-python-commons-2.13.2.tar.gz", hash = "sha256:8a03681330231b1deadd86b97ff68841c6591320114ae638570f1ed60d7a2033"},
|
||||||
{file = "allure_python_commons-2.13.1-py3-none-any.whl", hash = "sha256:d08e04867bddf44fef55def3d67f4bc25af58a1bf9fcffcf4ec3331f7f2ef0d0"},
|
{file = "allure_python_commons-2.13.2-py3-none-any.whl", hash = "sha256:2bb3646ec3fbf5b36d178a5e735002bc130ae9f9ba80f080af97d368ba375051"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -172,17 +172,6 @@ dev = ["Cython (>=0.29.24,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "flake8 (>=5.0.4
|
|||||||
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
|
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
|
||||||
test = ["flake8 (>=5.0.4,<5.1.0)", "uvloop (>=0.15.3)"]
|
test = ["flake8 (>=5.0.4,<5.1.0)", "uvloop (>=0.15.3)"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "atomicwrites"
|
|
||||||
version = "1.4.1"
|
|
||||||
description = "Atomic file writes."
|
|
||||||
category = "main"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|
||||||
files = [
|
|
||||||
{file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "21.4.0"
|
version = "21.4.0"
|
||||||
@@ -239,49 +228,49 @@ wrapt = "*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backoff"
|
name = "backoff"
|
||||||
version = "1.11.1"
|
version = "2.2.1"
|
||||||
description = "Function decoration for backoff and retry"
|
description = "Function decoration for backoff and retry"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
python-versions = ">=3.7,<4.0"
|
||||||
files = [
|
files = [
|
||||||
{file = "backoff-1.11.1-py2.py3-none-any.whl", hash = "sha256:61928f8fa48d52e4faa81875eecf308eccfb1016b018bb6bd21e05b5d90a96c5"},
|
{file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"},
|
||||||
{file = "backoff-1.11.1.tar.gz", hash = "sha256:ccb962a2378418c667b3c979b504fdeb7d9e0d29c0579e3b13b86467177728cb"},
|
{file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
version = "23.1.0"
|
version = "23.3.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.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"},
|
{file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"},
|
||||||
{file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"},
|
{file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"},
|
||||||
{file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"},
|
{file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"},
|
||||||
{file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"},
|
{file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"},
|
||||||
{file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"},
|
{file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"},
|
||||||
{file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"},
|
{file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"},
|
||||||
{file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"},
|
{file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"},
|
||||||
{file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"},
|
{file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"},
|
||||||
{file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"},
|
{file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"},
|
||||||
{file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"},
|
{file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"},
|
||||||
{file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"},
|
{file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"},
|
||||||
{file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"},
|
{file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"},
|
||||||
{file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"},
|
{file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"},
|
||||||
{file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"},
|
{file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"},
|
||||||
{file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"},
|
{file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"},
|
||||||
{file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"},
|
{file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"},
|
||||||
{file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"},
|
{file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"},
|
||||||
{file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"},
|
{file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"},
|
||||||
{file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"},
|
{file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"},
|
||||||
{file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"},
|
{file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"},
|
||||||
{file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"},
|
{file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"},
|
||||||
{file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"},
|
{file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"},
|
||||||
{file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"},
|
{file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"},
|
||||||
{file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"},
|
{file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"},
|
||||||
{file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"},
|
{file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -951,6 +940,21 @@ six = ">=1.9.0"
|
|||||||
gmpy = ["gmpy"]
|
gmpy = ["gmpy"]
|
||||||
gmpy2 = ["gmpy2"]
|
gmpy2 = ["gmpy2"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exceptiongroup"
|
||||||
|
version = "1.1.1"
|
||||||
|
description = "Backport of PEP 654 (exception groups)"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"},
|
||||||
|
{file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["pytest (>=6)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "execnet"
|
name = "execnet"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
@@ -1410,38 +1414,38 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mypy"
|
name = "mypy"
|
||||||
version = "1.1.1"
|
version = "1.3.0"
|
||||||
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-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eb485cea53f4f5284e5baf92902cd0088b24984f4209e25981cc359d64448d"},
|
||||||
{file = "mypy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61bf08362e93b6b12fad3eab68c4ea903a077b87c90ac06c11e3d7a09b56b9c1"},
|
{file = "mypy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c99c3ecf223cf2952638da9cd82793d8f3c0c5fa8b6ae2b2d9ed1e1ff51ba85"},
|
||||||
{file = "mypy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbb19c9f662e41e474e0cff502b7064a7edc6764f5262b6cd91d698163196799"},
|
{file = "mypy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:550a8b3a19bb6589679a7c3c31f64312e7ff482a816c96e0cecec9ad3a7564dd"},
|
||||||
{file = "mypy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:315ac73cc1cce4771c27d426b7ea558fb4e2836f89cb0296cbe056894e3a1f78"},
|
{file = "mypy-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cbc07246253b9e3d7d74c9ff948cd0fd7a71afcc2b77c7f0a59c26e9395cb152"},
|
||||||
{file = "mypy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5cb14ff9919b7df3538590fc4d4c49a0f84392237cbf5f7a816b4161c061829e"},
|
{file = "mypy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:a22435632710a4fcf8acf86cbd0d69f68ac389a3892cb23fbad176d1cddaf228"},
|
||||||
{file = "mypy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26cdd6a22b9b40b2fd71881a8a4f34b4d7914c679f154f43385ca878a8297389"},
|
{file = "mypy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6e33bb8b2613614a33dff70565f4c803f889ebd2f859466e42b46e1df76018dd"},
|
||||||
{file = "mypy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b5f81b40d94c785f288948c16e1f2da37203c6006546c5d947aab6f90aefef2"},
|
{file = "mypy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d23370d2a6b7a71dc65d1266f9a34e4cde9e8e21511322415db4b26f46f6b8c"},
|
||||||
{file = "mypy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b437be1c02712a605591e1ed1d858aba681757a1e55fe678a15c2244cd68a5"},
|
{file = "mypy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:658fe7b674769a0770d4b26cb4d6f005e88a442fe82446f020be8e5f5efb2fae"},
|
||||||
{file = "mypy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d809f88734f44a0d44959d795b1e6f64b2bbe0ea4d9cc4776aa588bb4229fc1c"},
|
{file = "mypy-1.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d29e324cdda61daaec2336c42512e59c7c375340bd202efa1fe0f7b8f8ca"},
|
||||||
{file = "mypy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:a380c041db500e1410bb5b16b3c1c35e61e773a5c3517926b81dfdab7582be54"},
|
{file = "mypy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:d0b6c62206e04061e27009481cb0ec966f7d6172b5b936f3ead3d74f29fe3dcf"},
|
||||||
{file = "mypy-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b7c7b708fe9a871a96626d61912e3f4ddd365bf7f39128362bc50cbd74a634d5"},
|
{file = "mypy-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:76ec771e2342f1b558c36d49900dfe81d140361dd0d2df6cd71b3db1be155409"},
|
||||||
{file = "mypy-1.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c10fa12df1232c936830839e2e935d090fc9ee315744ac33b8a32216b93707"},
|
{file = "mypy-1.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc95f8386314272bbc817026f8ce8f4f0d2ef7ae44f947c4664efac9adec929"},
|
||||||
{file = "mypy-1.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0a28a76785bf57655a8ea5eb0540a15b0e781c807b5aa798bd463779988fa1d5"},
|
{file = "mypy-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:faff86aa10c1aa4a10e1a301de160f3d8fc8703b88c7e98de46b531ff1276a9a"},
|
||||||
{file = "mypy-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ef6a01e563ec6a4940784c574d33f6ac1943864634517984471642908b30b6f7"},
|
{file = "mypy-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8c5979d0deb27e0f4479bee18ea0f83732a893e81b78e62e2dda3e7e518c92ee"},
|
||||||
{file = "mypy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d64c28e03ce40d5303450f547e07418c64c241669ab20610f273c9e6290b4b0b"},
|
{file = "mypy-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c5d2cc54175bab47011b09688b418db71403aefad07cbcd62d44010543fc143f"},
|
||||||
{file = "mypy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64cc3afb3e9e71a79d06e3ed24bb508a6d66f782aff7e56f628bf35ba2e0ba51"},
|
{file = "mypy-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87df44954c31d86df96c8bd6e80dfcd773473e877ac6176a8e29898bfb3501cb"},
|
||||||
{file = "mypy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce61663faf7a8e5ec6f456857bfbcec2901fbdb3ad958b778403f63b9e606a1b"},
|
{file = "mypy-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:473117e310febe632ddf10e745a355714e771ffe534f06db40702775056614c4"},
|
||||||
{file = "mypy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b0c373d071593deefbcdd87ec8db91ea13bd8f1328d44947e88beae21e8d5e9"},
|
{file = "mypy-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:74bc9b6e0e79808bf8678d7678b2ae3736ea72d56eede3820bd3849823e7f305"},
|
||||||
{file = "mypy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:2888ce4fe5aae5a673386fa232473014056967f3904f5abfcf6367b5af1f612a"},
|
{file = "mypy-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:44797d031a41516fcf5cbfa652265bb994e53e51994c1bd649ffcd0c3a7eccbf"},
|
||||||
{file = "mypy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:19ba15f9627a5723e522d007fe708007bae52b93faab00f95d72f03e1afa9598"},
|
{file = "mypy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ddae0f39ca146972ff6bb4399f3b2943884a774b8771ea0a8f50e971f5ea5ba8"},
|
||||||
{file = "mypy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:59bbd71e5c58eed2e992ce6523180e03c221dcd92b52f0e792f291d67b15a71c"},
|
{file = "mypy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c4c42c60a8103ead4c1c060ac3cdd3ff01e18fddce6f1016e08939647a0e703"},
|
||||||
{file = "mypy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9401e33814cec6aec8c03a9548e9385e0e228fc1b8b0a37b9ea21038e64cdd8a"},
|
{file = "mypy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e86c2c6852f62f8f2b24cb7a613ebe8e0c7dc1402c61d36a609174f63e0ff017"},
|
||||||
{file = "mypy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b398d8b1f4fba0e3c6463e02f8ad3346f71956b92287af22c9b12c3ec965a9f"},
|
{file = "mypy-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f9dca1e257d4cc129517779226753dbefb4f2266c4eaad610fc15c6a7e14283e"},
|
||||||
{file = "mypy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:69b35d1dcb5707382810765ed34da9db47e7f95b3528334a3c999b0c90fe523f"},
|
{file = "mypy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:95d8d31a7713510685b05fbb18d6ac287a56c8f6554d88c19e73f724a445448a"},
|
||||||
{file = "mypy-1.1.1-py3-none-any.whl", hash = "sha256:4e4e8b362cdf99ba00c2b218036002bdcdf1e0de085cdb296a49df03fb31dfc4"},
|
{file = "mypy-1.3.0-py3-none-any.whl", hash = "sha256:a8763e72d5d9574d45ce5881962bc8e9046bf7b375b0abf031f3e6811732a897"},
|
||||||
{file = "mypy-1.1.1.tar.gz", hash = "sha256:ae9ceae0f5b9059f33dbc62dea087e942c0ccab4b7a003719cb70f9b8abfa32f"},
|
{file = "mypy-1.3.0.tar.gz", hash = "sha256:e1f4d16e296f5135624b34e8fb741eb0eadedca90862405b1f1fde2040b9bd11"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1721,18 +1725,6 @@ files = [
|
|||||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"},
|
{file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "py"
|
|
||||||
version = "1.11.0"
|
|
||||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
|
||||||
category = "main"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
|
||||||
files = [
|
|
||||||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
|
||||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyasn1"
|
name = "pyasn1"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
@@ -1841,57 +1833,56 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "6.2.5"
|
version = "7.3.1"
|
||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
|
{file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"},
|
||||||
{file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
|
{file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
|
||||||
attrs = ">=19.2.0"
|
|
||||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||||
iniconfig = "*"
|
iniconfig = "*"
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
pluggy = ">=0.12,<2.0"
|
pluggy = ">=0.12,<2.0"
|
||||||
py = ">=1.8.2"
|
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||||
toml = "*"
|
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
|
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-asyncio"
|
name = "pytest-asyncio"
|
||||||
version = "0.19.0"
|
version = "0.21.0"
|
||||||
description = "Pytest support for asyncio"
|
description = "Pytest support for asyncio"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"},
|
{file = "pytest-asyncio-0.21.0.tar.gz", hash = "sha256:2b38a496aef56f56b0e87557ec313e11e1ab9276fc3863f6a7be0f1d0e415e1b"},
|
||||||
{file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"},
|
{file = "pytest_asyncio-0.21.0-py3-none-any.whl", hash = "sha256:f2b3366b7cd501a4056858bd39349d5af19742aed2d81660b7998b6341c7eb9c"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pytest = ">=6.1.0"
|
pytest = ">=7.0.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
|
||||||
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
|
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-httpserver"
|
name = "pytest-httpserver"
|
||||||
version = "1.0.6"
|
version = "1.0.8"
|
||||||
description = "pytest-httpserver is a httpserver for pytest"
|
description = "pytest-httpserver is a httpserver for pytest"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7,<4.0"
|
python-versions = ">=3.8,<4.0"
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest_httpserver-1.0.6-py3-none-any.whl", hash = "sha256:ac2379acc91fe8bdbe2911c93af8dd130e33b5899fb9934d15669480739c6d32"},
|
{file = "pytest_httpserver-1.0.8-py3-none-any.whl", hash = "sha256:24cd3d9f6a0b927c7bfc400d0b3fda7442721b8267ce29942bf307b190f0bb09"},
|
||||||
{file = "pytest_httpserver-1.0.6.tar.gz", hash = "sha256:9040d07bf59ac45d8de3db1d4468fd2d1d607975e4da4c872ecc0402cdbf7b3e"},
|
{file = "pytest_httpserver-1.0.8.tar.gz", hash = "sha256:e052f69bc8a9073db02484681e8e47004dd1fb3763b0ae833bd899e5895c559a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1914,14 +1905,14 @@ pytest = ">=3.2.5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-order"
|
name = "pytest-order"
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
description = "pytest plugin to run your tests in a specific order"
|
description = "pytest plugin to run your tests in a specific order"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-order-1.0.1.tar.gz", hash = "sha256:5dd6b929fbd7eaa6d0ee07586f65c623babb0afe72b4843c5f15055d6b3b1b1f"},
|
{file = "pytest-order-1.1.0.tar.gz", hash = "sha256:139d25b30826b78eebb42722f747eab14c44b88059d7a71d4f79d14a057269a5"},
|
||||||
{file = "pytest_order-1.0.1-py3-none-any.whl", hash = "sha256:bbe6e63a8e23741ab3e810d458d1ea7317e797b70f9550512d77d6e9e8fd1bbb"},
|
{file = "pytest_order-1.1.0-py3-none-any.whl", hash = "sha256:3b3730969c97900fa5cd31ecff80847680ed56b2490954565c14949ba60d9371"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1963,14 +1954,14 @@ pytest = ">=5.0.0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-xdist"
|
name = "pytest-xdist"
|
||||||
version = "3.0.2"
|
version = "3.3.1"
|
||||||
description = "pytest xdist plugin for distributed testing and loop-on-failing modes"
|
description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-xdist-3.0.2.tar.gz", hash = "sha256:688da9b814370e891ba5de650c9327d1a9d861721a524eb917e620eec3e90291"},
|
{file = "pytest-xdist-3.3.1.tar.gz", hash = "sha256:d5ee0520eb1b7bcca50a60a518ab7a7707992812c578198f8b44fdfac78e8c93"},
|
||||||
{file = "pytest_xdist-3.0.2-py3-none-any.whl", hash = "sha256:9feb9a18e1790696ea23e1434fa73b325ed4998b0e9fcb221f16fd1945e6df1b"},
|
{file = "pytest_xdist-3.3.1-py3-none-any.whl", hash = "sha256:ff9daa7793569e6a68544850fd3927cd257cc03a7ef76c95e86915355e82b5f2"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2148,29 +2139,29 @@ pyasn1 = ">=0.1.3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.0.255"
|
version = "0.0.269"
|
||||||
description = "An extremely fast Python linter, written in Rust."
|
description = "An extremely fast Python linter, written in Rust."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.0.255-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:b2d71fb6a7e50501a2473864acffc85dee6b750c25db198f7e71fe1dbbff1aad"},
|
{file = "ruff-0.0.269-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:3569bcdee679045c09c0161fabc057599759c49219a08d9a4aad2cc3982ccba3"},
|
||||||
{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.269-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:56347da63757a56cbce7d4b3d6044ca4f1941cd1bbff3714f7554360c3361f83"},
|
||||||
{file = "ruff-0.0.255-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a7fa60085079b91a298b963361be9b1b1c724582af6c84be954cbabdbd9309a"},
|
{file = "ruff-0.0.269-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6da8ee25ef2f0cc6cc8e6e20942c1d44d25a36dce35070d7184655bc14f63f63"},
|
||||||
{file = "ruff-0.0.255-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c089f7141496334ab5a127b54ce55e41f0d6714e68a4453a1e09d2204cdea8c3"},
|
{file = "ruff-0.0.269-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd81b8e681b9eaa6cf15484f3985bd8bd97c3d114e95bff3e8ea283bf8865062"},
|
||||||
{file = "ruff-0.0.255-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0423908caa7d437a416b853214565b9c33bbd1106c4f88147982216dddcbbd96"},
|
{file = "ruff-0.0.269-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f19f59ca3c28742955241fb452f3346241ddbd34e72ac5cb3d84fadebcf6bc8"},
|
||||||
{file = "ruff-0.0.255-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:981493e92547cacbb8e0874904ec049fe744507ee890dc8736caf89a8864f9a7"},
|
{file = "ruff-0.0.269-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f062059b8289a4fab7f6064601b811d447c2f9d3d432a17f689efe4d68988450"},
|
||||||
{file = "ruff-0.0.255-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d5193d2aedb35db180824462b374dbcfc306b2e76076245088afa6e5837df2"},
|
{file = "ruff-0.0.269-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f5dc7aac52c58e82510217e3c7efd80765c134c097c2815d59e40face0d1fe6"},
|
||||||
{file = "ruff-0.0.255-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd5e00733c9d160c8a34a22e62b390da9d1e9f326676402421cb8c1236beefc3"},
|
{file = "ruff-0.0.269-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e131b4dbe798c391090c6407641d6ab12c0fa1bb952379dde45e5000e208dabb"},
|
||||||
{file = "ruff-0.0.255-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:694418cf41838bd19c6229e4e1b2d04505b1e6b86fe3ab81165484fc96d36f01"},
|
{file = "ruff-0.0.269-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a374434e588e06550df0f8dcb74777290f285678de991fda4e1063c367ab2eb2"},
|
||||||
{file = "ruff-0.0.255-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5d0408985c9777369daebb5d3340a99e9f7294bdd7120642239261508185cf89"},
|
{file = "ruff-0.0.269-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cec2f4b84a14b87f1b121488649eb5b4eaa06467a2387373f750da74bdcb5679"},
|
||||||
{file = "ruff-0.0.255-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abd6376ef9d12f370d95a8c7c98682fbb9bfedfba59f40e84a816fef8ddcb8de"},
|
{file = "ruff-0.0.269-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:374b161753a247904aec7a32d45e165302b76b6e83d22d099bf3ff7c232c888f"},
|
||||||
{file = "ruff-0.0.255-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9b1a5df0bc09193cbef58a6f78e4a9a0b058a4f9733c0442866d078006d1bb9"},
|
{file = "ruff-0.0.269-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9ca0a1ddb1d835b5f742db9711c6cf59f213a1ad0088cb1e924a005fd399e7d8"},
|
||||||
{file = "ruff-0.0.255-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6a25c5f4ff087445b2e1bbcb9963f2ae7c868d65e4a8d5f84c36c12f71571179"},
|
{file = "ruff-0.0.269-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a20658f0b97d207c7841c13d528f36d666bf445b00b01139f28a8ccb80093bb"},
|
||||||
{file = "ruff-0.0.255-py3-none-win32.whl", hash = "sha256:1ff87a8310354f9f1a099625e54a27fdd6756d9cd2a40b45922f2e943daf982d"},
|
{file = "ruff-0.0.269-py3-none-win32.whl", hash = "sha256:03ff42bc91ceca58e0f0f072cb3f9286a9208f609812753474e799a997cdad1a"},
|
||||||
{file = "ruff-0.0.255-py3-none-win_amd64.whl", hash = "sha256:f3d8416be618f023f93ec4fd6ee3048585ef85dba9563b2a7e38fc7e5131d5b1"},
|
{file = "ruff-0.0.269-py3-none-win_amd64.whl", hash = "sha256:f3b59ccff57b21ef0967ea8021fd187ec14c528ec65507d8bcbe035912050776"},
|
||||||
{file = "ruff-0.0.255-py3-none-win_arm64.whl", hash = "sha256:8ba124819624145d7b6b53add40c367c44318893215ffc1bfe3d72e0225a1c9c"},
|
{file = "ruff-0.0.269-py3-none-win_arm64.whl", hash = "sha256:bbeb857b1e508a4487bdb02ca1e6d41dd8d5ac5335a5246e25de8a3dff38c1ff"},
|
||||||
{file = "ruff-0.0.255.tar.gz", hash = "sha256:f9eb1d3b2eecbeedae419fa494c4e2a5e4484baf93a1ce0f81eddb005e1919c5"},
|
{file = "ruff-0.0.269.tar.gz", hash = "sha256:11ddcfbab32cf5c420ea9dd5531170ace5a3e59c16d9251c7bd2581f7b16f602"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2271,7 +2262,7 @@ files = [
|
|||||||
name = "tomli"
|
name = "tomli"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
description = "A lil' TOML parser"
|
description = "A lil' TOML parser"
|
||||||
category = "dev"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
@@ -2281,42 +2272,54 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-psutil"
|
name = "types-psutil"
|
||||||
version = "5.9.5.4"
|
version = "5.9.5.12"
|
||||||
description = "Typing stubs for psutil"
|
description = "Typing stubs for psutil"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "types-psutil-5.9.5.4.tar.gz", hash = "sha256:aa09102b80c65a3b4573216614372398dab78972d650488eaff1ff05482cc18f"},
|
{file = "types-psutil-5.9.5.12.tar.gz", hash = "sha256:61a91679d3fe737250013b624dca09375e7cc3ad77dcc734553746c429c02aca"},
|
||||||
{file = "types_psutil-5.9.5.4-py3-none-any.whl", hash = "sha256:28e59764630187e462d43788efa16d59d5e77b510115f9e25901b2d4007fca62"},
|
{file = "types_psutil-5.9.5.12-py3-none-any.whl", hash = "sha256:e9a147b8561235c6afcce5aa1adb973fad9ab2c50cf89820697687f53510358f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-psycopg2"
|
name = "types-psycopg2"
|
||||||
version = "2.9.18"
|
version = "2.9.21.10"
|
||||||
description = "Typing stubs for psycopg2"
|
description = "Typing stubs for psycopg2"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "types-psycopg2-2.9.18.tar.gz", hash = "sha256:9b0e9e1f097b15cd9fa8aad2596a9e3082fd72f8d9cfe52b190cfa709105b6c0"},
|
{file = "types-psycopg2-2.9.21.10.tar.gz", hash = "sha256:c2600892312ae1c34e12f145749795d93dc4eac3ef7dbf8a9c1bfd45385e80d7"},
|
||||||
{file = "types_psycopg2-2.9.18-py3-none-any.whl", hash = "sha256:14c779dcab18c31453fa1cad3cf4b1601d33540a344adead3c47a6b8091cd2fa"},
|
{file = "types_psycopg2-2.9.21.10-py3-none-any.whl", hash = "sha256:918224a0731a3650832e46633e720703b5beef7693a064e777d9748654fcf5e5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-pytest-lazy-fixture"
|
||||||
|
version = "0.6.3.3"
|
||||||
|
description = "Typing stubs for pytest-lazy-fixture"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "types-pytest-lazy-fixture-0.6.3.3.tar.gz", hash = "sha256:2ef79d66bcde0e50acdac8dc55074b9ae0d4cfaeabdd638f5522f4cac7c8a2c7"},
|
||||||
|
{file = "types_pytest_lazy_fixture-0.6.3.3-py3-none-any.whl", hash = "sha256:a56a55649147ff960ff79d4b2c781a4f769351abc1876873f3116d0bd0c96353"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-requests"
|
name = "types-requests"
|
||||||
version = "2.28.5"
|
version = "2.31.0.0"
|
||||||
description = "Typing stubs for requests"
|
description = "Typing stubs for requests"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "types-requests-2.28.5.tar.gz", hash = "sha256:ac618bfefcb3742eaf97c961e13e9e5a226e545eda4a3dbe293b898d40933ad1"},
|
{file = "types-requests-2.31.0.0.tar.gz", hash = "sha256:c1c29d20ab8d84dff468d7febfe8e0cb0b4664543221b386605e14672b44ea25"},
|
||||||
{file = "types_requests-2.28.5-py3-none-any.whl", hash = "sha256:98ab647ae88b5e2c41d6d20cfcb5117da1bea561110000b6fdeeea07b3e89877"},
|
{file = "types_requests-2.31.0.0-py3-none-any.whl", hash = "sha256:7c5cea7940f8e92ec560bbc468f65bf684aa3dcf0554a6f8c4710f5f708dc598"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
types-urllib3 = "<1.27"
|
types-urllib3 = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-s3transfer"
|
name = "types-s3transfer"
|
||||||
@@ -2332,14 +2335,14 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-toml"
|
name = "types-toml"
|
||||||
version = "0.10.8"
|
version = "0.10.8.6"
|
||||||
description = "Typing stubs for toml"
|
description = "Typing stubs for toml"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "types-toml-0.10.8.tar.gz", hash = "sha256:b7e7ea572308b1030dc86c3ba825c5210814c2825612ec679eb7814f8dd9295a"},
|
{file = "types-toml-0.10.8.6.tar.gz", hash = "sha256:6d3ac79e36c9ee593c5d4fb33a50cca0e3adceb6ef5cff8b8e5aef67b4c4aaf2"},
|
||||||
{file = "types_toml-0.10.8-py3-none-any.whl", hash = "sha256:8300fd093e5829eb9c1fba69cee38130347d4b74ddf32d0a7df650ae55c2b599"},
|
{file = "types_toml-0.10.8.6-py3-none-any.whl", hash = "sha256:de7b2bb1831d6f7a4b554671ffe5875e729753496961b3e9b202745e4955dafa"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2356,14 +2359,14 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.3.0"
|
version = "4.6.1"
|
||||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
|
{file = "typing_extensions-4.6.1-py3-none-any.whl", hash = "sha256:6bac751f4789b135c43228e72de18637e9a6c29d12777023a703fd1a6858469f"},
|
||||||
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
|
{file = "typing_extensions-4.6.1.tar.gz", hash = "sha256:558bc0c4145f01e6405f4a5fdbd82050bd221b119f4bf72a961a1cfd471349d6"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2611,4 +2614,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 = "a0bd73376a3e9479f2379265ccec8dd6ac9df2e525909d12b77d918d590fba55"
|
content-hash = "c6c217033f50430c31b0979b74db222e6bab2301abd8b9f0cce5a9d5bccc578f"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use tokio::io::{AsyncRead, AsyncWrite};
|
|||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use utils::{project_git_version, sentry_init::init_sentry};
|
use utils::{project_git_version, sentry_init::init_sentry};
|
||||||
|
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn, Instrument};
|
||||||
|
|
||||||
project_git_version!(GIT_VERSION);
|
project_git_version!(GIT_VERSION);
|
||||||
|
|
||||||
@@ -141,7 +141,6 @@ async fn task_main(
|
|||||||
tokio::select! {
|
tokio::select! {
|
||||||
accept_result = listener.accept() => {
|
accept_result = listener.accept() => {
|
||||||
let (socket, peer_addr) = accept_result?;
|
let (socket, peer_addr) = accept_result?;
|
||||||
info!("accepted postgres client connection from {peer_addr}");
|
|
||||||
|
|
||||||
let session_id = uuid::Uuid::new_v4();
|
let session_id = uuid::Uuid::new_v4();
|
||||||
let tls_config = Arc::clone(&tls_config);
|
let tls_config = Arc::clone(&tls_config);
|
||||||
@@ -149,18 +148,18 @@ async fn task_main(
|
|||||||
|
|
||||||
connections.spawn(
|
connections.spawn(
|
||||||
async move {
|
async move {
|
||||||
info!("spawned a task for {peer_addr}");
|
|
||||||
|
|
||||||
socket
|
socket
|
||||||
.set_nodelay(true)
|
.set_nodelay(true)
|
||||||
.context("failed to set socket option")?;
|
.context("failed to set socket option")?;
|
||||||
|
|
||||||
handle_client(dest_suffix, tls_config, session_id, socket).await
|
info!(%peer_addr, "serving");
|
||||||
|
handle_client(dest_suffix, tls_config, socket).await
|
||||||
}
|
}
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| {
|
||||||
// Acknowledge that the task has finished with an error.
|
// Acknowledge that the task has finished with an error.
|
||||||
error!("per-client task finished with an error: {e:#}");
|
error!("per-client task finished with an error: {e:#}");
|
||||||
}),
|
})
|
||||||
|
.instrument(tracing::info_span!("handle_client", ?session_id))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_ = cancellation_token.cancelled() => {
|
_ = cancellation_token.cancelled() => {
|
||||||
@@ -192,7 +191,6 @@ async fn ssl_handshake<S: AsyncRead + AsyncWrite + Unpin>(
|
|||||||
let mut stream = PqStream::new(Stream::from_raw(raw_stream));
|
let mut stream = PqStream::new(Stream::from_raw(raw_stream));
|
||||||
|
|
||||||
let msg = stream.read_startup_packet().await?;
|
let msg = stream.read_startup_packet().await?;
|
||||||
info!("received {msg:?}");
|
|
||||||
use pq_proto::FeStartupPacket::*;
|
use pq_proto::FeStartupPacket::*;
|
||||||
|
|
||||||
match msg {
|
match msg {
|
||||||
@@ -215,15 +213,19 @@ async fn ssl_handshake<S: AsyncRead + AsyncWrite + Unpin>(
|
|||||||
}
|
}
|
||||||
Ok(raw.upgrade(tls_config).await?)
|
Ok(raw.upgrade(tls_config).await?)
|
||||||
}
|
}
|
||||||
_ => stream.throw_error_str(ERR_INSECURE_CONNECTION).await?,
|
unexpected => {
|
||||||
|
info!(
|
||||||
|
?unexpected,
|
||||||
|
"unexpected startup packet, rejecting connection"
|
||||||
|
);
|
||||||
|
stream.throw_error_str(ERR_INSECURE_CONNECTION).await?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(fields(session_id = ?session_id), skip_all)]
|
|
||||||
async fn handle_client(
|
async fn handle_client(
|
||||||
dest_suffix: Arc<String>,
|
dest_suffix: Arc<String>,
|
||||||
tls_config: Arc<rustls::ServerConfig>,
|
tls_config: Arc<rustls::ServerConfig>,
|
||||||
session_id: uuid::Uuid,
|
|
||||||
stream: impl AsyncRead + AsyncWrite + Unpin,
|
stream: impl AsyncRead + AsyncWrite + Unpin,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let tls_stream = ssl_handshake(stream, tls_config).await?;
|
let tls_stream = ssl_handshake(stream, tls_config).await?;
|
||||||
|
|||||||
+16
-15
@@ -6,40 +6,41 @@ authors = []
|
|||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.9"
|
python = "^3.9"
|
||||||
pytest = "^6.2.5"
|
pytest = "^7.3.1"
|
||||||
psycopg2-binary = "^2.9.1"
|
psycopg2-binary = "^2.9.1"
|
||||||
typing-extensions = "^4.1.0"
|
typing-extensions = "^4.6.1"
|
||||||
PyJWT = {version = "^2.1.0", extras = ["crypto"]}
|
PyJWT = {version = "^2.1.0", extras = ["crypto"]}
|
||||||
requests = "^2.31.0"
|
requests = "^2.31.0"
|
||||||
pytest-xdist = "^3.0.2"
|
pytest-xdist = "^3.3.1"
|
||||||
asyncpg = "^0.27.0"
|
asyncpg = "^0.27.0"
|
||||||
aiopg = "^1.3.1"
|
aiopg = "^1.3.1"
|
||||||
Jinja2 = "^3.0.2"
|
Jinja2 = "^3.0.2"
|
||||||
types-requests = "^2.28.5"
|
types-requests = "^2.31.0.0"
|
||||||
types-psycopg2 = "^2.9.18"
|
types-psycopg2 = "^2.9.21.10"
|
||||||
boto3 = "^1.26.16"
|
boto3 = "^1.26.16"
|
||||||
boto3-stubs = {extras = ["s3"], version = "^1.26.16"}
|
boto3-stubs = {extras = ["s3"], version = "^1.26.16"}
|
||||||
moto = {extras = ["server"], version = "^4.1.2"}
|
moto = {extras = ["server"], version = "^4.1.2"}
|
||||||
backoff = "^1.11.1"
|
backoff = "^2.2.1"
|
||||||
pytest-lazy-fixture = "^0.6.3"
|
pytest-lazy-fixture = "^0.6.3"
|
||||||
prometheus-client = "^0.14.1"
|
prometheus-client = "^0.14.1"
|
||||||
pytest-timeout = "^2.1.0"
|
pytest-timeout = "^2.1.0"
|
||||||
Werkzeug = "^2.2.3"
|
Werkzeug = "^2.2.3"
|
||||||
pytest-order = "^1.0.1"
|
pytest-order = "^1.1.0"
|
||||||
allure-pytest = "^2.13.1"
|
allure-pytest = "^2.13.2"
|
||||||
pytest-asyncio = "^0.19.0"
|
pytest-asyncio = "^0.21.0"
|
||||||
toml = "^0.10.2"
|
toml = "^0.10.2"
|
||||||
psutil = "^5.9.4"
|
psutil = "^5.9.4"
|
||||||
types-psutil = "^5.9.5.4"
|
types-psutil = "^5.9.5.12"
|
||||||
types-toml = "^0.10.8"
|
types-toml = "^0.10.8.6"
|
||||||
pytest-httpserver = "^1.0.6"
|
pytest-httpserver = "^1.0.8"
|
||||||
aiohttp = "3.7.4"
|
aiohttp = "3.7.4"
|
||||||
pytest-rerunfailures = "^11.1.2"
|
pytest-rerunfailures = "^11.1.2"
|
||||||
|
types-pytest-lazy-fixture = "^0.6.3.3"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
black = "^23.1.0"
|
black = "^23.3.0"
|
||||||
mypy = "==1.1.1"
|
mypy = "==1.3.0"
|
||||||
ruff = "^0.0.255"
|
ruff = "^0.0.269"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ use tokio::task::JoinHandle;
|
|||||||
use tokio::{runtime, time::sleep};
|
use tokio::{runtime, time::sleep};
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
|
use crate::metrics::BROKER_ITERATION_TIMELINES;
|
||||||
use crate::metrics::BROKER_PULLED_UPDATES;
|
use crate::metrics::BROKER_PULLED_UPDATES;
|
||||||
use crate::metrics::BROKER_PUSHED_UPDATES;
|
use crate::metrics::BROKER_PUSHED_UPDATES;
|
||||||
|
use crate::metrics::BROKER_PUSH_ALL_UPDATES_SECONDS;
|
||||||
use crate::GlobalTimelines;
|
use crate::GlobalTimelines;
|
||||||
use crate::SafeKeeperConf;
|
use crate::SafeKeeperConf;
|
||||||
|
|
||||||
@@ -61,8 +63,14 @@ async fn push_loop(conf: SafeKeeperConf) -> anyhow::Result<()> {
|
|||||||
BROKER_PUSHED_UPDATES.inc();
|
BROKER_PUSHED_UPDATES.inc();
|
||||||
}
|
}
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
// Log duration every second. Should be about 10MB of logs per day.
|
|
||||||
info!("pushed {} timeline updates to broker in {:?}", active_tlis.len(), elapsed);
|
BROKER_PUSH_ALL_UPDATES_SECONDS.observe(elapsed.as_secs_f64());
|
||||||
|
BROKER_ITERATION_TIMELINES.observe(active_tlis.len() as f64);
|
||||||
|
|
||||||
|
if elapsed > push_interval / 2 {
|
||||||
|
info!("broker push is too long, pushed {} timeline updates to broker in {:?}", active_tlis.len(), elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
sleep(push_interval).await;
|
sleep(push_interval).await;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -125,6 +125,25 @@ pub static BACKUP_ERRORS: Lazy<IntCounter> = Lazy::new(|| {
|
|||||||
)
|
)
|
||||||
.expect("Failed to register safekeeper_backup_errors_total counter")
|
.expect("Failed to register safekeeper_backup_errors_total counter")
|
||||||
});
|
});
|
||||||
|
pub static BROKER_PUSH_ALL_UPDATES_SECONDS: Lazy<Histogram> = Lazy::new(|| {
|
||||||
|
register_histogram!(
|
||||||
|
"safekeeper_broker_push_update_seconds",
|
||||||
|
"Seconds to push all timeline updates to the broker",
|
||||||
|
DISK_WRITE_SECONDS_BUCKETS.to_vec()
|
||||||
|
)
|
||||||
|
.expect("Failed to register safekeeper_broker_push_update_seconds histogram vec")
|
||||||
|
});
|
||||||
|
pub const TIMELINES_COUNT_BUCKETS: &[f64] = &[
|
||||||
|
1.0, 10.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0,
|
||||||
|
];
|
||||||
|
pub static BROKER_ITERATION_TIMELINES: Lazy<Histogram> = Lazy::new(|| {
|
||||||
|
register_histogram!(
|
||||||
|
"safekeeper_broker_iteration_timelines",
|
||||||
|
"Count of timelines pushed to the broker in a single iteration",
|
||||||
|
TIMELINES_COUNT_BUCKETS.to_vec()
|
||||||
|
)
|
||||||
|
.expect("Failed to register safekeeper_broker_iteration_timelines histogram vec")
|
||||||
|
});
|
||||||
|
|
||||||
pub const LABEL_UNKNOWN: &str = "unknown";
|
pub const LABEL_UNKNOWN: &str = "unknown";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// The script parses Allure reports and posts a comment with a summary of the test results to the PR.
|
// The script parses Allure reports and posts a comment with a summary of the test results to the PR or to the latest commit in the branch.
|
||||||
//
|
//
|
||||||
// The comment is updated on each run with the latest results.
|
// The comment is updated on each run with the latest results.
|
||||||
//
|
//
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
// - uses: actions/github-script@v6
|
// - uses: actions/github-script@v6
|
||||||
// with:
|
// with:
|
||||||
// script: |
|
// script: |
|
||||||
// const script = require("./scripts/pr-comment-test-report.js")
|
// const script = require("./scripts/comment-test-report.js")
|
||||||
// await script({
|
// await script({
|
||||||
// github,
|
// github,
|
||||||
// context,
|
// context,
|
||||||
@@ -35,8 +35,12 @@ class DefaultMap extends Map {
|
|||||||
module.exports = async ({ github, context, fetch, report }) => {
|
module.exports = async ({ github, context, fetch, report }) => {
|
||||||
// Marker to find the comment in the subsequent runs
|
// Marker to find the comment in the subsequent runs
|
||||||
const startMarker = `<!--AUTOMATIC COMMENT START #${context.payload.number}-->`
|
const startMarker = `<!--AUTOMATIC COMMENT START #${context.payload.number}-->`
|
||||||
|
// If we run the script in the PR or in the branch (main/release/...)
|
||||||
|
const isPullRequest = !!context.payload.pull_request
|
||||||
|
// Latest commit in PR or in the branch
|
||||||
|
const commitSha = isPullRequest ? context.payload.pull_request.head.sha : context.sha
|
||||||
// Let users know that the comment is updated automatically
|
// Let users know that the comment is updated automatically
|
||||||
const autoupdateNotice = `<div align="right"><sub>The comment gets automatically updated with the latest test results<br>${context.payload.pull_request.head.sha} at ${new Date().toISOString()} :recycle:</sub></div>`
|
const autoupdateNotice = `<div align="right"><sub>The comment gets automatically updated with the latest test results<br>${commitSha} at ${new Date().toISOString()} :recycle:</sub></div>`
|
||||||
// GitHub bot id taken from (https://api.github.com/users/github-actions[bot])
|
// GitHub bot id taken from (https://api.github.com/users/github-actions[bot])
|
||||||
const githubActionsBotId = 41898282
|
const githubActionsBotId = 41898282
|
||||||
// Commend body itself
|
// Commend body itself
|
||||||
@@ -166,22 +170,39 @@ module.exports = async ({ github, context, fetch, report }) => {
|
|||||||
|
|
||||||
commentBody += autoupdateNotice
|
commentBody += autoupdateNotice
|
||||||
|
|
||||||
const { data: comments } = await github.rest.issues.listComments({
|
let createCommentFn, listCommentsFn, updateCommentFn, issueNumberOrSha
|
||||||
issue_number: context.payload.number,
|
if (isPullRequest) {
|
||||||
|
createCommentFn = github.rest.issues.createComment
|
||||||
|
listCommentsFn = github.rest.issues.listComments
|
||||||
|
updateCommentFn = github.rest.issues.updateComment
|
||||||
|
issueNumberOrSha = {
|
||||||
|
issue_number: context.payload.number,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateCommentFn = github.rest.repos.updateCommitComment
|
||||||
|
listCommentsFn = github.rest.repos.listCommentsForCommit
|
||||||
|
createCommentFn = github.rest.repos.createCommitComment
|
||||||
|
issueNumberOrSha = {
|
||||||
|
commit_sha: commitSha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: comments } = await listCommentsFn({
|
||||||
|
...issueNumberOrSha,
|
||||||
...ownerRepoParams,
|
...ownerRepoParams,
|
||||||
})
|
})
|
||||||
|
|
||||||
const comment = comments.find(comment => comment.user.id === githubActionsBotId && comment.body.startsWith(startMarker))
|
const comment = comments.find(comment => comment.user.id === githubActionsBotId && comment.body.startsWith(startMarker))
|
||||||
if (comment) {
|
if (comment) {
|
||||||
await github.rest.issues.updateComment({
|
await updateCommentFn({
|
||||||
comment_id: comment.id,
|
comment_id: comment.id,
|
||||||
body: commentBody,
|
body: commentBody,
|
||||||
...ownerRepoParams,
|
...ownerRepoParams,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await github.rest.issues.createComment({
|
await createCommentFn({
|
||||||
issue_number: context.payload.number,
|
|
||||||
body: commentBody,
|
body: commentBody,
|
||||||
|
...issueNumberOrSha,
|
||||||
...ownerRepoParams,
|
...ownerRepoParams,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
+15
-6
@@ -156,7 +156,9 @@ class LLVM:
|
|||||||
profdata: Path,
|
profdata: Path,
|
||||||
objects: List[str],
|
objects: List[str],
|
||||||
sources: List[str],
|
sources: List[str],
|
||||||
demangler: Optional[Path] = None) -> None:
|
demangler: Optional[Path] = None,
|
||||||
|
output_file: Optional[Path] = None,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
cwd = self.cargo.cwd
|
cwd = self.cargo.cwd
|
||||||
objects = list(intersperse('-object', objects))
|
objects = list(intersperse('-object', objects))
|
||||||
@@ -180,14 +182,18 @@ class LLVM:
|
|||||||
*objects,
|
*objects,
|
||||||
*sources,
|
*sources,
|
||||||
]
|
]
|
||||||
subprocess.check_call(cmd, cwd=cwd)
|
if output_file is not None:
|
||||||
|
with output_file.open('w') as outfile:
|
||||||
|
subprocess.check_call(cmd, cwd=cwd, stdout=outfile)
|
||||||
|
else:
|
||||||
|
subprocess.check_call(cmd, cwd=cwd)
|
||||||
|
|
||||||
def cov_report(self, **kwargs) -> None:
|
def cov_report(self, **kwargs) -> None:
|
||||||
self._cov(subcommand='report', **kwargs)
|
self._cov(subcommand='report', **kwargs)
|
||||||
|
|
||||||
def cov_export(self, *, kind: str, **kwargs) -> None:
|
def cov_export(self, *, kind: str, output_file: Optional[Path], **kwargs) -> None:
|
||||||
extras = (f'-format={kind}', )
|
extras = (f'-format={kind}', )
|
||||||
self._cov(subcommand='export', *extras, **kwargs)
|
self._cov(subcommand='export', *extras, output_file=output_file, **kwargs)
|
||||||
|
|
||||||
def cov_show(self, *, kind: str, output_dir: Optional[Path] = None, **kwargs) -> None:
|
def cov_show(self, *, kind: str, output_dir: Optional[Path] = None, **kwargs) -> None:
|
||||||
extras = [f'-format={kind}']
|
extras = [f'-format={kind}']
|
||||||
@@ -283,9 +289,12 @@ class TextReport(Report):
|
|||||||
self.llvm.cov_show(kind='text', **self._common_kwargs())
|
self.llvm.cov_show(kind='text', **self._common_kwargs())
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class LcovReport(Report):
|
class LcovReport(Report):
|
||||||
|
output_file: Path
|
||||||
|
|
||||||
def generate(self) -> None:
|
def generate(self) -> None:
|
||||||
self.llvm.cov_export(kind='lcov', **self._common_kwargs())
|
self.llvm.cov_export(kind='lcov', output_file=self.output_file, **self._common_kwargs())
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -475,7 +484,7 @@ class State:
|
|||||||
'text':
|
'text':
|
||||||
lambda: TextReport(**params),
|
lambda: TextReport(**params),
|
||||||
'lcov':
|
'lcov':
|
||||||
lambda: LcovReport(**params),
|
lambda: LcovReport(**params, output_file=self.report_dir / 'lcov.info'),
|
||||||
'summary':
|
'summary':
|
||||||
lambda: SummaryReport(**params),
|
lambda: SummaryReport(**params),
|
||||||
'github':
|
'github':
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ class PgProtocol:
|
|||||||
Returns psycopg2's connection object.
|
Returns psycopg2's connection object.
|
||||||
This method passes all extra params to connstr.
|
This method passes all extra params to connstr.
|
||||||
"""
|
"""
|
||||||
conn = psycopg2.connect(**self.conn_options(**kwargs))
|
conn: PgConnection = psycopg2.connect(**self.conn_options(**kwargs))
|
||||||
|
|
||||||
# WARNING: this setting affects *all* tests!
|
# WARNING: this setting affects *all* tests!
|
||||||
conn.autocommit = autocommit
|
conn.autocommit = autocommit
|
||||||
@@ -535,8 +535,8 @@ def export_timeline(
|
|||||||
|
|
||||||
|
|
||||||
def main(args: argparse.Namespace):
|
def main(args: argparse.Namespace):
|
||||||
# any psql version will do here. use current DEFAULT_PG_VERSION = 14
|
# any psql version will do here. use current DEFAULT_PG_VERSION = 15
|
||||||
psql_path = str(Path(args.pg_distrib_dir) / "v14" / "bin" / "psql")
|
psql_path = str(Path(args.pg_distrib_dir) / "v15" / "bin" / "psql")
|
||||||
|
|
||||||
old_pageserver_host = args.old_pageserver_host
|
old_pageserver_host = args.old_pageserver_host
|
||||||
new_pageserver_host = args.new_pageserver_host
|
new_pageserver_host = args.new_pageserver_host
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ def get_connection_cursor():
|
|||||||
connstr = os.getenv("DATABASE_URL")
|
connstr = os.getenv("DATABASE_URL")
|
||||||
if not connstr:
|
if not connstr:
|
||||||
err("DATABASE_URL environment variable is not set")
|
err("DATABASE_URL environment variable is not set")
|
||||||
with psycopg2.connect(connstr) as conn:
|
with psycopg2.connect(connstr, connect_timeout=30) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
yield cur
|
yield cur
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ pub type BrokerClientChannel = BrokerServiceClient<Channel>;
|
|||||||
// Create connection object configured to run TLS if schema starts with https://
|
// Create connection object configured to run TLS if schema starts with https://
|
||||||
// and plain text otherwise. Connection is lazy, only endpoint sanity is
|
// and plain text otherwise. Connection is lazy, only endpoint sanity is
|
||||||
// validated here.
|
// validated here.
|
||||||
|
//
|
||||||
|
// NB: this function is not async, but still must be run on a tokio runtime thread
|
||||||
|
// because that's a requirement of tonic_endpoint.connect_lazy()'s Channel::new call.
|
||||||
pub fn connect<U>(endpoint: U, keepalive_interval: Duration) -> anyhow::Result<BrokerClientChannel>
|
pub fn connect<U>(endpoint: U, keepalive_interval: Duration) -> anyhow::Result<BrokerClientChannel>
|
||||||
where
|
where
|
||||||
U: std::convert::TryInto<Uri>,
|
U: std::convert::TryInto<Uri>,
|
||||||
|
|||||||
@@ -312,6 +312,6 @@ def neon_with_baseline(request: FixtureRequest) -> PgCompare:
|
|||||||
implementation-specific logic is widely useful across multiple tests, it might
|
implementation-specific logic is widely useful across multiple tests, it might
|
||||||
make sense to add methods to the PgCompare class.
|
make sense to add methods to the PgCompare class.
|
||||||
"""
|
"""
|
||||||
fixture = request.getfixturevalue(request.param) # type: ignore
|
fixture = request.getfixturevalue(request.param)
|
||||||
assert isinstance(fixture, PgCompare), f"test error: fixture {fixture} is not PgCompare"
|
assert isinstance(fixture, PgCompare), f"test error: fixture {fixture} is not PgCompare"
|
||||||
return fixture
|
return fixture
|
||||||
|
|||||||
@@ -65,12 +65,19 @@ PAGESERVER_PER_TENANT_METRICS: Tuple[str, ...] = (
|
|||||||
"pageserver_getpage_reconstruct_seconds_bucket",
|
"pageserver_getpage_reconstruct_seconds_bucket",
|
||||||
"pageserver_getpage_reconstruct_seconds_count",
|
"pageserver_getpage_reconstruct_seconds_count",
|
||||||
"pageserver_getpage_reconstruct_seconds_sum",
|
"pageserver_getpage_reconstruct_seconds_sum",
|
||||||
|
"pageserver_getpage_get_reconstruct_data_seconds_bucket",
|
||||||
|
"pageserver_getpage_get_reconstruct_data_seconds_count",
|
||||||
|
"pageserver_getpage_get_reconstruct_data_seconds_sum",
|
||||||
"pageserver_io_operations_bytes_total",
|
"pageserver_io_operations_bytes_total",
|
||||||
"pageserver_io_operations_seconds_bucket",
|
"pageserver_io_operations_seconds_bucket",
|
||||||
"pageserver_io_operations_seconds_count",
|
"pageserver_io_operations_seconds_count",
|
||||||
"pageserver_io_operations_seconds_sum",
|
"pageserver_io_operations_seconds_sum",
|
||||||
"pageserver_last_record_lsn",
|
"pageserver_last_record_lsn",
|
||||||
"pageserver_materialized_cache_hits_total",
|
"pageserver_materialized_cache_hits_total",
|
||||||
|
"pageserver_materialized_cache_hits_direct_total",
|
||||||
|
"pageserver_read_num_fs_layers_bucket",
|
||||||
|
"pageserver_read_num_fs_layers_count",
|
||||||
|
"pageserver_read_num_fs_layers_sum",
|
||||||
"pageserver_smgr_query_seconds_bucket",
|
"pageserver_smgr_query_seconds_bucket",
|
||||||
"pageserver_smgr_query_seconds_count",
|
"pageserver_smgr_query_seconds_count",
|
||||||
"pageserver_smgr_query_seconds_sum",
|
"pageserver_smgr_query_seconds_sum",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union, cast
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import asyncpg
|
import asyncpg
|
||||||
import backoff # type: ignore
|
import backoff
|
||||||
import boto3
|
import boto3
|
||||||
import jwt
|
import jwt
|
||||||
import psycopg2
|
import psycopg2
|
||||||
@@ -354,7 +354,7 @@ class PgProtocol:
|
|||||||
Returns psycopg2's connection object.
|
Returns psycopg2's connection object.
|
||||||
This method passes all extra params to connstr.
|
This method passes all extra params to connstr.
|
||||||
"""
|
"""
|
||||||
conn = psycopg2.connect(**self.conn_options(**kwargs))
|
conn: PgConnection = psycopg2.connect(**self.conn_options(**kwargs))
|
||||||
|
|
||||||
# WARNING: this setting affects *all* tests!
|
# WARNING: this setting affects *all* tests!
|
||||||
conn.autocommit = autocommit
|
conn.autocommit = autocommit
|
||||||
@@ -1603,8 +1603,6 @@ class NeonPageserver(PgProtocol):
|
|||||||
# https://github.com/neondatabase/neon/issues/2442
|
# https://github.com/neondatabase/neon/issues/2442
|
||||||
".*could not remove ephemeral file.*No such file or directory.*",
|
".*could not remove ephemeral file.*No such file or directory.*",
|
||||||
# FIXME: These need investigation
|
# FIXME: These need investigation
|
||||||
".*gc_loop.*Failed to get a tenant .* Tenant .* not found.*",
|
|
||||||
".*compaction_loop.*Failed to get a tenant .* Tenant .* not found.*",
|
|
||||||
".*manual_gc.*is_shutdown_requested\\(\\) called in an unexpected task or thread.*",
|
".*manual_gc.*is_shutdown_requested\\(\\) called in an unexpected task or thread.*",
|
||||||
".*tenant_list: timeline is not found in remote index while it is present in the tenants registry.*",
|
".*tenant_list: timeline is not found in remote index while it is present in the tenants registry.*",
|
||||||
".*Removing intermediate uninit mark file.*",
|
".*Removing intermediate uninit mark file.*",
|
||||||
@@ -1621,6 +1619,10 @@ class NeonPageserver(PgProtocol):
|
|||||||
".*task iteration took longer than the configured period.*",
|
".*task iteration took longer than the configured period.*",
|
||||||
# this is until #3501
|
# this is until #3501
|
||||||
".*Compaction failed, retrying in [^:]+: Cannot run compaction iteration on inactive tenant",
|
".*Compaction failed, retrying in [^:]+: Cannot run compaction iteration on inactive tenant",
|
||||||
|
# these can happen anytime we do compactions from background task and shutdown pageserver
|
||||||
|
r".*ERROR.*ancestor timeline \S+ is being stopped",
|
||||||
|
# this is expected given our collaborative shutdown approach for the UploadQueue
|
||||||
|
".*Compaction failed, retrying in .*: queue is in state Stopped.*",
|
||||||
]
|
]
|
||||||
|
|
||||||
def start(
|
def start(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -109,6 +110,10 @@ class PageserverHttpClient(requests.Session):
|
|||||||
if auth_token is not None:
|
if auth_token is not None:
|
||||||
self.headers["Authorization"] = f"Bearer {auth_token}"
|
self.headers["Authorization"] = f"Bearer {auth_token}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_url(self) -> str:
|
||||||
|
return f"http://localhost:{self.port}"
|
||||||
|
|
||||||
def verbose_error(self, res: requests.Response):
|
def verbose_error(self, res: requests.Response):
|
||||||
try:
|
try:
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
@@ -150,14 +155,14 @@ class PageserverHttpClient(requests.Session):
|
|||||||
return res_json
|
return res_json
|
||||||
|
|
||||||
def tenant_create(
|
def tenant_create(
|
||||||
self, new_tenant_id: Optional[TenantId] = None, conf: Optional[Dict[str, Any]] = None
|
self, new_tenant_id: TenantId, conf: Optional[Dict[str, Any]] = None
|
||||||
) -> TenantId:
|
) -> TenantId:
|
||||||
if conf is not None:
|
if conf is not None:
|
||||||
assert "new_tenant_id" not in conf.keys()
|
assert "new_tenant_id" not in conf.keys()
|
||||||
res = self.post(
|
res = self.post(
|
||||||
f"http://localhost:{self.port}/v1/tenant",
|
f"http://localhost:{self.port}/v1/tenant",
|
||||||
json={
|
json={
|
||||||
"new_tenant_id": str(new_tenant_id) if new_tenant_id else None,
|
"new_tenant_id": str(new_tenant_id),
|
||||||
**(conf or {}),
|
**(conf or {}),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -168,8 +173,22 @@ class PageserverHttpClient(requests.Session):
|
|||||||
assert isinstance(new_tenant_id, str)
|
assert isinstance(new_tenant_id, str)
|
||||||
return TenantId(new_tenant_id)
|
return TenantId(new_tenant_id)
|
||||||
|
|
||||||
def tenant_attach(self, tenant_id: TenantId):
|
def tenant_attach(
|
||||||
res = self.post(f"http://localhost:{self.port}/v1/tenant/{tenant_id}/attach")
|
self, tenant_id: TenantId, config: None | Dict[str, Any] = None, config_null: bool = False
|
||||||
|
):
|
||||||
|
if config_null:
|
||||||
|
assert config is None
|
||||||
|
body = "null"
|
||||||
|
else:
|
||||||
|
# null-config is prohibited by the API
|
||||||
|
if config is None:
|
||||||
|
config = {}
|
||||||
|
body = json.dumps({"config": config})
|
||||||
|
res = self.post(
|
||||||
|
f"http://localhost:{self.port}/v1/tenant/{tenant_id}/attach",
|
||||||
|
data=body,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
self.verbose_error(res)
|
self.verbose_error(res)
|
||||||
|
|
||||||
def tenant_detach(self, tenant_id: TenantId, detach_ignored=False):
|
def tenant_detach(self, tenant_id: TenantId, detach_ignored=False):
|
||||||
@@ -274,13 +293,13 @@ class PageserverHttpClient(requests.Session):
|
|||||||
self,
|
self,
|
||||||
pg_version: PgVersion,
|
pg_version: PgVersion,
|
||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
new_timeline_id: Optional[TimelineId] = None,
|
new_timeline_id: TimelineId,
|
||||||
ancestor_timeline_id: Optional[TimelineId] = None,
|
ancestor_timeline_id: Optional[TimelineId] = None,
|
||||||
ancestor_start_lsn: Optional[Lsn] = None,
|
ancestor_start_lsn: Optional[Lsn] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Dict[Any, Any]:
|
) -> Dict[Any, Any]:
|
||||||
body: Dict[str, Any] = {
|
body: Dict[str, Any] = {
|
||||||
"new_timeline_id": str(new_timeline_id) if new_timeline_id else None,
|
"new_timeline_id": str(new_timeline_id),
|
||||||
"ancestor_start_lsn": str(ancestor_start_lsn) if ancestor_start_lsn else None,
|
"ancestor_start_lsn": str(ancestor_start_lsn) if ancestor_start_lsn else None,
|
||||||
"ancestor_timeline_id": str(ancestor_timeline_id) if ancestor_timeline_id else None,
|
"ancestor_timeline_id": str(ancestor_timeline_id) if ancestor_timeline_id else None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ class PgVersion(str, enum.Enum):
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"'{self.value}'"
|
return f"'{self.value}'"
|
||||||
|
|
||||||
|
# Make this explicit for Python 3.11 compatibility, which changes the behavior of enums
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
# In GitHub workflows we use Postgres version with v-prefix (e.g. v14 instead of just 14),
|
# In GitHub workflows we use Postgres version with v-prefix (e.g. v14 instead of just 14),
|
||||||
# sometime we need to do so in tests.
|
# sometime we need to do so in tests.
|
||||||
@property
|
@property
|
||||||
@@ -78,11 +82,11 @@ def pytest_addoption(parser: Parser):
|
|||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def pg_version(request: FixtureRequest) -> Iterator[PgVersion]:
|
def pg_version(request: FixtureRequest) -> Iterator[PgVersion]:
|
||||||
if v := request.config.getoption("--pg-version"):
|
if v := request.config.getoption("--pg-version"):
|
||||||
version, source = v, "from --pg-version commad-line argument"
|
version, source = v, "from --pg-version command-line argument"
|
||||||
elif v := os.environ.get("DEFAULT_PG_VERSION"):
|
elif v := os.environ.get("DEFAULT_PG_VERSION"):
|
||||||
version, source = PgVersion(v), "from DEFAULT_PG_VERSION environment variable"
|
version, source = PgVersion(v), "from DEFAULT_PG_VERSION environment variable"
|
||||||
else:
|
else:
|
||||||
version, source = DEFAULT_VERSION, "default verson"
|
version, source = DEFAULT_VERSION, "default version"
|
||||||
|
|
||||||
log.info(f"pg_version is {version} ({source})")
|
log.info(f"pg_version is {version} ({source})")
|
||||||
yield version
|
yield version
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from contextlib import closing
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fixtures.compare_fixtures import PgCompare
|
from fixtures.compare_fixtures import PgCompare
|
||||||
from pytest_lazyfixture import lazy_fixture # type: ignore
|
from pytest_lazyfixture import lazy_fixture
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from contextlib import closing
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fixtures.compare_fixtures import PgCompare
|
from fixtures.compare_fixtures import PgCompare
|
||||||
from pytest_lazyfixture import lazy_fixture # type: ignore
|
from pytest_lazyfixture import lazy_fixture
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from contextlib import closing
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fixtures.compare_fixtures import PgCompare
|
from fixtures.compare_fixtures import PgCompare
|
||||||
from pytest_lazyfixture import lazy_fixture # type: ignore
|
from pytest_lazyfixture import lazy_fixture
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pytest
|
|||||||
from fixtures.benchmark_fixture import MetricReport
|
from fixtures.benchmark_fixture import MetricReport
|
||||||
from fixtures.compare_fixtures import PgCompare
|
from fixtures.compare_fixtures import PgCompare
|
||||||
from fixtures.log_helper import log
|
from fixtures.log_helper import log
|
||||||
from pytest_lazyfixture import lazy_fixture # type: ignore
|
from pytest_lazyfixture import lazy_fixture
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Generator, Optional
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fixtures.neon_fixtures import (
|
||||||
|
LocalFsStorage,
|
||||||
|
NeonEnv,
|
||||||
|
NeonEnvBuilder,
|
||||||
|
RemoteStorageKind,
|
||||||
|
)
|
||||||
|
from fixtures.pageserver.http import PageserverApiException, TenantConfig
|
||||||
|
from fixtures.types import TenantId
|
||||||
|
from fixtures.utils import wait_until
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def positive_env(neon_env_builder: NeonEnvBuilder) -> NeonEnv:
|
||||||
|
neon_env_builder.enable_remote_storage(
|
||||||
|
remote_storage_kind=RemoteStorageKind.LOCAL_FS,
|
||||||
|
test_name="test_attach_tenant_config",
|
||||||
|
)
|
||||||
|
env = neon_env_builder.init_start()
|
||||||
|
assert isinstance(env.remote_storage, LocalFsStorage)
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NegativeTests:
|
||||||
|
neon_env: NeonEnv
|
||||||
|
tenant_id: TenantId
|
||||||
|
config_pre_detach: TenantConfig
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def negative_env(neon_env_builder: NeonEnvBuilder) -> Generator[NegativeTests, None, None]:
|
||||||
|
neon_env_builder.enable_remote_storage(
|
||||||
|
remote_storage_kind=RemoteStorageKind.LOCAL_FS,
|
||||||
|
test_name="test_attach_tenant_config",
|
||||||
|
)
|
||||||
|
env = neon_env_builder.init_start()
|
||||||
|
assert isinstance(env.remote_storage, LocalFsStorage)
|
||||||
|
|
||||||
|
ps_http = env.pageserver.http_client()
|
||||||
|
(tenant_id, _) = env.neon_cli.create_tenant()
|
||||||
|
assert ps_http.tenant_config(tenant_id).tenant_specific_overrides == {}
|
||||||
|
config_pre_detach = ps_http.tenant_config(tenant_id)
|
||||||
|
assert tenant_id in [TenantId(t["id"]) for t in ps_http.tenant_list()]
|
||||||
|
ps_http.tenant_detach(tenant_id)
|
||||||
|
assert tenant_id not in [TenantId(t["id"]) for t in ps_http.tenant_list()]
|
||||||
|
|
||||||
|
yield NegativeTests(env, tenant_id, config_pre_detach)
|
||||||
|
|
||||||
|
assert tenant_id not in [
|
||||||
|
TenantId(t["id"]) for t in ps_http.tenant_list()
|
||||||
|
], "tenant should not be attached after negative test"
|
||||||
|
|
||||||
|
env.pageserver.allowed_errors.append(".*Error processing HTTP request: Bad request")
|
||||||
|
|
||||||
|
def log_contains_bad_request():
|
||||||
|
env.pageserver.log_contains(".*Error processing HTTP request: Bad request")
|
||||||
|
|
||||||
|
wait_until(50, 0.1, log_contains_bad_request)
|
||||||
|
|
||||||
|
|
||||||
|
def test_null_body(negative_env: NegativeTests):
|
||||||
|
"""
|
||||||
|
If we send `null` in the body, the request should be rejected with status 400.
|
||||||
|
"""
|
||||||
|
env = negative_env.neon_env
|
||||||
|
tenant_id = negative_env.tenant_id
|
||||||
|
ps_http = env.pageserver.http_client()
|
||||||
|
|
||||||
|
res = ps_http.post(
|
||||||
|
f"{ps_http.base_url}/v1/tenant/{tenant_id}/attach",
|
||||||
|
data=b"null",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
assert res.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_null_config(negative_env: NegativeTests):
|
||||||
|
"""
|
||||||
|
If the `config` field is `null`, the request should be rejected with status 400.
|
||||||
|
"""
|
||||||
|
|
||||||
|
env = negative_env.neon_env
|
||||||
|
tenant_id = negative_env.tenant_id
|
||||||
|
ps_http = env.pageserver.http_client()
|
||||||
|
|
||||||
|
res = ps_http.post(
|
||||||
|
f"{ps_http.base_url}/v1/tenant/{tenant_id}/attach",
|
||||||
|
data=b'{"config": null}',
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
assert res.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_with_unknown_keys_is_bad_request(negative_env: NegativeTests):
|
||||||
|
"""
|
||||||
|
If we send a config with unknown keys, the request should be rejected with status 400.
|
||||||
|
"""
|
||||||
|
|
||||||
|
env = negative_env.neon_env
|
||||||
|
tenant_id = negative_env.tenant_id
|
||||||
|
ps_http = env.pageserver.http_client()
|
||||||
|
|
||||||
|
config_with_unknown_keys = {
|
||||||
|
"compaction_period": "1h",
|
||||||
|
"this_key_does_not_exist": "some value",
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(PageserverApiException) as e:
|
||||||
|
ps_http.tenant_attach(tenant_id, config=config_with_unknown_keys)
|
||||||
|
assert e.type == PageserverApiException
|
||||||
|
assert e.value.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("content_type", [None, "application/json"])
|
||||||
|
def test_empty_body(positive_env: NeonEnv, content_type: Optional[str]):
|
||||||
|
"""
|
||||||
|
For backwards-compatiblity: if we send an empty body,
|
||||||
|
the request should be accepted and the config should be the default config.
|
||||||
|
"""
|
||||||
|
env = positive_env
|
||||||
|
ps_http = env.pageserver.http_client()
|
||||||
|
(tenant_id, _) = env.neon_cli.create_tenant()
|
||||||
|
assert ps_http.tenant_config(tenant_id).tenant_specific_overrides == {}
|
||||||
|
config_pre_detach = ps_http.tenant_config(tenant_id)
|
||||||
|
assert tenant_id in [TenantId(t["id"]) for t in ps_http.tenant_list()]
|
||||||
|
ps_http.tenant_detach(tenant_id)
|
||||||
|
assert tenant_id not in [TenantId(t["id"]) for t in ps_http.tenant_list()]
|
||||||
|
|
||||||
|
ps_http.post(
|
||||||
|
f"{ps_http.base_url}/v1/tenant/{tenant_id}/attach",
|
||||||
|
data=b"",
|
||||||
|
headers=None if content_type else {"Content-Type": "application/json"},
|
||||||
|
).raise_for_status()
|
||||||
|
|
||||||
|
assert ps_http.tenant_config(tenant_id).tenant_specific_overrides == {}
|
||||||
|
assert ps_http.tenant_config(tenant_id).effective_config == config_pre_detach.effective_config
|
||||||
|
|
||||||
|
|
||||||
|
def test_fully_custom_config(positive_env: NeonEnv):
|
||||||
|
"""
|
||||||
|
If we send a valid config in the body, the request should be accepted and the config should be applied.
|
||||||
|
"""
|
||||||
|
env = positive_env
|
||||||
|
|
||||||
|
fully_custom_config = {
|
||||||
|
"compaction_period": "1h",
|
||||||
|
"compaction_threshold": 13,
|
||||||
|
"compaction_target_size": 1048576,
|
||||||
|
"checkpoint_distance": 10000,
|
||||||
|
"checkpoint_timeout": "13m",
|
||||||
|
"eviction_policy": {
|
||||||
|
"kind": "LayerAccessThreshold",
|
||||||
|
"period": "20s",
|
||||||
|
"threshold": "23h",
|
||||||
|
},
|
||||||
|
"evictions_low_residence_duration_metric_threshold": "2days",
|
||||||
|
"gc_feedback": True,
|
||||||
|
"gc_horizon": 23 * (1024 * 1024),
|
||||||
|
"gc_period": "2h 13m",
|
||||||
|
"image_creation_threshold": 7,
|
||||||
|
"pitr_interval": "1m",
|
||||||
|
"lagging_wal_timeout": "23m",
|
||||||
|
"max_lsn_wal_lag": 230000,
|
||||||
|
"min_resident_size_override": 23,
|
||||||
|
"trace_read_requests": True,
|
||||||
|
"walreceiver_connect_timeout": "13m",
|
||||||
|
}
|
||||||
|
|
||||||
|
ps_http = env.pageserver.http_client()
|
||||||
|
|
||||||
|
initial_tenant_config = ps_http.tenant_config(env.initial_tenant)
|
||||||
|
assert initial_tenant_config.tenant_specific_overrides == {}
|
||||||
|
assert set(initial_tenant_config.effective_config.keys()) == set(
|
||||||
|
fully_custom_config.keys()
|
||||||
|
), "ensure we cover all config options"
|
||||||
|
|
||||||
|
(tenant_id, _) = env.neon_cli.create_tenant()
|
||||||
|
ps_http.set_tenant_config(tenant_id, fully_custom_config)
|
||||||
|
our_tenant_config = ps_http.tenant_config(tenant_id)
|
||||||
|
assert our_tenant_config.tenant_specific_overrides == fully_custom_config
|
||||||
|
assert set(our_tenant_config.effective_config.keys()) == set(
|
||||||
|
fully_custom_config.keys()
|
||||||
|
), "ensure we cover all config options"
|
||||||
|
assert {
|
||||||
|
k: initial_tenant_config.effective_config[k] != our_tenant_config.effective_config[k]
|
||||||
|
for k in fully_custom_config.keys()
|
||||||
|
} == {
|
||||||
|
k: True for k in fully_custom_config.keys()
|
||||||
|
}, "ensure our custom config has different values than the default config for all config options, so we know we overrode everything"
|
||||||
|
|
||||||
|
ps_http.tenant_detach(tenant_id)
|
||||||
|
ps_http.tenant_attach(tenant_id, config=fully_custom_config)
|
||||||
|
|
||||||
|
assert ps_http.tenant_config(tenant_id).tenant_specific_overrides == fully_custom_config
|
||||||
|
assert set(ps_http.tenant_config(tenant_id).effective_config.keys()) == set(
|
||||||
|
fully_custom_config.keys()
|
||||||
|
), "ensure we cover all config options"
|
||||||
@@ -3,7 +3,7 @@ from contextlib import closing
|
|||||||
import pytest
|
import pytest
|
||||||
from fixtures.neon_fixtures import NeonEnvBuilder, PgProtocol
|
from fixtures.neon_fixtures import NeonEnvBuilder, PgProtocol
|
||||||
from fixtures.pageserver.http import PageserverApiException
|
from fixtures.pageserver.http import PageserverApiException
|
||||||
from fixtures.types import TenantId
|
from fixtures.types import TenantId, TimelineId
|
||||||
|
|
||||||
|
|
||||||
def test_pageserver_auth(neon_env_builder: NeonEnvBuilder):
|
def test_pageserver_auth(neon_env_builder: NeonEnvBuilder):
|
||||||
@@ -25,21 +25,19 @@ def test_pageserver_auth(neon_env_builder: NeonEnvBuilder):
|
|||||||
ps.safe_psql("set FOO", password=tenant_token)
|
ps.safe_psql("set FOO", password=tenant_token)
|
||||||
ps.safe_psql("set FOO", password=pageserver_token)
|
ps.safe_psql("set FOO", password=pageserver_token)
|
||||||
|
|
||||||
new_timeline_id = env.neon_cli.create_branch(
|
|
||||||
"test_pageserver_auth", tenant_id=env.initial_tenant
|
|
||||||
)
|
|
||||||
|
|
||||||
# tenant can create branches
|
# tenant can create branches
|
||||||
tenant_http_client.timeline_create(
|
tenant_http_client.timeline_create(
|
||||||
pg_version=env.pg_version,
|
pg_version=env.pg_version,
|
||||||
tenant_id=env.initial_tenant,
|
tenant_id=env.initial_tenant,
|
||||||
ancestor_timeline_id=new_timeline_id,
|
new_timeline_id=TimelineId.generate(),
|
||||||
|
ancestor_timeline_id=env.initial_timeline,
|
||||||
)
|
)
|
||||||
# console can create branches for tenant
|
# console can create branches for tenant
|
||||||
pageserver_http_client.timeline_create(
|
pageserver_http_client.timeline_create(
|
||||||
pg_version=env.pg_version,
|
pg_version=env.pg_version,
|
||||||
tenant_id=env.initial_tenant,
|
tenant_id=env.initial_tenant,
|
||||||
ancestor_timeline_id=new_timeline_id,
|
new_timeline_id=TimelineId.generate(),
|
||||||
|
ancestor_timeline_id=env.initial_timeline,
|
||||||
)
|
)
|
||||||
|
|
||||||
# fail to create branch using token with different tenant_id
|
# fail to create branch using token with different tenant_id
|
||||||
@@ -49,18 +47,19 @@ def test_pageserver_auth(neon_env_builder: NeonEnvBuilder):
|
|||||||
invalid_tenant_http_client.timeline_create(
|
invalid_tenant_http_client.timeline_create(
|
||||||
pg_version=env.pg_version,
|
pg_version=env.pg_version,
|
||||||
tenant_id=env.initial_tenant,
|
tenant_id=env.initial_tenant,
|
||||||
ancestor_timeline_id=new_timeline_id,
|
new_timeline_id=TimelineId.generate(),
|
||||||
|
ancestor_timeline_id=env.initial_timeline,
|
||||||
)
|
)
|
||||||
|
|
||||||
# create tenant using management token
|
# create tenant using management token
|
||||||
pageserver_http_client.tenant_create()
|
pageserver_http_client.tenant_create(TenantId.generate())
|
||||||
|
|
||||||
# fail to create tenant using tenant token
|
# fail to create tenant using tenant token
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
PageserverApiException,
|
PageserverApiException,
|
||||||
match="Forbidden: Attempt to access management api with tenant scope. Permission denied",
|
match="Forbidden: Attempt to access management api with tenant scope. Permission denied",
|
||||||
):
|
):
|
||||||
tenant_http_client.tenant_create()
|
tenant_http_client.tenant_create(TenantId.generate())
|
||||||
|
|
||||||
|
|
||||||
def test_compute_auth_to_pageserver(neon_env_builder: NeonEnvBuilder):
|
def test_compute_auth_to_pageserver(neon_env_builder: NeonEnvBuilder):
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def test_broken_timeline(neon_env_builder: NeonEnvBuilder):
|
|||||||
".*is not active. Current state: Broken.*",
|
".*is not active. Current state: Broken.*",
|
||||||
".*will not become active. Current state: Broken.*",
|
".*will not become active. Current state: Broken.*",
|
||||||
".*failed to load metadata.*",
|
".*failed to load metadata.*",
|
||||||
".*could not load tenant.*load local timeline.*",
|
".*load failed.*load local timeline.*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
from types import TracebackType
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import psycopg2
|
||||||
|
import pytest
|
||||||
|
from fixtures.log_helper import log
|
||||||
|
from fixtures.neon_fixtures import VanillaPostgres
|
||||||
|
from pytest_httpserver import HTTPServer
|
||||||
|
from werkzeug.wrappers.request import Request
|
||||||
|
from werkzeug.wrappers.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
def handle_db(dbs, roles, operation):
|
||||||
|
if operation["op"] == "set":
|
||||||
|
if "old_name" in operation and operation["old_name"] in dbs:
|
||||||
|
dbs[operation["name"]] = dbs[operation["old_name"]]
|
||||||
|
dbs.pop(operation["old_name"])
|
||||||
|
if "owner" in operation:
|
||||||
|
dbs[operation["name"]] = operation["owner"]
|
||||||
|
elif operation["op"] == "del":
|
||||||
|
dbs.pop(operation["name"])
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid op")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_role(dbs, roles, operation):
|
||||||
|
if operation["op"] == "set":
|
||||||
|
if "old_name" in operation and operation["old_name"] in roles:
|
||||||
|
roles[operation["name"]] = roles[operation["old_name"]]
|
||||||
|
roles.pop(operation["old_name"])
|
||||||
|
for db, owner in dbs.items():
|
||||||
|
if owner == operation["old_name"]:
|
||||||
|
dbs[db] = operation["name"]
|
||||||
|
if "password" in operation:
|
||||||
|
roles[operation["name"]] = operation["password"]
|
||||||
|
elif operation["op"] == "del":
|
||||||
|
if "old_name" in operation:
|
||||||
|
roles.pop(operation["old_name"])
|
||||||
|
roles.pop(operation["name"])
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid op")
|
||||||
|
|
||||||
|
|
||||||
|
fail = False
|
||||||
|
|
||||||
|
|
||||||
|
def ddl_forward_handler(request: Request, dbs: Dict[str, str], roles: Dict[str, str]) -> Response:
|
||||||
|
log.info(f"Received request with data {request.get_data(as_text=True)}")
|
||||||
|
if fail:
|
||||||
|
log.info("FAILING")
|
||||||
|
return Response(status=500, response="Failed just cuz")
|
||||||
|
if request.json is None:
|
||||||
|
log.info("Received invalid JSON")
|
||||||
|
return Response(status=400)
|
||||||
|
json = request.json
|
||||||
|
# Handle roles first
|
||||||
|
if "roles" in json:
|
||||||
|
for operation in json["roles"]:
|
||||||
|
handle_role(dbs, roles, operation)
|
||||||
|
if "dbs" in json:
|
||||||
|
for operation in json["dbs"]:
|
||||||
|
handle_db(dbs, roles, operation)
|
||||||
|
return Response(status=200)
|
||||||
|
|
||||||
|
|
||||||
|
class DdlForwardingContext:
|
||||||
|
def __init__(self, httpserver: HTTPServer, vanilla_pg: VanillaPostgres, host: str, port: int):
|
||||||
|
self.server = httpserver
|
||||||
|
self.pg = vanilla_pg
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.dbs: Dict[str, str] = {}
|
||||||
|
self.roles: Dict[str, str] = {}
|
||||||
|
endpoint = "/management/api/v2/roles_and_databases"
|
||||||
|
ddl_url = f"http://{host}:{port}{endpoint}"
|
||||||
|
self.pg.configure(
|
||||||
|
[
|
||||||
|
f"neon.console_url={ddl_url}",
|
||||||
|
"shared_preload_libraries = 'neon'",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
log.info(f"Listening on {ddl_url}")
|
||||||
|
self.server.expect_request(endpoint, method="PATCH").respond_with_handler(
|
||||||
|
lambda request: ddl_forward_handler(request, self.dbs, self.roles)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.pg.start()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: Optional[Type[BaseException]],
|
||||||
|
exc: Optional[BaseException],
|
||||||
|
tb: Optional[TracebackType],
|
||||||
|
):
|
||||||
|
self.pg.stop()
|
||||||
|
|
||||||
|
def send(self, query: str) -> List[Tuple[Any, ...]]:
|
||||||
|
return self.pg.safe_psql(query)
|
||||||
|
|
||||||
|
def wait(self, timeout=3):
|
||||||
|
self.server.wait(timeout=timeout)
|
||||||
|
|
||||||
|
def send_and_wait(self, query: str, timeout=3) -> List[Tuple[Any, ...]]:
|
||||||
|
res = self.send(query)
|
||||||
|
self.wait(timeout=timeout)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def ddl(
|
||||||
|
httpserver: HTTPServer, vanilla_pg: VanillaPostgres, httpserver_listen_address: tuple[str, int]
|
||||||
|
):
|
||||||
|
(host, port) = httpserver_listen_address
|
||||||
|
with DdlForwardingContext(httpserver, vanilla_pg, host, port) as ddl:
|
||||||
|
yield ddl
|
||||||
|
|
||||||
|
|
||||||
|
def test_ddl_forwarding(ddl: DdlForwardingContext):
|
||||||
|
curr_user = ddl.send("SELECT current_user")[0][0]
|
||||||
|
log.info(f"Current user is {curr_user}")
|
||||||
|
ddl.send_and_wait("CREATE DATABASE bork")
|
||||||
|
assert ddl.dbs == {"bork": curr_user}
|
||||||
|
ddl.send_and_wait("CREATE ROLE volk WITH PASSWORD 'nu_zayats'")
|
||||||
|
ddl.send_and_wait("ALTER DATABASE bork RENAME TO nu_pogodi")
|
||||||
|
assert ddl.dbs == {"nu_pogodi": curr_user}
|
||||||
|
ddl.send_and_wait("ALTER DATABASE nu_pogodi OWNER TO volk")
|
||||||
|
assert ddl.dbs == {"nu_pogodi": "volk"}
|
||||||
|
ddl.send_and_wait("DROP DATABASE nu_pogodi")
|
||||||
|
assert ddl.dbs == {}
|
||||||
|
ddl.send_and_wait("DROP ROLE volk")
|
||||||
|
assert ddl.roles == {}
|
||||||
|
|
||||||
|
ddl.send_and_wait("CREATE ROLE tarzan WITH PASSWORD 'of_the_apes'")
|
||||||
|
assert ddl.roles == {"tarzan": "of_the_apes"}
|
||||||
|
ddl.send_and_wait("DROP ROLE tarzan")
|
||||||
|
assert ddl.roles == {}
|
||||||
|
ddl.send_and_wait("CREATE ROLE tarzan WITH PASSWORD 'of_the_apes'")
|
||||||
|
assert ddl.roles == {"tarzan": "of_the_apes"}
|
||||||
|
ddl.send_and_wait("ALTER ROLE tarzan WITH PASSWORD 'jungle_man'")
|
||||||
|
assert ddl.roles == {"tarzan": "jungle_man"}
|
||||||
|
ddl.send_and_wait("ALTER ROLE tarzan RENAME TO mowgli")
|
||||||
|
assert ddl.roles == {"mowgli": "jungle_man"}
|
||||||
|
ddl.send_and_wait("DROP ROLE mowgli")
|
||||||
|
assert ddl.roles == {}
|
||||||
|
|
||||||
|
conn = ddl.pg.connect()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
cur.execute("BEGIN")
|
||||||
|
cur.execute("CREATE ROLE bork WITH PASSWORD 'cork'")
|
||||||
|
cur.execute("COMMIT")
|
||||||
|
ddl.wait()
|
||||||
|
assert ddl.roles == {"bork": "cork"}
|
||||||
|
cur.execute("BEGIN")
|
||||||
|
cur.execute("CREATE ROLE stork WITH PASSWORD 'pork'")
|
||||||
|
cur.execute("ABORT")
|
||||||
|
ddl.wait()
|
||||||
|
assert ("stork", "pork") not in ddl.roles.items()
|
||||||
|
cur.execute("BEGIN")
|
||||||
|
cur.execute("ALTER ROLE bork WITH PASSWORD 'pork'")
|
||||||
|
cur.execute("ALTER ROLE bork RENAME TO stork")
|
||||||
|
cur.execute("COMMIT")
|
||||||
|
ddl.wait()
|
||||||
|
assert ddl.roles == {"stork": "pork"}
|
||||||
|
cur.execute("BEGIN")
|
||||||
|
cur.execute("CREATE ROLE dork WITH PASSWORD 'york'")
|
||||||
|
cur.execute("SAVEPOINT point")
|
||||||
|
cur.execute("ALTER ROLE dork WITH PASSWORD 'zork'")
|
||||||
|
cur.execute("ALTER ROLE dork RENAME TO fork")
|
||||||
|
cur.execute("ROLLBACK TO SAVEPOINT point")
|
||||||
|
cur.execute("ALTER ROLE dork WITH PASSWORD 'fork'")
|
||||||
|
cur.execute("ALTER ROLE dork RENAME TO zork")
|
||||||
|
cur.execute("RELEASE SAVEPOINT point")
|
||||||
|
cur.execute("COMMIT")
|
||||||
|
ddl.wait()
|
||||||
|
assert ddl.roles == {"stork": "pork", "zork": "fork"}
|
||||||
|
|
||||||
|
cur.execute("DROP ROLE stork")
|
||||||
|
cur.execute("DROP ROLE zork")
|
||||||
|
ddl.wait()
|
||||||
|
assert ddl.roles == {}
|
||||||
|
|
||||||
|
cur.execute("CREATE ROLE bork WITH PASSWORD 'dork'")
|
||||||
|
cur.execute("CREATE ROLE stork WITH PASSWORD 'cork'")
|
||||||
|
cur.execute("BEGIN")
|
||||||
|
cur.execute("DROP ROLE bork")
|
||||||
|
cur.execute("ALTER ROLE stork RENAME TO bork")
|
||||||
|
cur.execute("COMMIT")
|
||||||
|
ddl.wait()
|
||||||
|
assert ddl.roles == {"bork": "cork"}
|
||||||
|
|
||||||
|
cur.execute("DROP ROLE bork")
|
||||||
|
ddl.wait()
|
||||||
|
assert ddl.roles == {}
|
||||||
|
|
||||||
|
cur.execute("CREATE ROLE bork WITH PASSWORD 'dork'")
|
||||||
|
cur.execute("CREATE DATABASE stork WITH OWNER=bork")
|
||||||
|
cur.execute("ALTER ROLE bork RENAME TO cork")
|
||||||
|
ddl.wait()
|
||||||
|
assert ddl.dbs == {"stork": "cork"}
|
||||||
|
|
||||||
|
with pytest.raises(psycopg2.InternalError):
|
||||||
|
global fail
|
||||||
|
fail = True
|
||||||
|
cur.execute("CREATE DATABASE failure WITH OWNER=cork")
|
||||||
|
ddl.wait()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
@@ -228,7 +228,6 @@ def proxy_with_metric_collector(
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_proxy_metric_collection(
|
async def test_proxy_metric_collection(
|
||||||
httpserver: HTTPServer,
|
httpserver: HTTPServer,
|
||||||
httpserver_listen_address,
|
|
||||||
proxy_with_metric_collector: NeonProxy,
|
proxy_with_metric_collector: NeonProxy,
|
||||||
vanilla_pg: VanillaPostgres,
|
vanilla_pg: VanillaPostgres,
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ def test_remote_storage_backup_and_restore(
|
|||||||
# This is before the failures injected by test_remote_failures, so it's a permanent error.
|
# This is before the failures injected by test_remote_failures, so it's a permanent error.
|
||||||
pageserver_http.configure_failpoints(("storage-sync-list-remote-timelines", "return"))
|
pageserver_http.configure_failpoints(("storage-sync-list-remote-timelines", "return"))
|
||||||
env.pageserver.allowed_errors.append(
|
env.pageserver.allowed_errors.append(
|
||||||
".*error attaching tenant: storage-sync-list-remote-timelines",
|
".*attach failed.*: storage-sync-list-remote-timelines",
|
||||||
)
|
)
|
||||||
# Attach it. This HTTP request will succeed and launch a
|
# Attach it. This HTTP request will succeed and launch a
|
||||||
# background task to load the tenant. In that background task,
|
# background task to load the tenant. In that background task,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
|||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
import backoff # type: ignore
|
import backoff
|
||||||
from fixtures.log_helper import log
|
from fixtures.log_helper import log
|
||||||
from fixtures.neon_fixtures import PgProtocol, PortDistributor, VanillaPostgres
|
from fixtures.neon_fixtures import PgProtocol, PortDistributor, VanillaPostgres
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ class PgSniRouter(PgProtocol):
|
|||||||
destination: str,
|
destination: str,
|
||||||
tls_cert: Path,
|
tls_cert: Path,
|
||||||
tls_key: Path,
|
tls_key: Path,
|
||||||
|
test_output_dir: Path,
|
||||||
):
|
):
|
||||||
# Must use a hostname rather than IP here, for SNI to work
|
# Must use a hostname rather than IP here, for SNI to work
|
||||||
host = "localhost"
|
host = "localhost"
|
||||||
@@ -49,6 +50,7 @@ class PgSniRouter(PgProtocol):
|
|||||||
self.tls_cert = tls_cert
|
self.tls_cert = tls_cert
|
||||||
self.tls_key = tls_key
|
self.tls_key = tls_key
|
||||||
self._popen: Optional[subprocess.Popen[bytes]] = None
|
self._popen: Optional[subprocess.Popen[bytes]] = None
|
||||||
|
self.test_output_dir = test_output_dir
|
||||||
|
|
||||||
def start(self) -> "PgSniRouter":
|
def start(self) -> "PgSniRouter":
|
||||||
assert self._popen is None
|
assert self._popen is None
|
||||||
@@ -60,8 +62,12 @@ class PgSniRouter(PgProtocol):
|
|||||||
*["--destination", self.destination],
|
*["--destination", self.destination],
|
||||||
]
|
]
|
||||||
|
|
||||||
self._popen = subprocess.Popen(args)
|
router_log_path = self.test_output_dir / "pg_sni_router.log"
|
||||||
|
router_log = open(router_log_path, "w")
|
||||||
|
|
||||||
|
self._popen = subprocess.Popen(args, stderr=router_log)
|
||||||
self._wait_until_ready()
|
self._wait_until_ready()
|
||||||
|
log.info(f"pg_sni_router started, log file: {router_log_path}")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@backoff.on_exception(backoff.expo, OSError, max_time=10)
|
@backoff.on_exception(backoff.expo, OSError, max_time=10)
|
||||||
@@ -121,6 +127,7 @@ def test_pg_sni_router(
|
|||||||
destination="localtest.me",
|
destination="localtest.me",
|
||||||
tls_cert=test_output_dir / "router.crt",
|
tls_cert=test_output_dir / "router.crt",
|
||||||
tls_key=test_output_dir / "router.key",
|
tls_key=test_output_dir / "router.key",
|
||||||
|
test_output_dir=test_output_dir,
|
||||||
) as router:
|
) as router:
|
||||||
router.start()
|
router.start()
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ eviction_policy = { "kind" = "LayerAccessThreshold", period = "20s", threshold =
|
|||||||
log.info(f"show {env.initial_tenant}")
|
log.info(f"show {env.initial_tenant}")
|
||||||
pscur.execute(f"show {env.initial_tenant}")
|
pscur.execute(f"show {env.initial_tenant}")
|
||||||
res = pscur.fetchone()
|
res = pscur.fetchone()
|
||||||
|
assert res is not None
|
||||||
assert all(
|
assert all(
|
||||||
i in res.items()
|
i in res.items()
|
||||||
for i in {
|
for i in {
|
||||||
@@ -101,6 +102,7 @@ eviction_policy = { "kind" = "LayerAccessThreshold", period = "20s", threshold =
|
|||||||
pscur.execute(f"show {tenant}")
|
pscur.execute(f"show {tenant}")
|
||||||
res = pscur.fetchone()
|
res = pscur.fetchone()
|
||||||
log.info(f"res: {res}")
|
log.info(f"res: {res}")
|
||||||
|
assert res is not None
|
||||||
assert all(
|
assert all(
|
||||||
i in res.items()
|
i in res.items()
|
||||||
for i in {
|
for i in {
|
||||||
@@ -163,6 +165,7 @@ eviction_policy = { "kind" = "LayerAccessThreshold", period = "20s", threshold =
|
|||||||
pscur.execute(f"show {tenant}")
|
pscur.execute(f"show {tenant}")
|
||||||
res = pscur.fetchone()
|
res = pscur.fetchone()
|
||||||
log.info(f"after config res: {res}")
|
log.info(f"after config res: {res}")
|
||||||
|
assert res is not None
|
||||||
assert all(
|
assert all(
|
||||||
i in res.items()
|
i in res.items()
|
||||||
for i in {
|
for i in {
|
||||||
@@ -218,6 +221,7 @@ eviction_policy = { "kind" = "LayerAccessThreshold", period = "20s", threshold =
|
|||||||
pscur.execute(f"show {tenant}")
|
pscur.execute(f"show {tenant}")
|
||||||
res = pscur.fetchone()
|
res = pscur.fetchone()
|
||||||
log.info(f"after restart res: {res}")
|
log.info(f"after restart res: {res}")
|
||||||
|
assert res is not None
|
||||||
assert all(
|
assert all(
|
||||||
i in res.items()
|
i in res.items()
|
||||||
for i in {
|
for i in {
|
||||||
@@ -278,6 +282,7 @@ eviction_policy = { "kind" = "LayerAccessThreshold", period = "20s", threshold =
|
|||||||
pscur.execute(f"show {tenant}")
|
pscur.execute(f"show {tenant}")
|
||||||
res = pscur.fetchone()
|
res = pscur.fetchone()
|
||||||
log.info(f"after restart res: {res}")
|
log.info(f"after restart res: {res}")
|
||||||
|
assert res is not None
|
||||||
assert all(
|
assert all(
|
||||||
i in res.items()
|
i in res.items()
|
||||||
for i in {
|
for i in {
|
||||||
|
|||||||
@@ -647,7 +647,9 @@ def test_ignored_tenant_stays_broken_without_metadata(
|
|||||||
metadata_removed = True
|
metadata_removed = True
|
||||||
assert metadata_removed, f"Failed to find metadata file in {tenant_timeline_dir}"
|
assert metadata_removed, f"Failed to find metadata file in {tenant_timeline_dir}"
|
||||||
|
|
||||||
env.pageserver.allowed_errors.append(".*could not load tenant .*?: failed to load metadata.*")
|
env.pageserver.allowed_errors.append(
|
||||||
|
f".*{tenant_id}.*: load failed.*: failed to load metadata.*"
|
||||||
|
)
|
||||||
|
|
||||||
# now, load it from the local files and expect it to be broken due to inability to load tenant files into memory
|
# now, load it from the local files and expect it to be broken due to inability to load tenant files into memory
|
||||||
pageserver_http.tenant_load(tenant_id=tenant_id)
|
pageserver_http.tenant_load(tenant_id=tenant_id)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from fixtures.neon_fixtures import (
|
|||||||
available_remote_storages,
|
available_remote_storages,
|
||||||
)
|
)
|
||||||
from fixtures.types import Lsn, TenantId, TimelineId
|
from fixtures.types import Lsn, TenantId, TimelineId
|
||||||
|
from fixtures.utils import wait_until
|
||||||
from prometheus_client.samples import Sample
|
from prometheus_client.samples import Sample
|
||||||
|
|
||||||
|
|
||||||
@@ -308,27 +309,26 @@ def test_pageserver_with_empty_tenants(
|
|||||||
env.pageserver.allowed_errors.append(
|
env.pageserver.allowed_errors.append(
|
||||||
".*marking .* as locally complete, while it doesnt exist in remote index.*"
|
".*marking .* as locally complete, while it doesnt exist in remote index.*"
|
||||||
)
|
)
|
||||||
env.pageserver.allowed_errors.append(
|
env.pageserver.allowed_errors.append(".*load failed.*list timelines directory.*")
|
||||||
".*could not load tenant.*Failed to list timelines directory.*"
|
|
||||||
)
|
|
||||||
|
|
||||||
client = env.pageserver.http_client()
|
client = env.pageserver.http_client()
|
||||||
|
|
||||||
tenant_with_empty_timelines_dir = client.tenant_create()
|
tenant_with_empty_timelines = TenantId.generate()
|
||||||
temp_timelines = client.timeline_list(tenant_with_empty_timelines_dir)
|
client.tenant_create(tenant_with_empty_timelines)
|
||||||
|
temp_timelines = client.timeline_list(tenant_with_empty_timelines)
|
||||||
for temp_timeline in temp_timelines:
|
for temp_timeline in temp_timelines:
|
||||||
client.timeline_delete(
|
client.timeline_delete(
|
||||||
tenant_with_empty_timelines_dir, TimelineId(temp_timeline["timeline_id"])
|
tenant_with_empty_timelines, TimelineId(temp_timeline["timeline_id"])
|
||||||
)
|
)
|
||||||
files_in_timelines_dir = sum(
|
files_in_timelines_dir = sum(
|
||||||
1
|
1
|
||||||
for _p in Path.iterdir(
|
for _p in Path.iterdir(
|
||||||
Path(env.repo_dir) / "tenants" / str(tenant_with_empty_timelines_dir) / "timelines"
|
Path(env.repo_dir) / "tenants" / str(tenant_with_empty_timelines) / "timelines"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
files_in_timelines_dir == 0
|
files_in_timelines_dir == 0
|
||||||
), f"Tenant {tenant_with_empty_timelines_dir} should have an empty timelines/ directory"
|
), f"Tenant {tenant_with_empty_timelines} should have an empty timelines/ directory"
|
||||||
|
|
||||||
# Trigger timeline re-initialization after pageserver restart
|
# Trigger timeline re-initialization after pageserver restart
|
||||||
env.endpoints.stop_all()
|
env.endpoints.stop_all()
|
||||||
@@ -340,9 +340,15 @@ def test_pageserver_with_empty_tenants(
|
|||||||
env.pageserver.start()
|
env.pageserver.start()
|
||||||
|
|
||||||
client = env.pageserver.http_client()
|
client = env.pageserver.http_client()
|
||||||
tenants = client.tenant_list()
|
|
||||||
|
|
||||||
assert len(tenants) == 2
|
def not_loading():
|
||||||
|
tenants = client.tenant_list()
|
||||||
|
assert len(tenants) == 2
|
||||||
|
assert all(t["state"]["slug"] != "Loading" for t in tenants)
|
||||||
|
|
||||||
|
wait_until(10, 0.2, not_loading)
|
||||||
|
|
||||||
|
tenants = client.tenant_list()
|
||||||
|
|
||||||
[broken_tenant] = [t for t in tenants if t["id"] == str(tenant_without_timelines_dir)]
|
[broken_tenant] = [t for t in tenants if t["id"] == str(tenant_without_timelines_dir)]
|
||||||
assert (
|
assert (
|
||||||
@@ -354,17 +360,17 @@ def test_pageserver_with_empty_tenants(
|
|||||||
broken_tenant_status["state"]["slug"] == "Broken"
|
broken_tenant_status["state"]["slug"] == "Broken"
|
||||||
), f"Tenant {tenant_without_timelines_dir} without timelines dir should be broken"
|
), f"Tenant {tenant_without_timelines_dir} without timelines dir should be broken"
|
||||||
|
|
||||||
assert env.pageserver.log_contains(".*Setting tenant as Broken state, reason:.*")
|
assert env.pageserver.log_contains(".*load failed, setting tenant state to Broken:.*")
|
||||||
|
|
||||||
[loaded_tenant] = [t for t in tenants if t["id"] == str(tenant_with_empty_timelines_dir)]
|
[loaded_tenant] = [t for t in tenants if t["id"] == str(tenant_with_empty_timelines)]
|
||||||
assert (
|
assert (
|
||||||
loaded_tenant["state"]["slug"] == "Active"
|
loaded_tenant["state"]["slug"] == "Active"
|
||||||
), "Tenant {tenant_with_empty_timelines_dir} with empty timelines dir should be active and ready for timeline creation"
|
), "Tenant {tenant_with_empty_timelines} with empty timelines dir should be active and ready for timeline creation"
|
||||||
|
|
||||||
loaded_tenant_status = client.tenant_status(tenant_with_empty_timelines_dir)
|
loaded_tenant_status = client.tenant_status(tenant_with_empty_timelines)
|
||||||
assert (
|
assert (
|
||||||
loaded_tenant_status["state"]["slug"] == "Active"
|
loaded_tenant_status["state"]["slug"] == "Active"
|
||||||
), f"Tenant {tenant_with_empty_timelines_dir} without timelines dir should be active"
|
), f"Tenant {tenant_with_empty_timelines} without timelines dir should be active"
|
||||||
|
|
||||||
time.sleep(1) # to allow metrics propagation
|
time.sleep(1) # to allow metrics propagation
|
||||||
|
|
||||||
@@ -374,7 +380,7 @@ def test_pageserver_with_empty_tenants(
|
|||||||
"state": "Broken",
|
"state": "Broken",
|
||||||
}
|
}
|
||||||
active_tenants_metric_filter = {
|
active_tenants_metric_filter = {
|
||||||
"tenant_id": str(tenant_with_empty_timelines_dir),
|
"tenant_id": str(tenant_with_empty_timelines),
|
||||||
"state": "Active",
|
"state": "Active",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,7 +392,7 @@ def test_pageserver_with_empty_tenants(
|
|||||||
|
|
||||||
assert (
|
assert (
|
||||||
tenant_active_count == 1
|
tenant_active_count == 1
|
||||||
), f"Tenant {tenant_with_empty_timelines_dir} should have metric as active"
|
), f"Tenant {tenant_with_empty_timelines} should have metric as active"
|
||||||
|
|
||||||
tenant_broken_count = int(
|
tenant_broken_count = int(
|
||||||
ps_metrics.query_one(
|
ps_metrics.query_one(
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ def test_concurrent_timeline_delete_if_first_stuck_at_index_upload(
|
|||||||
|
|
||||||
# make the second call and assert behavior
|
# make the second call and assert behavior
|
||||||
log.info("second call start")
|
log.info("second call start")
|
||||||
error_msg_re = "another task is already setting the deleted_flag, started at"
|
error_msg_re = "timeline deletion is already in progress"
|
||||||
with pytest.raises(PageserverApiException, match=error_msg_re) as second_call_err:
|
with pytest.raises(PageserverApiException, match=error_msg_re) as second_call_err:
|
||||||
ps_http.timeline_delete(env.initial_tenant, child_timeline_id)
|
ps_http.timeline_delete(env.initial_tenant, child_timeline_id)
|
||||||
assert second_call_err.value.status_code == 500
|
assert second_call_err.value.status_code == 500
|
||||||
@@ -437,12 +437,22 @@ def test_delete_timeline_client_hangup(neon_env_builder: NeonEnvBuilder):
|
|||||||
|
|
||||||
wait_until(50, 0.1, got_hangup_log_message)
|
wait_until(50, 0.1, got_hangup_log_message)
|
||||||
|
|
||||||
# ok, retry without failpoint, it should succeed
|
# check that the timeline is still present
|
||||||
|
ps_http.timeline_detail(env.initial_tenant, child_timeline_id)
|
||||||
|
|
||||||
|
# ok, disable the failpoint to let the deletion finish
|
||||||
ps_http.configure_failpoints((failpoint_name, "off"))
|
ps_http.configure_failpoints((failpoint_name, "off"))
|
||||||
|
|
||||||
# this should succeed
|
def first_request_finished():
|
||||||
ps_http.timeline_delete(env.initial_tenant, child_timeline_id, timeout=2)
|
message = f".*DELETE.*{child_timeline_id}.*Cancelled request finished"
|
||||||
# the second call will try to transition the timeline into Stopping state, but it's already in that state
|
assert env.pageserver.log_contains(message)
|
||||||
env.pageserver.allowed_errors.append(
|
|
||||||
f".*{child_timeline_id}.*Ignoring new state, equal to the existing one: Stopping"
|
wait_until(50, 0.1, first_request_finished)
|
||||||
)
|
|
||||||
|
# check that the timeline is gone
|
||||||
|
notfound_message = f"Timeline {env.initial_tenant}/{child_timeline_id} was not found"
|
||||||
|
env.pageserver.allowed_errors.append(".*" + notfound_message)
|
||||||
|
with pytest.raises(PageserverApiException, match=notfound_message) as exc:
|
||||||
|
ps_http.timeline_detail(env.initial_tenant, child_timeline_id)
|
||||||
|
|
||||||
|
assert exc.value.status_code == 404
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ futures-core = { version = "0.3" }
|
|||||||
futures-executor = { version = "0.3" }
|
futures-executor = { version = "0.3" }
|
||||||
futures-sink = { version = "0.3" }
|
futures-sink = { version = "0.3" }
|
||||||
futures-util = { version = "0.3", features = ["channel", "io", "sink"] }
|
futures-util = { version = "0.3", features = ["channel", "io", "sink"] }
|
||||||
hashbrown = { version = "0.12", features = ["raw"] }
|
|
||||||
itertools = { version = "0.10" }
|
itertools = { version = "0.10" }
|
||||||
libc = { version = "0.2", features = ["extra_traits"] }
|
libc = { version = "0.2", features = ["extra_traits"] }
|
||||||
log = { version = "0.4", default-features = false, features = ["std"] }
|
log = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
@@ -39,7 +38,7 @@ num-traits = { version = "0.2", features = ["i128"] }
|
|||||||
prost = { version = "0.11" }
|
prost = { version = "0.11" }
|
||||||
rand = { version = "0.8", features = ["small_rng"] }
|
rand = { version = "0.8", features = ["small_rng"] }
|
||||||
regex = { version = "1" }
|
regex = { version = "1" }
|
||||||
regex-syntax = { version = "0.6" }
|
regex-syntax = { version = "0.7" }
|
||||||
reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] }
|
reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] }
|
||||||
ring = { version = "0.16", features = ["std"] }
|
ring = { version = "0.16", features = ["std"] }
|
||||||
rustls = { version = "0.20", features = ["dangerous_configuration"] }
|
rustls = { version = "0.20", features = ["dangerous_configuration"] }
|
||||||
@@ -62,7 +61,6 @@ url = { version = "2", features = ["serde"] }
|
|||||||
anyhow = { version = "1", features = ["backtrace"] }
|
anyhow = { version = "1", features = ["backtrace"] }
|
||||||
bytes = { version = "1", features = ["serde"] }
|
bytes = { version = "1", features = ["serde"] }
|
||||||
either = { version = "1" }
|
either = { version = "1" }
|
||||||
hashbrown = { version = "0.12", features = ["raw"] }
|
|
||||||
itertools = { version = "0.10" }
|
itertools = { version = "0.10" }
|
||||||
libc = { version = "0.2", features = ["extra_traits"] }
|
libc = { version = "0.2", features = ["extra_traits"] }
|
||||||
log = { version = "0.4", default-features = false, features = ["std"] }
|
log = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
@@ -70,7 +68,7 @@ memchr = { version = "2" }
|
|||||||
nom = { version = "7" }
|
nom = { version = "7" }
|
||||||
prost = { version = "0.11" }
|
prost = { version = "0.11" }
|
||||||
regex = { version = "1" }
|
regex = { version = "1" }
|
||||||
regex-syntax = { version = "0.6" }
|
regex-syntax = { version = "0.7" }
|
||||||
serde = { version = "1", features = ["alloc", "derive"] }
|
serde = { version = "1", features = ["alloc", "derive"] }
|
||||||
syn-dff4ba8e3ae991db = { package = "syn", version = "1", features = ["extra-traits", "full", "visit", "visit-mut"] }
|
syn-dff4ba8e3ae991db = { package = "syn", version = "1", features = ["extra-traits", "full", "visit", "visit-mut"] }
|
||||||
syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "full", "visit-mut"] }
|
syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "full", "visit-mut"] }
|
||||||
|
|||||||
Reference in New Issue
Block a user