Compare commits

..

21 Commits

Author SHA1 Message Date
Alek Westover
34ac170904 update comments 2023-06-15 12:33:53 -04:00
Alek Westover
86d7cb674d update extensions to use my code 2023-06-14 10:11:57 -04:00
Alek Westover
7d3a7091df made the s3bucket list_files function much better 2023-06-14 09:53:08 -04:00
Alek Westover
d6848d53eb rename file 2023-06-14 08:49:24 -04:00
Alek Westover
4b21d23785 improvement to S3 bucket list_files 2023-06-14 08:48:25 -04:00
Alek Westover
30815582a7 improvements to S3 bucket list_files 2023-06-14 08:48:06 -04:00
Alek Westover
0c515ac034 minor fixes 2023-06-13 15:46:21 -04:00
Alek Westover
fa6472e2a1 looking a lot better 2023-06-13 15:24:54 -04:00
Alek Westover
fc35a19ede minor changes 2023-06-13 14:55:17 -04:00
Alek Westover
79459e8c0a realized that we actually need to add a new function to remote_storage for listing files. working on it 2023-06-13 14:51:00 -04:00
Alek Westover
5df798c454 ready 2023-06-12 14:05:39 -04:00
Alek Westover
eebe9c513f get ready for next step 2023-06-12 14:04:12 -04:00
Alek Westover
6990102bb2 revert small change 2023-06-12 13:59:04 -04:00
Alek Westover
a5e8e38bc5 more MWEs, stuff is starting to work 2023-06-12 13:54:21 -04:00
Alek Westover
77217a473d ammending comit 2023-06-09 14:16:42 -04:00
Alek Westover
6f0246372a remove comment 2023-06-08 15:28:17 -04:00
Alek Westover
77aa65f2f2 added suppport for globbing 2023-06-08 14:49:44 -04:00
Alek Westover
38bed024f2 Merge branch 'main' of github.com:neondatabase/neon into pg-extensions 2023-06-07 17:17:10 -04:00
Alek Westover
40089beac5 made it compile; the code is not nice yet, but first am going to try to get it to work 2023-06-07 09:34:33 -04:00
Alek Westover
bf033294b1 fixed most of the errors preventing it from building 2023-06-06 16:33:08 -04:00
Alek Westover
fb6a942665 drafted some modifications to compute_ctl to add support for downloading pg extensions. not tested yet. 2023-06-06 15:39:05 -04:00
300 changed files with 10157 additions and 15296 deletions

View File

@@ -12,11 +12,6 @@ opt-level = 3
# Turn on a small amount of optimization in Development mode. # Turn on a small amount of optimization in Development mode.
opt-level = 1 opt-level = 1
[build]
# This is only present for local builds, as it will be overridden
# by the RUSTDOCFLAGS env var in CI.
rustdocflags = ["-Arustdoc::private_intra_doc_links"]
[alias] [alias]
build_testing = ["build", "--features", "testing"] build_testing = ["build", "--features", "testing"]
neon = ["run", "--bin", "neon_local"] neon = ["run", "--bin", "neon_local"]

View File

@@ -21,5 +21,4 @@
!workspace_hack/ !workspace_hack/
!neon_local/ !neon_local/
!scripts/ninstall.sh !scripts/ninstall.sh
!scripts/combine_control_files.py
!vm-cgconfig.conf !vm-cgconfig.conf

View File

@@ -105,7 +105,7 @@ runs:
# Get previously uploaded data for this run # Get previously uploaded data for this run
ZSTD_NBTHREADS=0 ZSTD_NBTHREADS=0
S3_FILEPATHS=$(aws s3api list-objects-v2 --bucket ${BUCKET} --prefix ${RAW_PREFIX}/ | jq --raw-output '.Contents[]?.Key') S3_FILEPATHS=$(aws s3api list-objects-v2 --bucket ${BUCKET} --prefix ${RAW_PREFIX}/ | jq --raw-output '.Contents[].Key')
if [ -z "$S3_FILEPATHS" ]; then if [ -z "$S3_FILEPATHS" ]; then
# There's no previously uploaded data for this $GITHUB_RUN_ID # There's no previously uploaded data for this $GITHUB_RUN_ID
exit 0 exit 0

View File

@@ -150,14 +150,6 @@ runs:
EXTRA_PARAMS="--flaky-tests-json $TEST_OUTPUT/flaky.json $EXTRA_PARAMS" EXTRA_PARAMS="--flaky-tests-json $TEST_OUTPUT/flaky.json $EXTRA_PARAMS"
fi fi
# We use pytest-split plugin to run benchmarks in parallel on different CI runners
if [ "${TEST_SELECTION}" = "test_runner/performance" ] && [ "${{ inputs.build_type }}" != "remote" ]; then
mkdir -p $TEST_OUTPUT
poetry run ./scripts/benchmark_durations.py "${TEST_RESULT_CONNSTR}" --days 10 --output "$TEST_OUTPUT/benchmark_durations.json"
EXTRA_PARAMS="--durations-path $TEST_OUTPUT/benchmark_durations.json $EXTRA_PARAMS"
fi
if [[ "${{ inputs.build_type }}" == "debug" ]]; then if [[ "${{ inputs.build_type }}" == "debug" ]]; then
cov_prefix=(scripts/coverage "--profraw-prefix=$GITHUB_JOB" --dir=/tmp/coverage run) cov_prefix=(scripts/coverage "--profraw-prefix=$GITHUB_JOB" --dir=/tmp/coverage run)
elif [[ "${{ inputs.build_type }}" == "release" ]]; then elif [[ "${{ inputs.build_type }}" == "release" ]]; then
@@ -209,4 +201,4 @@ runs:
uses: ./.github/actions/allure-report-store uses: ./.github/actions/allure-report-store
with: with:
report-dir: /tmp/test_output/allure/results report-dir: /tmp/test_output/allure/results
unique-key: ${{ inputs.build_type }}-${{ inputs.pg_version }} unique-key: ${{ inputs.build_type }}

View File

@@ -1,55 +0,0 @@
name: Handle `approved-for-ci-run` label
# This workflow helps to run CI pipeline for PRs made by external contributors (from forks).
on:
pull_request:
types:
# Default types that triggers a workflow ([1]):
# - [1] https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
- opened
- synchronize
- reopened
# Types that we wand to handle in addition to keep labels tidy:
- closed
# Actual magic happens here:
- labeled
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
jobs:
remove-label:
# Remove `approved-for-ci-run` label if the workflow is triggered by changes in a PR.
# The PR should be reviewed and labelled manually again.
runs-on: [ ubuntu-latest ]
if: |
contains(fromJSON('["opened", "synchronize", "reopened", "closed"]'), github.event.action) &&
contains(github.event.pull_request.labels.*.name, 'approved-for-ci-run')
steps:
- run: gh pr --repo "${GITHUB_REPOSITORY}" edit "${PR_NUMBER}" --remove-label "approved-for-ci-run"
create-branch:
# Create a local branch for an `approved-for-ci-run` labelled PR to run CI pipeline in it.
runs-on: [ ubuntu-latest ]
if: |
github.event.action == 'labeled' &&
contains(github.event.pull_request.labels.*.name, 'approved-for-ci-run')
steps:
- run: gh pr --repo "${GITHUB_REPOSITORY}" edit "${PR_NUMBER}" --remove-label "approved-for-ci-run"
- uses: actions/checkout@v3
with:
ref: main
- run: gh pr checkout "${PR_NUMBER}"
- run: git checkout -b "ci-run/pr-${PR_NUMBER}"
- run: git push --force origin "ci-run/pr-${PR_NUMBER}"

View File

@@ -180,8 +180,7 @@ jobs:
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
options: --init options: --init
# Increase timeout to 8h, default timeout is 6h timeout-minutes: 360 # 6h
timeout-minutes: 480
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -322,6 +321,8 @@ jobs:
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
options: --init options: --init
timeout-minutes: 360 # 6h
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -413,6 +414,8 @@ jobs:
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
options: --init options: --init
timeout-minutes: 360 # 6h
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -498,6 +501,8 @@ jobs:
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
options: --init options: --init
timeout-minutes: 360 # 6h
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@@ -5,7 +5,6 @@ on:
branches: branches:
- main - main
- release - release
- ci-run/pr-*
pull_request: pull_request:
defaults: defaults:
@@ -128,11 +127,6 @@ jobs:
- name: Run cargo clippy (release) - name: Run cargo clippy (release)
run: cargo hack --feature-powerset clippy --release $CLIPPY_COMMON_ARGS run: cargo hack --feature-powerset clippy --release $CLIPPY_COMMON_ARGS
- name: Check documentation generation
run: cargo doc --workspace --no-deps --document-private-items
env:
RUSTDOCFLAGS: "-Dwarnings -Arustdoc::private_intra_doc_links"
# Use `${{ !cancelled() }}` to run quck tests after the longer clippy run # Use `${{ !cancelled() }}` to run quck tests after the longer clippy run
- name: Check formatting - name: Check formatting
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
@@ -161,7 +155,7 @@ jobs:
build_type: [ debug, release ] build_type: [ debug, release ]
env: env:
BUILD_TYPE: ${{ matrix.build_type }} BUILD_TYPE: ${{ matrix.build_type }}
GIT_VERSION: ${{ github.event.pull_request.head.sha || github.sha }} GIT_VERSION: ${{ github.sha }}
steps: steps:
- name: Fix git ownership - name: Fix git ownership
@@ -180,27 +174,6 @@ jobs:
submodules: true submodules: true
fetch-depth: 1 fetch-depth: 1
- name: Check Postgres submodules revision
shell: bash -euo pipefail {0}
run: |
# This is a temporary solution to ensure that the Postgres submodules revision is correct (i.e. the updated intentionally).
# Eventually it will be replaced by a regression test https://github.com/neondatabase/neon/pull/4603
FAILED=false
for postgres in postgres-v14 postgres-v15; do
expected=$(cat vendor/revisions.json | jq --raw-output '."'"${postgres}"'"')
actual=$(git rev-parse "HEAD:vendor/${postgres}")
if [ "${expected}" != "${actual}" ]; then
echo >&2 "Expected ${postgres} rev to be at '${expected}', but it is at '${actual}'"
FAILED=true
fi
done
if [ "${FAILED}" = "true" ]; then
echo >&2 "Please update vendors/revisions.json if these changes are intentional"
exit 1
fi
- name: Set pg 14 revision for caching - name: Set pg 14 revision for caching
id: pg_v14_rev id: pg_v14_rev
run: echo pg_rev=$(git rev-parse HEAD:vendor/postgres-v14) >> $GITHUB_OUTPUT run: echo pg_rev=$(git rev-parse HEAD:vendor/postgres-v14) >> $GITHUB_OUTPUT
@@ -291,7 +264,7 @@ jobs:
export REMOTE_STORAGE_S3_BUCKET=neon-github-public-dev export REMOTE_STORAGE_S3_BUCKET=neon-github-public-dev
export REMOTE_STORAGE_S3_REGION=eu-central-1 export REMOTE_STORAGE_S3_REGION=eu-central-1
# Avoid `$CARGO_FEATURES` since there's no `testing` feature in the e2e tests now # Avoid `$CARGO_FEATURES` since there's no `testing` feature in the e2e tests now
${cov_prefix} cargo test $CARGO_FLAGS --package remote_storage --test test_real_s3 ${cov_prefix} cargo test $CARGO_FLAGS --package remote_storage --test pagination_tests -- s3_pagination_should_work --exact
- name: Install rust binaries - name: Install rust binaries
run: | run: |
@@ -396,11 +369,13 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
pytest_split_group: [ 1, 2, 3, 4 ]
build_type: [ release ] build_type: [ release ]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
submodules: true
fetch-depth: 1
- name: Pytest benchmarks - name: Pytest benchmarks
uses: ./.github/actions/run-python-test-set uses: ./.github/actions/run-python-test-set
@@ -409,11 +384,9 @@ jobs:
test_selection: performance test_selection: performance
run_in_parallel: false run_in_parallel: false
save_perf_report: ${{ github.ref_name == 'main' }} save_perf_report: ${{ github.ref_name == 'main' }}
extra_params: --splits ${{ strategy.job-total }} --group ${{ matrix.pytest_split_group }}
env: env:
VIP_VAP_ACCESS_TOKEN: "${{ secrets.VIP_VAP_ACCESS_TOKEN }}" VIP_VAP_ACCESS_TOKEN: "${{ secrets.VIP_VAP_ACCESS_TOKEN }}"
PERF_TEST_RESULT_CONNSTR: "${{ secrets.PERF_TEST_RESULT_CONNSTR }}" PERF_TEST_RESULT_CONNSTR: "${{ secrets.PERF_TEST_RESULT_CONNSTR }}"
TEST_RESULT_CONNSTR: "${{ secrets.REGRESS_TEST_RESULT_CONNSTR }}"
# XXX: no coverage data handling here, since benchmarks are run on release builds, # XXX: no coverage data handling here, since benchmarks are run on release builds,
# while coverage is currently collected for the debug ones # while coverage is currently collected for the debug ones
@@ -641,7 +614,7 @@ jobs:
/kaniko/executor --reproducible --snapshot-mode=redo --skip-unused-stages --cache=true /kaniko/executor --reproducible --snapshot-mode=redo --skip-unused-stages --cache=true
--cache-repo 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cache --cache-repo 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cache
--context . --context .
--build-arg GIT_VERSION=${{ github.event.pull_request.head.sha || github.sha }} --build-arg GIT_VERSION=${{ github.sha }}
--build-arg REPOSITORY=369495373322.dkr.ecr.eu-central-1.amazonaws.com --build-arg REPOSITORY=369495373322.dkr.ecr.eu-central-1.amazonaws.com
--destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}} --destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}}
--destination neondatabase/neon:${{needs.tag.outputs.build-tag}} --destination neondatabase/neon:${{needs.tag.outputs.build-tag}}
@@ -650,6 +623,51 @@ jobs:
- name: Cleanup ECR folder - name: Cleanup ECR folder
run: rm -rf ~/.ecr run: rm -rf ~/.ecr
neon-image-depot:
# For testing this will run side-by-side for a few merges.
# This action is not really optimized yet, but gets the job done
runs-on: [ self-hosted, gen3, large ]
needs: [ tag ]
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/base:pinned
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: true
fetch-depth: 0
- name: Setup go
uses: actions/setup-go@v3
with:
go-version: '1.19'
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: Install Crane & ECR helper
run: go install github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login@69c85dc22db6511932bbf119e1a0cc5c90c69a7f # v0.6.0
- name: Configure ECR login
run: |
mkdir /github/home/.docker/
echo "{\"credsStore\":\"ecr-login\"}" > /github/home/.docker/config.json
- name: Build and push
uses: depot/build-push-action@v1
with:
# if no depot.json file is at the root of your repo, you must specify the project id
project: nrdv0s4kcs
push: true
tags: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:depot-${{needs.tag.outputs.build-tag}}
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 ]
needs: [ tag ] needs: [ tag ]
@@ -685,8 +703,7 @@ jobs:
/kaniko/executor --reproducible --snapshot-mode=redo --skip-unused-stages --cache=true /kaniko/executor --reproducible --snapshot-mode=redo --skip-unused-stages --cache=true
--cache-repo 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cache --cache-repo 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cache
--context . --context .
--build-arg GIT_VERSION=${{ github.event.pull_request.head.sha || github.sha }} --build-arg GIT_VERSION=${{ github.sha }}
--build-arg BUILD_TAG=${{needs.tag.outputs.build-tag}}
--build-arg REPOSITORY=369495373322.dkr.ecr.eu-central-1.amazonaws.com --build-arg REPOSITORY=369495373322.dkr.ecr.eu-central-1.amazonaws.com
--dockerfile Dockerfile.compute-tools --dockerfile Dockerfile.compute-tools
--destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} --destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}}
@@ -742,42 +759,12 @@ jobs:
/kaniko/executor --reproducible --snapshot-mode=redo --skip-unused-stages --cache=true /kaniko/executor --reproducible --snapshot-mode=redo --skip-unused-stages --cache=true
--cache-repo 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cache --cache-repo 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cache
--context . --context .
--build-arg GIT_VERSION=${{ github.event.pull_request.head.sha || github.sha }} --build-arg GIT_VERSION=${{ github.sha }}
--build-arg PG_VERSION=${{ matrix.version }} --build-arg PG_VERSION=${{ matrix.version }}
--build-arg BUILD_TAG=${{needs.tag.outputs.build-tag}}
--build-arg REPOSITORY=369495373322.dkr.ecr.eu-central-1.amazonaws.com --build-arg REPOSITORY=369495373322.dkr.ecr.eu-central-1.amazonaws.com
--dockerfile Dockerfile.compute-node --dockerfile Dockerfile.compute-node
--destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}} --destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
--destination neondatabase/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}} --destination neondatabase/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
--cleanup
# Due to a kaniko bug, we can't use cache for extensions image, thus it takes about the same amount of time as compute-node image to build (~10 min)
# During the transition period we need to have extensions in both places (in S3 and in compute-node image),
# so we won't build extension twice, but extract them from compute-node.
#
# For now we use extensions image only for new custom extensitons
- name: Kaniko build extensions only
run: |
# Kaniko is suposed to clean up after itself if --cleanup flag is set, but it doesn't.
# Despite some fixes were made in https://github.com/GoogleContainerTools/kaniko/pull/2504 (in kaniko v1.11.0),
# it still fails with error:
# error building image: could not save file: copying file: symlink postgres /kaniko/1/usr/local/pgsql/bin/postmaster: file exists
#
# Ref https://github.com/GoogleContainerTools/kaniko/issues/1406
find /kaniko -maxdepth 1 -mindepth 1 -type d -regex "/kaniko/[0-9]*" -exec rm -rv {} \;
/kaniko/executor --reproducible --snapshot-mode=redo --skip-unused-stages --cache=true \
--cache-repo 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cache \
--context . \
--build-arg GIT_VERSION=${{ github.event.pull_request.head.sha || github.sha }} \
--build-arg PG_VERSION=${{ matrix.version }} \
--build-arg BUILD_TAG=${{needs.tag.outputs.build-tag}} \
--build-arg REPOSITORY=369495373322.dkr.ecr.eu-central-1.amazonaws.com \
--dockerfile Dockerfile.compute-node \
--destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/extensions-${{ matrix.version }}:${{needs.tag.outputs.build-tag}} \
--destination neondatabase/extensions-${{ matrix.version }}:${{needs.tag.outputs.build-tag}} \
--cleanup \
--target postgres-extensions
# Cleanup script fails otherwise - rm: cannot remove '/nvme/actions-runner/_work/_temp/_github_home/.ecr': Permission denied # Cleanup script fails otherwise - rm: cannot remove '/nvme/actions-runner/_work/_temp/_github_home/.ecr': Permission denied
- name: Cleanup ECR folder - name: Cleanup ECR folder
@@ -794,7 +781,7 @@ jobs:
run: run:
shell: sh -eu {0} shell: sh -eu {0}
env: env:
VM_BUILDER_VERSION: v0.13.1 VM_BUILDER_VERSION: v0.8.0
steps: steps:
- name: Checkout - name: Checkout
@@ -896,10 +883,8 @@ jobs:
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} latest crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} latest
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:${{needs.tag.outputs.build-tag}} latest crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:${{needs.tag.outputs.build-tag}} latest
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} latest crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} latest
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/extensions-v14:${{needs.tag.outputs.build-tag}} latest
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:${{needs.tag.outputs.build-tag}} latest crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:${{needs.tag.outputs.build-tag}} latest
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} latest crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} latest
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/extensions-v15:${{needs.tag.outputs.build-tag}} latest
- name: Push images to production ECR - name: Push images to production ECR
if: | if: |
@@ -910,10 +895,8 @@ jobs:
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:latest crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:latest
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:latest crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:latest
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:latest crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:latest
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/extensions-v14:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/extensions-v14:latest
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:latest crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:latest
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:latest crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:latest
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/extensions-v15:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/extensions-v15:latest
- name: Configure Docker Hub login - name: Configure Docker Hub login
run: | run: |
@@ -935,65 +918,16 @@ jobs:
crane tag neondatabase/compute-tools:${{needs.tag.outputs.build-tag}} latest crane tag neondatabase/compute-tools:${{needs.tag.outputs.build-tag}} latest
crane tag neondatabase/compute-node-v14:${{needs.tag.outputs.build-tag}} latest crane tag neondatabase/compute-node-v14:${{needs.tag.outputs.build-tag}} latest
crane tag neondatabase/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} latest crane tag neondatabase/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} latest
crane tag neondatabase/extensions-v14:${{needs.tag.outputs.build-tag}} latest
crane tag neondatabase/compute-node-v15:${{needs.tag.outputs.build-tag}} latest crane tag neondatabase/compute-node-v15:${{needs.tag.outputs.build-tag}} latest
crane tag neondatabase/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} latest crane tag neondatabase/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} latest
crane tag neondatabase/extensions-v15:${{needs.tag.outputs.build-tag}} latest
- name: Cleanup ECR folder - name: Cleanup ECR folder
run: rm -rf ~/.ecr run: rm -rf ~/.ecr
upload-postgres-extensions-to-s3:
if: |
(github.ref_name == 'main' || github.ref_name == 'release') &&
github.event_name != 'workflow_dispatch'
runs-on: ${{ github.ref_name == 'release' && fromJSON('["self-hosted", "prod", "x64"]') || fromJSON('["self-hosted", "gen3", "small"]') }}
needs: [ tag, promote-images ]
strategy:
fail-fast: false
matrix:
version: [ v14, v15 ]
env:
EXTENSIONS_IMAGE: ${{ github.ref_name == 'release' && '093970136003' || '369495373322'}}.dkr.ecr.eu-central-1.amazonaws.com/extensions-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}
AWS_ACCESS_KEY_ID: ${{ github.ref_name == 'release' && secrets.AWS_ACCESS_KEY_PROD || secrets.AWS_ACCESS_KEY_DEV }}
AWS_SECRET_ACCESS_KEY: ${{ github.ref_name == 'release' && secrets.AWS_SECRET_KEY_PROD || secrets.AWS_SECRET_KEY_DEV }}
S3_BUCKETS: ${{ github.ref_name == 'release' && vars.S3_EXTENSIONS_BUCKETS_PROD || vars.S3_EXTENSIONS_BUCKETS_DEV }}
steps:
- name: Pull postgres-extensions image
run: |
docker pull ${EXTENSIONS_IMAGE}
- name: Create postgres-extensions container
id: create-container
run: |
EID=$(docker create ${EXTENSIONS_IMAGE} true)
echo "EID=${EID}" >> $GITHUB_OUTPUT
- name: Extract postgres-extensions from container
run: |
rm -rf ./extensions-to-upload # Just in case
mkdir -p extensions-to-upload
docker cp ${{ steps.create-container.outputs.EID }}:/extensions/ ./extensions-to-upload/
docker cp ${{ steps.create-container.outputs.EID }}:/ext_index.json ./extensions-to-upload/
- name: Upload postgres-extensions to S3
run: |
for BUCKET in $(echo ${S3_BUCKETS:-[]} | jq --raw-output '.[]'); do
aws s3 cp --recursive --only-show-errors ./extensions-to-upload s3://${BUCKET}/${{ needs.tag.outputs.build-tag }}/${{ matrix.version }}
done
- name: Cleanup
if: ${{ always() && steps.create-container.outputs.EID }}
run: |
docker rm ${{ steps.create-container.outputs.EID }} || true
deploy: deploy:
runs-on: [ self-hosted, gen3, small ] runs-on: [ self-hosted, gen3, small ]
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:latest container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:latest
needs: [ upload-postgres-extensions-to-s3, promote-images, tag, regress-tests ] needs: [ promote-images, tag, regress-tests ]
if: ( github.ref_name == 'main' || github.ref_name == 'release' ) && github.event_name != 'workflow_dispatch' if: ( github.ref_name == 'main' || github.ref_name == 'release' ) && github.event_name != 'workflow_dispatch'
steps: steps:
- name: Fix git ownership - name: Fix git ownership
@@ -1025,20 +959,6 @@ jobs:
exit 1 exit 1
fi fi
- name: Create git tag
if: github.ref_name == 'release'
uses: actions/github-script@v6
with:
# Retry script for 5XX server errors: https://github.com/actions/github-script#retries
retries: 5
script: |
github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: "refs/tags/${{ needs.tag.outputs.build-tag }}",
sha: context.sha,
})
promote-compatibility-data: promote-compatibility-data:
runs-on: [ self-hosted, gen3, small ] runs-on: [ self-hosted, gen3, small ]
container: container:

View File

@@ -3,8 +3,7 @@ name: Check neon with extra platform builds
on: on:
push: push:
branches: branches:
- main - main
- ci-run/pr-*
pull_request: pull_request:
defaults: defaults:

View File

@@ -3,7 +3,6 @@ name: Create Release Branch
on: on:
schedule: schedule:
- cron: '0 10 * * 2' - cron: '0 10 * * 2'
workflow_dispatch:
jobs: jobs:
create_release_branch: create_release_branch:

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
/pg_install /pg_install
/target /target
/alek_ext/target
/tmp_check /tmp_check
/tmp_check_cli /tmp_check_cli
__pycache__/ __pycache__/

348
Cargo.lock generated
View File

@@ -37,6 +37,10 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "alek_ext"
version = "0.1.0"
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@@ -158,19 +162,6 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "async-compression"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0122885821398cc923ece939e24d1056a2384ee719432397fa9db87230ff11"
dependencies = [
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "async-stream" name = "async-stream"
version = "0.3.5" version = "0.3.5"
@@ -213,6 +204,17 @@ dependencies = [
"critical-section", "critical-section",
] ]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi 0.1.19",
"libc",
"winapi",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@@ -606,7 +608,7 @@ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"libc", "libc",
"miniz_oxide 0.6.2", "miniz_oxide",
"object", "object",
"rustc-demangle", "rustc-demangle",
] ]
@@ -807,6 +809,18 @@ dependencies = [
"libloading", "libloading",
] ]
[[package]]
name = "clap"
version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
dependencies = [
"bitflags",
"clap_lex 0.2.4",
"indexmap",
"textwrap",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.3.0" version = "4.3.0"
@@ -827,7 +841,7 @@ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
"bitflags", "bitflags",
"clap_lex", "clap_lex 0.5.0",
"strsim", "strsim",
] ]
@@ -843,6 +857,15 @@ dependencies = [
"syn 2.0.16", "syn 2.0.16",
] ]
[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
]
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.5.0" version = "0.5.0"
@@ -895,11 +918,9 @@ name = "compute_tools"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-compression",
"chrono", "chrono",
"clap", "clap 4.3.0",
"compute_api", "compute_api",
"flate2",
"futures", "futures",
"hyper", "hyper",
"notify", "notify",
@@ -907,12 +928,14 @@ dependencies = [
"opentelemetry", "opentelemetry",
"postgres", "postgres",
"regex", "regex",
"remote_storage",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"tar", "tar",
"tokio", "tokio",
"tokio-postgres", "tokio-postgres",
"toml_edit",
"tracing", "tracing",
"tracing-opentelemetry", "tracing-opentelemetry",
"tracing-subscriber", "tracing-subscriber",
@@ -960,7 +983,7 @@ name = "control_plane"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap 4.3.0",
"comfy-table", "comfy-table",
"compute_api", "compute_api",
"git-version", "git-version",
@@ -1030,19 +1053,19 @@ dependencies = [
[[package]] [[package]]
name = "criterion" name = "criterion"
version = "0.5.1" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb"
dependencies = [ dependencies = [
"anes", "anes",
"atty",
"cast", "cast",
"ciborium", "ciborium",
"clap", "clap 3.2.25",
"criterion-plot", "criterion-plot",
"is-terminal",
"itertools", "itertools",
"lazy_static",
"num-traits", "num-traits",
"once_cell",
"oorandom", "oorandom",
"plotters", "plotters",
"rayon", "rayon",
@@ -1123,7 +1146,7 @@ dependencies = [
"crossterm_winapi", "crossterm_winapi",
"libc", "libc",
"mio", "mio",
"parking_lot 0.12.1", "parking_lot",
"signal-hook", "signal-hook",
"signal-hook-mio", "signal-hook-mio",
"winapi", "winapi",
@@ -1193,7 +1216,7 @@ dependencies = [
"hashbrown 0.12.3", "hashbrown 0.12.3",
"lock_api", "lock_api",
"once_cell", "once_cell",
"parking_lot_core 0.9.7", "parking_lot_core",
] ]
[[package]] [[package]]
@@ -1382,16 +1405,6 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flate2"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
dependencies = [
"crc32fast",
"miniz_oxide 0.7.1",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@@ -1669,6 +1682,15 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.2.6" version = "0.2.6"
@@ -1923,9 +1945,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
] ]
[[package]] [[package]]
@@ -2176,15 +2195,6 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "miniz_oxide"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
"adler",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.6" version = "0.8.6"
@@ -2263,6 +2273,16 @@ dependencies = [
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]] [[package]]
name = "num-bigint" name = "num-bigint"
version = "0.4.3" version = "0.4.3"
@@ -2335,9 +2355,9 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.55" version = "0.10.52"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if", "cfg-if",
@@ -2367,9 +2387,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.90" version = "0.9.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@@ -2379,9 +2399,9 @@ dependencies = [
[[package]] [[package]]
name = "opentelemetry" name = "opentelemetry"
version = "0.19.0" version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4b8347cc26099d3aeee044065ecc3ae11469796b4d65d065a23a584ed92a6f" checksum = "69d6c3d7288a106c0a363e4b0e8d308058d56902adefb16f4936f417ffef086e"
dependencies = [ dependencies = [
"opentelemetry_api", "opentelemetry_api",
"opentelemetry_sdk", "opentelemetry_sdk",
@@ -2389,9 +2409,9 @@ dependencies = [
[[package]] [[package]]
name = "opentelemetry-http" name = "opentelemetry-http"
version = "0.8.0" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a819b71d6530c4297b49b3cae2939ab3a8cc1b9f382826a1bc29dd0ca3864906" checksum = "1edc79add46364183ece1a4542592ca593e6421c60807232f5b8f7a31703825d"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
@@ -2402,9 +2422,9 @@ dependencies = [
[[package]] [[package]]
name = "opentelemetry-otlp" name = "opentelemetry-otlp"
version = "0.12.0" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8af72d59a4484654ea8eb183fea5ae4eb6a41d7ac3e3bae5f4d2a282a3a7d3ca" checksum = "d1c928609d087790fc936a1067bdc310ae702bdf3b090c3f281b713622c8bbde"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"futures", "futures",
@@ -2420,47 +2440,48 @@ dependencies = [
[[package]] [[package]]
name = "opentelemetry-proto" name = "opentelemetry-proto"
version = "0.2.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "045f8eea8c0fa19f7d48e7bc3128a39c2e5c533d5c61298c548dfefc1064474c" checksum = "d61a2f56df5574508dd86aaca016c917489e589ece4141df1b5e349af8d66c28"
dependencies = [ dependencies = [
"futures", "futures",
"futures-util", "futures-util",
"opentelemetry", "opentelemetry",
"prost", "prost",
"tonic 0.8.3", "tonic 0.8.3",
"tonic-build 0.8.4",
] ]
[[package]] [[package]]
name = "opentelemetry-semantic-conventions" name = "opentelemetry-semantic-conventions"
version = "0.11.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24e33428e6bf08c6f7fcea4ddb8e358fab0fe48ab877a87c70c6ebe20f673ce5" checksum = "9b02e0230abb0ab6636d18e2ba8fa02903ea63772281340ccac18e0af3ec9eeb"
dependencies = [ dependencies = [
"opentelemetry", "opentelemetry",
] ]
[[package]] [[package]]
name = "opentelemetry_api" name = "opentelemetry_api"
version = "0.19.0" version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed41783a5bf567688eb38372f2b7a8530f5a607a4b49d38dd7573236c23ca7e2" checksum = "c24f96e21e7acc813c7a8394ee94978929db2bcc46cf6b5014fc612bf7760c22"
dependencies = [ dependencies = [
"fnv", "fnv",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"indexmap", "indexmap",
"js-sys",
"once_cell", "once_cell",
"pin-project-lite", "pin-project-lite",
"thiserror", "thiserror",
"urlencoding",
] ]
[[package]] [[package]]
name = "opentelemetry_sdk" name = "opentelemetry_sdk"
version = "0.19.0" version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b3a2a91fdbfdd4d212c0dcc2ab540de2c2bcbbd90be17de7a7daf8822d010c1" checksum = "1ca41c4933371b61c2a2f214bf16931499af4ec90543604ec828f7a625c09113"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"crossbeam-channel", "crossbeam-channel",
@@ -2489,24 +2510,35 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "os_str_bytes"
version = "6.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
[[package]] [[package]]
name = "outref" name = "outref"
version = "0.5.1" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "pagectl" name = "pagectl"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
"clap", "clap 4.3.0",
"git-version", "git-version",
"pageserver", "pageserver",
"postgres_ffi", "postgres_ffi",
"svg_fmt", "svg_fmt",
"tokio",
"utils", "utils",
"workspace_hack", "workspace_hack",
] ]
@@ -2516,13 +2548,12 @@ name = "pageserver"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-compression",
"async-stream", "async-stream",
"async-trait", "async-trait",
"byteorder", "byteorder",
"bytes", "bytes",
"chrono", "chrono",
"clap", "clap 4.3.0",
"close_fds", "close_fds",
"const_format", "const_format",
"consumption_metrics", "consumption_metrics",
@@ -2533,7 +2564,6 @@ dependencies = [
"enum-map", "enum-map",
"enumset", "enumset",
"fail", "fail",
"flate2",
"futures", "futures",
"git-version", "git-version",
"hex", "hex",
@@ -2545,7 +2575,6 @@ dependencies = [
"metrics", "metrics",
"nix", "nix",
"num-traits", "num-traits",
"num_cpus",
"once_cell", "once_cell",
"pageserver_api", "pageserver_api",
"pin-project-lite", "pin-project-lite",
@@ -2606,17 +2635,6 @@ dependencies = [
"workspace_hack", "workspace_hack",
] ]
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core 0.8.6",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@@ -2624,21 +2642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [ dependencies = [
"lock_api", "lock_api",
"parking_lot_core 0.9.7", "parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall 0.2.16",
"smallvec",
"winapi",
] ]
[[package]] [[package]]
@@ -2772,7 +2776,7 @@ dependencies = [
[[package]] [[package]]
name = "postgres" name = "postgres"
version = "0.19.4" version = "0.19.4"
source = "git+https://github.com/neondatabase/rust-postgres.git?rev=9011f7110db12b5e15afaf98f8ac834501d50ddc#9011f7110db12b5e15afaf98f8ac834501d50ddc" source = "git+https://github.com/neondatabase/rust-postgres.git?rev=2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9#2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9"
dependencies = [ dependencies = [
"bytes", "bytes",
"fallible-iterator", "fallible-iterator",
@@ -2785,7 +2789,7 @@ dependencies = [
[[package]] [[package]]
name = "postgres-native-tls" name = "postgres-native-tls"
version = "0.5.0" version = "0.5.0"
source = "git+https://github.com/neondatabase/rust-postgres.git?rev=9011f7110db12b5e15afaf98f8ac834501d50ddc#9011f7110db12b5e15afaf98f8ac834501d50ddc" source = "git+https://github.com/neondatabase/rust-postgres.git?rev=2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9#2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9"
dependencies = [ dependencies = [
"native-tls", "native-tls",
"tokio", "tokio",
@@ -2796,7 +2800,7 @@ dependencies = [
[[package]] [[package]]
name = "postgres-protocol" name = "postgres-protocol"
version = "0.6.4" version = "0.6.4"
source = "git+https://github.com/neondatabase/rust-postgres.git?rev=9011f7110db12b5e15afaf98f8ac834501d50ddc#9011f7110db12b5e15afaf98f8ac834501d50ddc" source = "git+https://github.com/neondatabase/rust-postgres.git?rev=2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9#2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9"
dependencies = [ dependencies = [
"base64 0.20.0", "base64 0.20.0",
"byteorder", "byteorder",
@@ -2814,7 +2818,7 @@ dependencies = [
[[package]] [[package]]
name = "postgres-types" name = "postgres-types"
version = "0.2.4" version = "0.2.4"
source = "git+https://github.com/neondatabase/rust-postgres.git?rev=9011f7110db12b5e15afaf98f8ac834501d50ddc#9011f7110db12b5e15afaf98f8ac834501d50ddc" source = "git+https://github.com/neondatabase/rust-postgres.git?rev=2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9#2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9"
dependencies = [ dependencies = [
"bytes", "bytes",
"fallible-iterator", "fallible-iterator",
@@ -2928,9 +2932,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.64" version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -2959,7 +2963,7 @@ dependencies = [
"lazy_static", "lazy_static",
"libc", "libc",
"memchr", "memchr",
"parking_lot 0.12.1", "parking_lot",
"procfs", "procfs",
"thiserror", "thiserror",
] ]
@@ -3024,13 +3028,13 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"atty",
"base64 0.13.1", "base64 0.13.1",
"bstr", "bstr",
"bytes", "bytes",
"chrono", "chrono",
"clap", "clap 4.3.0",
"consumption_metrics", "consumption_metrics",
"fallible-iterator",
"futures", "futures",
"git-version", "git-version",
"hashbrown 0.13.2", "hashbrown 0.13.2",
@@ -3047,10 +3051,9 @@ dependencies = [
"native-tls", "native-tls",
"once_cell", "once_cell",
"opentelemetry", "opentelemetry",
"parking_lot 0.12.1", "parking_lot",
"pin-project-lite", "pin-project-lite",
"postgres-native-tls", "postgres-native-tls",
"postgres-protocol",
"postgres_backend", "postgres_backend",
"pq_proto", "pq_proto",
"prometheus", "prometheus",
@@ -3059,7 +3062,6 @@ dependencies = [
"regex", "regex",
"reqwest", "reqwest",
"reqwest-middleware", "reqwest-middleware",
"reqwest-retry",
"reqwest-tracing", "reqwest-tracing",
"routerify", "routerify",
"rstest", "rstest",
@@ -3074,7 +3076,6 @@ dependencies = [
"thiserror", "thiserror",
"tls-listener", "tls-listener",
"tokio", "tokio",
"tokio-native-tls",
"tokio-postgres", "tokio-postgres",
"tokio-postgres-rustls", "tokio-postgres-rustls",
"tokio-rustls 0.23.4", "tokio-rustls 0.23.4",
@@ -3296,34 +3297,11 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "reqwest-retry"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d0fd6ef4c6d23790399fe15efc8d12cd9f3d4133958f9bd7801ee5cbaec6c4"
dependencies = [
"anyhow",
"async-trait",
"chrono",
"futures",
"getrandom",
"http",
"hyper",
"parking_lot 0.11.2",
"reqwest",
"reqwest-middleware",
"retry-policies",
"task-local-extensions",
"tokio",
"tracing",
"wasm-timer",
]
[[package]] [[package]]
name = "reqwest-tracing" name = "reqwest-tracing"
version = "0.4.5" version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b97ad83c2fc18113346b7158d79732242002427c30f620fa817c1f32901e0a8" checksum = "783e8130d2427ddd7897dd3f814d4a3aea31b05deb42a4fdf8c18258fe5aefd1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -3337,17 +3315,6 @@ dependencies = [
"tracing-opentelemetry", "tracing-opentelemetry",
] ]
[[package]]
name = "retry-policies"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e09bbcb5003282bcb688f0bae741b278e9c7e8f378f561522c9806c58e075d9b"
dependencies = [
"anyhow",
"chrono",
"rand",
]
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.16.20" version = "0.16.20"
@@ -3546,7 +3513,7 @@ dependencies = [
"byteorder", "byteorder",
"bytes", "bytes",
"chrono", "chrono",
"clap", "clap 4.3.0",
"const_format", "const_format",
"crc32c", "crc32c",
"fs2", "fs2",
@@ -3557,7 +3524,7 @@ dependencies = [
"hyper", "hyper",
"metrics", "metrics",
"once_cell", "once_cell",
"parking_lot 0.12.1", "parking_lot",
"postgres", "postgres",
"postgres-protocol", "postgres-protocol",
"postgres_backend", "postgres_backend",
@@ -3848,8 +3815,7 @@ dependencies = [
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.4" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/neondatabase/sharded-slab.git?rev=98d16753ab01c61f0a028de44167307a00efea00#98d16753ab01c61f0a028de44167307a00efea00"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
] ]
@@ -3977,7 +3943,7 @@ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
"bytes", "bytes",
"clap", "clap 4.3.0",
"const_format", "const_format",
"futures", "futures",
"futures-core", "futures-core",
@@ -3987,12 +3953,12 @@ dependencies = [
"hyper", "hyper",
"metrics", "metrics",
"once_cell", "once_cell",
"parking_lot 0.12.1", "parking_lot",
"prost", "prost",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tonic 0.9.2", "tonic 0.9.2",
"tonic-build", "tonic-build 0.9.2",
"tracing", "tracing",
"utils", "utils",
"workspace_hack", "workspace_hack",
@@ -4093,7 +4059,7 @@ checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6"
dependencies = [ dependencies = [
"filetime", "filetime",
"libc", "libc",
"xattr 0.2.3", "xattr",
] ]
[[package]] [[package]]
@@ -4158,6 +4124,12 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "textwrap"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.40" version = "1.0.40"
@@ -4306,7 +4278,7 @@ dependencies = [
[[package]] [[package]]
name = "tokio-postgres" name = "tokio-postgres"
version = "0.7.7" version = "0.7.7"
source = "git+https://github.com/neondatabase/rust-postgres.git?rev=9011f7110db12b5e15afaf98f8ac834501d50ddc#9011f7110db12b5e15afaf98f8ac834501d50ddc" source = "git+https://github.com/neondatabase/rust-postgres.git?rev=2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9#2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"byteorder", "byteorder",
@@ -4315,7 +4287,7 @@ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"log", "log",
"parking_lot 0.12.1", "parking_lot",
"percent-encoding", "percent-encoding",
"phf", "phf",
"pin-project-lite", "pin-project-lite",
@@ -4374,17 +4346,16 @@ dependencies = [
[[package]] [[package]]
name = "tokio-tar" name = "tokio-tar"
version = "0.3.1" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/neondatabase/tokio-tar.git?rev=404df61437de0feef49ba2ccdbdd94eb8ad6e142#404df61437de0feef49ba2ccdbdd94eb8ad6e142"
checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75"
dependencies = [ dependencies = [
"filetime", "filetime",
"futures-core", "futures-core",
"libc", "libc",
"redox_syscall 0.3.5", "redox_syscall 0.2.16",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"xattr 1.0.0", "xattr",
] ]
[[package]] [[package]]
@@ -4511,6 +4482,19 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "tonic-build"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf5e9b9c0f7e0a7c027dcfaba7b2c60816c7049171f679d99ee2ff65d0de8c4"
dependencies = [
"prettyplease 0.1.25",
"proc-macro2",
"prost-build",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "tonic-build" name = "tonic-build"
version = "0.9.2" version = "0.9.2"
@@ -4561,7 +4545,7 @@ name = "trace"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap 4.3.0",
"pageserver_api", "pageserver_api",
"utils", "utils",
"workspace_hack", "workspace_hack",
@@ -4634,9 +4618,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-opentelemetry" name = "tracing-opentelemetry"
version = "0.19.0" version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00a39dcf9bfc1742fa4d6215253b33a6e474be78275884c216fc2a06267b3600" checksum = "21ebb87a95ea13271332df069020513ab70bdb5637ca42d6e492dc3bbbad48de"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"opentelemetry", "opentelemetry",
@@ -4663,6 +4647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
dependencies = [ dependencies = [
"matchers", "matchers",
"nu-ansi-term",
"once_cell", "once_cell",
"regex", "regex",
"serde", "serde",
@@ -4831,11 +4816,11 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"atty",
"bincode", "bincode",
"byteorder", "byteorder",
"bytes", "bytes",
"chrono", "chrono",
"const_format",
"criterion", "criterion",
"futures", "futures",
"heapless", "heapless",
@@ -4861,7 +4846,6 @@ dependencies = [
"tempfile", "tempfile",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-stream",
"tracing", "tracing",
"tracing-error", "tracing-error",
"tracing-subscriber", "tracing-subscriber",
@@ -4909,7 +4893,7 @@ name = "wal_craft"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap 4.3.0",
"env_logger", "env_logger",
"log", "log",
"once_cell", "once_cell",
@@ -5013,21 +4997,6 @@ version = "0.2.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93"
[[package]]
name = "wasm-timer"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f"
dependencies = [
"futures",
"js-sys",
"parking_lot 0.11.2",
"pin-utils",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.63" version = "0.3.63"
@@ -5289,7 +5258,7 @@ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
"chrono", "chrono",
"clap", "clap 4.3.0",
"clap_builder", "clap_builder",
"crossbeam-utils", "crossbeam-utils",
"either", "either",
@@ -5359,15 +5328,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "xattr"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea263437ca03c1522846a4ddafbca2542d0ad5ed9b784909d4b27b76f62bc34a"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "xmlparser" name = "xmlparser"
version = "0.13.5" version = "0.13.5"

View File

@@ -1,5 +1,6 @@
[workspace] [workspace]
members = [ members = [
"alek_ext",
"compute_tools", "compute_tools",
"control_plane", "control_plane",
"pageserver", "pageserver",
@@ -32,10 +33,9 @@ license = "Apache-2.0"
## All dependency versions, used in the project ## All dependency versions, used in the project
[workspace.dependencies] [workspace.dependencies]
anyhow = { version = "1.0", features = ["backtrace"] } anyhow = { version = "1.0", features = ["backtrace"] }
async-compression = { version = "0.4.0", features = ["tokio", "gzip"] }
flate2 = "1.0.26"
async-stream = "0.3" async-stream = "0.3"
async-trait = "0.1" async-trait = "0.1"
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.27" aws-sdk-s3 = "0.27"
aws-smithy-http = "0.55" aws-smithy-http = "0.55"
@@ -84,9 +84,9 @@ notify = "5.0.0"
num_cpus = "1.15" num_cpus = "1.15"
num-traits = "0.2.15" num-traits = "0.2.15"
once_cell = "1.13" once_cell = "1.13"
opentelemetry = "0.19.0" opentelemetry = "0.18.0"
opentelemetry-otlp = { version = "0.12.0", default_features=false, features = ["http-proto", "trace", "http", "reqwest-client"] } opentelemetry-otlp = { version = "0.11.0", default_features=false, features = ["http-proto", "trace", "http", "reqwest-client"] }
opentelemetry-semantic-conventions = "0.11.0" opentelemetry-semantic-conventions = "0.10.0"
parking_lot = "0.12" parking_lot = "0.12"
pin-project-lite = "0.2" pin-project-lite = "0.2"
prometheus = {version = "0.13", default_features=false, features = ["process"]} # removes protobuf dependency prometheus = {version = "0.13", default_features=false, features = ["process"]} # removes protobuf dependency
@@ -94,9 +94,8 @@ prost = "0.11"
rand = "0.8" rand = "0.8"
regex = "1.4" regex = "1.4"
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] } reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
reqwest-tracing = { version = "0.4.0", features = ["opentelemetry_0_19"] } reqwest-tracing = { version = "0.4.0", features = ["opentelemetry_0_18"] }
reqwest-middleware = "0.2.0" reqwest-middleware = "0.2.0"
reqwest-retry = "0.2.2"
routerify = "3" routerify = "3"
rpds = "0.13" rpds = "0.13"
rustls = "0.20" rustls = "0.20"
@@ -123,15 +122,14 @@ tokio-io-timeout = "1.2.0"
tokio-postgres-rustls = "0.9.0" tokio-postgres-rustls = "0.9.0"
tokio-rustls = "0.23" tokio-rustls = "0.23"
tokio-stream = "0.1" tokio-stream = "0.1"
tokio-tar = "0.3"
tokio-util = { version = "0.7", features = ["io"] } tokio-util = { version = "0.7", features = ["io"] }
toml = "0.7" toml = "0.7"
toml_edit = "0.19" toml_edit = "0.19"
tonic = {version = "0.9", features = ["tls", "tls-roots"]} tonic = {version = "0.9", features = ["tls", "tls-roots"]}
tracing = "0.1" tracing = "0.1"
tracing-error = "0.2.0" tracing-error = "0.2.0"
tracing-opentelemetry = "0.19.0" tracing-opentelemetry = "0.18.0"
tracing-subscriber = { version = "0.3", default_features = false, features = ["smallvec", "fmt", "tracing-log", "std", "env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
url = "2.2" url = "2.2"
uuid = { version = "1.2", features = ["v4", "serde"] } uuid = { version = "1.2", features = ["v4", "serde"] }
walkdir = "2.3.2" walkdir = "2.3.2"
@@ -143,11 +141,12 @@ env_logger = "0.10"
log = "0.4" log = "0.4"
## Libraries from neondatabase/ git forks, ideally with changes to be upstreamed ## Libraries from neondatabase/ git forks, ideally with changes to be upstreamed
postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="9011f7110db12b5e15afaf98f8ac834501d50ddc" } postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9" }
postgres-native-tls = { git = "https://github.com/neondatabase/rust-postgres.git", rev="9011f7110db12b5e15afaf98f8ac834501d50ddc" } postgres-native-tls = { git = "https://github.com/neondatabase/rust-postgres.git", rev="2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9" }
postgres-protocol = { git = "https://github.com/neondatabase/rust-postgres.git", rev="9011f7110db12b5e15afaf98f8ac834501d50ddc" } postgres-protocol = { git = "https://github.com/neondatabase/rust-postgres.git", rev="2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9" }
postgres-types = { git = "https://github.com/neondatabase/rust-postgres.git", rev="9011f7110db12b5e15afaf98f8ac834501d50ddc" } postgres-types = { git = "https://github.com/neondatabase/rust-postgres.git", rev="2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9" }
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="9011f7110db12b5e15afaf98f8ac834501d50ddc" } tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9" }
tokio-tar = { git = "https://github.com/neondatabase/tokio-tar.git", rev="404df61437de0feef49ba2ccdbdd94eb8ad6e142" }
## Other git libraries ## Other git libraries
heapless = { default-features=false, features=[], git = "https://github.com/japaric/heapless.git", rev = "644653bf3b831c6bb4963be2de24804acf5e5001" } # upstream release pending heapless = { default-features=false, features=[], git = "https://github.com/japaric/heapless.git", rev = "644653bf3b831c6bb4963be2de24804acf5e5001" } # upstream release pending
@@ -172,7 +171,7 @@ utils = { version = "0.1", path = "./libs/utils/" }
workspace_hack = { version = "0.1", path = "./workspace_hack/" } workspace_hack = { version = "0.1", path = "./workspace_hack/" }
## Build dependencies ## Build dependencies
criterion = "0.5.1" criterion = "0.4"
rcgen = "0.10" rcgen = "0.10"
rstest = "0.17" rstest = "0.17"
tempfile = "3.4" tempfile = "3.4"
@@ -182,7 +181,12 @@ tonic-build = "0.9"
# This is only needed for proxy's tests. # This is only needed for proxy's tests.
# TODO: we should probably fork `tokio-postgres-rustls` instead. # TODO: we should probably fork `tokio-postgres-rustls` instead.
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="9011f7110db12b5e15afaf98f8ac834501d50ddc" } tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="2e9b5f1ddc481d1a98fa79f6b9378ac4f170b7c9" }
# Changes the MAX_THREADS limit from 4096 to 32768.
# This is a temporary workaround for using tracing from many threads in safekeepers code,
# until async safekeepers patch is merged to the main.
sharded-slab = { git = "https://github.com/neondatabase/sharded-slab.git", rev="98d16753ab01c61f0a028de44167307a00efea00" }
################# Binary contents sections ################# Binary contents sections

View File

@@ -2,7 +2,6 @@ ARG PG_VERSION
ARG REPOSITORY=neondatabase ARG REPOSITORY=neondatabase
ARG IMAGE=rust ARG IMAGE=rust
ARG TAG=pinned ARG TAG=pinned
ARG BUILD_TAG
######################################################################################### #########################################################################################
# #
@@ -13,7 +12,7 @@ FROM debian:bullseye-slim AS build-deps
RUN apt update && \ RUN apt update && \
apt install -y git autoconf automake libtool build-essential bison flex libreadline-dev \ apt install -y git autoconf automake libtool build-essential bison flex libreadline-dev \
zlib1g-dev libxml2-dev libcurl4-openssl-dev libossp-uuid-dev wget pkg-config libssl-dev \ zlib1g-dev libxml2-dev libcurl4-openssl-dev libossp-uuid-dev wget pkg-config libssl-dev \
libicu-dev libxslt1-dev liblz4-dev libzstd-dev zstd libicu-dev libxslt1-dev liblz4-dev libzstd-dev
######################################################################################### #########################################################################################
# #
@@ -68,7 +67,7 @@ RUN apt update && \
RUN wget https://gitlab.com/Oslandia/SFCGAL/-/archive/v1.3.10/SFCGAL-v1.3.10.tar.gz -O SFCGAL.tar.gz && \ RUN wget https://gitlab.com/Oslandia/SFCGAL/-/archive/v1.3.10/SFCGAL-v1.3.10.tar.gz -O SFCGAL.tar.gz && \
echo "4e39b3b2adada6254a7bdba6d297bb28e1a9835a9f879b74f37e2dab70203232 SFCGAL.tar.gz" | sha256sum --check && \ echo "4e39b3b2adada6254a7bdba6d297bb28e1a9835a9f879b74f37e2dab70203232 SFCGAL.tar.gz" | sha256sum --check && \
mkdir sfcgal-src && cd sfcgal-src && tar xvzf ../SFCGAL.tar.gz --strip-components=1 -C . && \ mkdir sfcgal-src && cd sfcgal-src && tar xvzf ../SFCGAL.tar.gz --strip-components=1 -C . && \
cmake -DCMAKE_BUILD_TYPE=Release . && make -j $(getconf _NPROCESSORS_ONLN) && \ cmake . && make -j $(getconf _NPROCESSORS_ONLN) && \
DESTDIR=/sfcgal make install -j $(getconf _NPROCESSORS_ONLN) && \ DESTDIR=/sfcgal make install -j $(getconf _NPROCESSORS_ONLN) && \
make clean && cp -R /sfcgal/* / make clean && cp -R /sfcgal/* /
@@ -77,7 +76,6 @@ ENV PATH "/usr/local/pgsql/bin:$PATH"
RUN wget https://download.osgeo.org/postgis/source/postgis-3.3.2.tar.gz -O postgis.tar.gz && \ RUN wget https://download.osgeo.org/postgis/source/postgis-3.3.2.tar.gz -O postgis.tar.gz && \
echo "9a2a219da005a1730a39d1959a1c7cec619b1efb009b65be80ffc25bad299068 postgis.tar.gz" | sha256sum --check && \ echo "9a2a219da005a1730a39d1959a1c7cec619b1efb009b65be80ffc25bad299068 postgis.tar.gz" | sha256sum --check && \
mkdir postgis-src && cd postgis-src && tar xvzf ../postgis.tar.gz --strip-components=1 -C . && \ mkdir postgis-src && cd postgis-src && tar xvzf ../postgis.tar.gz --strip-components=1 -C . && \
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /before.txt &&\
./autogen.sh && \ ./autogen.sh && \
./configure --with-sfcgal=/usr/local/bin/sfcgal-config && \ ./configure --with-sfcgal=/usr/local/bin/sfcgal-config && \
make -j $(getconf _NPROCESSORS_ONLN) install && \ make -j $(getconf _NPROCESSORS_ONLN) install && \
@@ -90,28 +88,17 @@ RUN wget https://download.osgeo.org/postgis/source/postgis-3.3.2.tar.gz -O postg
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis_tiger_geocoder.control && \ echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis_tiger_geocoder.control && \
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis_topology.control && \ echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis_topology.control && \
echo 'trusted = true' >> /usr/local/pgsql/share/extension/address_standardizer.control && \ echo 'trusted = true' >> /usr/local/pgsql/share/extension/address_standardizer.control && \
echo 'trusted = true' >> /usr/local/pgsql/share/extension/address_standardizer_data_us.control && \ echo 'trusted = true' >> /usr/local/pgsql/share/extension/address_standardizer_data_us.control
mkdir -p /extensions/postgis && \
cp /usr/local/pgsql/share/extension/postgis.control /extensions/postgis && \
cp /usr/local/pgsql/share/extension/postgis_raster.control /extensions/postgis && \
cp /usr/local/pgsql/share/extension/postgis_sfcgal.control /extensions/postgis && \
cp /usr/local/pgsql/share/extension/postgis_tiger_geocoder.control /extensions/postgis && \
cp /usr/local/pgsql/share/extension/postgis_topology.control /extensions/postgis && \
cp /usr/local/pgsql/share/extension/address_standardizer.control /extensions/postgis && \
cp /usr/local/pgsql/share/extension/address_standardizer_data_us.control /extensions/postgis
RUN wget https://github.com/pgRouting/pgrouting/archive/v3.4.2.tar.gz -O pgrouting.tar.gz && \ RUN wget https://github.com/pgRouting/pgrouting/archive/v3.4.2.tar.gz -O pgrouting.tar.gz && \
echo "cac297c07d34460887c4f3b522b35c470138760fe358e351ad1db4edb6ee306e pgrouting.tar.gz" | sha256sum --check && \ echo "cac297c07d34460887c4f3b522b35c470138760fe358e351ad1db4edb6ee306e pgrouting.tar.gz" | sha256sum --check && \
mkdir pgrouting-src && cd pgrouting-src && tar xvzf ../pgrouting.tar.gz --strip-components=1 -C . && \ mkdir pgrouting-src && cd pgrouting-src && tar xvzf ../pgrouting.tar.gz --strip-components=1 -C . && \
mkdir build && cd build && \ mkdir build && \
cmake -DCMAKE_BUILD_TYPE=Release .. && \ cd build && \
cmake .. && \
make -j $(getconf _NPROCESSORS_ONLN) && \ make -j $(getconf _NPROCESSORS_ONLN) && \
make -j $(getconf _NPROCESSORS_ONLN) install && \ make -j $(getconf _NPROCESSORS_ONLN) install && \
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgrouting.control && \ echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgrouting.control
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /after.txt &&\
cp /usr/local/pgsql/share/extension/pgrouting.control /extensions/postgis && \
sort -o /before.txt /before.txt && sort -o /after.txt /after.txt && \
comm -13 /before.txt /after.txt | tar --directory=/usr/local/pgsql --zstd -cf /extensions/postgis.tar.zst -T -
######################################################################################### #########################################################################################
# #
@@ -144,20 +131,10 @@ RUN wget https://github.com/plv8/plv8/archive/refs/tags/v3.1.5.tar.gz -O plv8.ta
FROM build-deps AS h3-pg-build FROM build-deps AS h3-pg-build
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
RUN case "$(uname -m)" in \ # packaged cmake is too old
"x86_64") \ RUN wget https://github.com/Kitware/CMake/releases/download/v3.24.2/cmake-3.24.2-linux-x86_64.sh \
export CMAKE_CHECKSUM=739d372726cb23129d57a539ce1432453448816e345e1545f6127296926b6754 \
;; \
"aarch64") \
export CMAKE_CHECKSUM=281b42627c9a1beed03e29706574d04c6c53fae4994472e90985ef018dd29c02 \
;; \
*) \
echo "Unsupported architecture '$(uname -m)'. Supported are x86_64 and aarch64" && exit 1 \
;; \
esac && \
wget https://github.com/Kitware/CMake/releases/download/v3.24.2/cmake-3.24.2-linux-$(uname -m).sh \
-q -O /tmp/cmake-install.sh \ -q -O /tmp/cmake-install.sh \
&& echo "${CMAKE_CHECKSUM} /tmp/cmake-install.sh" | sha256sum --check \ && echo "739d372726cb23129d57a539ce1432453448816e345e1545f6127296926b6754 /tmp/cmake-install.sh" | sha256sum --check \
&& chmod u+x /tmp/cmake-install.sh \ && chmod u+x /tmp/cmake-install.sh \
&& /tmp/cmake-install.sh --skip-license --prefix=/usr/local/ \ && /tmp/cmake-install.sh --skip-license --prefix=/usr/local/ \
&& rm /tmp/cmake-install.sh && rm /tmp/cmake-install.sh
@@ -211,8 +188,8 @@ RUN wget https://github.com/df7cb/postgresql-unit/archive/refs/tags/7.7.tar.gz -
FROM build-deps AS vector-pg-build FROM build-deps AS vector-pg-build
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
RUN wget https://github.com/pgvector/pgvector/archive/refs/tags/v0.4.4.tar.gz -O pgvector.tar.gz && \ RUN wget https://github.com/pgvector/pgvector/archive/refs/tags/v0.4.0.tar.gz -O pgvector.tar.gz && \
echo "1cb70a63f8928e396474796c22a20be9f7285a8a013009deb8152445b61b72e6 pgvector.tar.gz" | sha256sum --check && \ echo "b76cf84ddad452cc880a6c8c661d137ddd8679c000a16332f4f03ecf6e10bcc8 pgvector.tar.gz" | sha256sum --check && \
mkdir pgvector-src && cd pgvector-src && tar xvzf ../pgvector.tar.gz --strip-components=1 -C . && \ mkdir pgvector-src && cd pgvector-src && tar xvzf ../pgvector.tar.gz --strip-components=1 -C . && \
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \ make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \ make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
@@ -378,7 +355,7 @@ RUN apt-get update && \
wget https://github.com/timescale/timescaledb/archive/refs/tags/2.10.1.tar.gz -O timescaledb.tar.gz && \ wget https://github.com/timescale/timescaledb/archive/refs/tags/2.10.1.tar.gz -O timescaledb.tar.gz && \
echo "6fca72a6ed0f6d32d2b3523951ede73dc5f9b0077b38450a029a5f411fdb8c73 timescaledb.tar.gz" | sha256sum --check && \ echo "6fca72a6ed0f6d32d2b3523951ede73dc5f9b0077b38450a029a5f411fdb8c73 timescaledb.tar.gz" | sha256sum --check && \
mkdir timescaledb-src && cd timescaledb-src && tar xvzf ../timescaledb.tar.gz --strip-components=1 -C . && \ mkdir timescaledb-src && cd timescaledb-src && tar xvzf ../timescaledb.tar.gz --strip-components=1 -C . && \
./bootstrap -DSEND_TELEMETRY_DEFAULT:BOOL=OFF -DUSE_TELEMETRY:BOOL=OFF -DAPACHE_ONLY:BOOL=ON -DCMAKE_BUILD_TYPE=Release && \ ./bootstrap -DSEND_TELEMETRY_DEFAULT:BOOL=OFF -DUSE_TELEMETRY:BOOL=OFF -DAPACHE_ONLY:BOOL=ON && \
cd build && \ cd build && \
make -j $(getconf _NPROCESSORS_ONLN) && \ make -j $(getconf _NPROCESSORS_ONLN) && \
make install -j $(getconf _NPROCESSORS_ONLN) && \ make install -j $(getconf _NPROCESSORS_ONLN) && \
@@ -431,16 +408,12 @@ RUN apt-get update && \
wget https://github.com/ketteq-neon/postgres-exts/archive/e0bd1a9d9313d7120c1b9c7bb15c48c0dede4c4e.tar.gz -O kq_imcx.tar.gz && \ wget https://github.com/ketteq-neon/postgres-exts/archive/e0bd1a9d9313d7120c1b9c7bb15c48c0dede4c4e.tar.gz -O kq_imcx.tar.gz && \
echo "dc93a97ff32d152d32737ba7e196d9687041cda15e58ab31344c2f2de8855336 kq_imcx.tar.gz" | sha256sum --check && \ echo "dc93a97ff32d152d32737ba7e196d9687041cda15e58ab31344c2f2de8855336 kq_imcx.tar.gz" | sha256sum --check && \
mkdir kq_imcx-src && cd kq_imcx-src && tar xvzf ../kq_imcx.tar.gz --strip-components=1 -C . && \ mkdir kq_imcx-src && cd kq_imcx-src && tar xvzf ../kq_imcx.tar.gz --strip-components=1 -C . && \
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /before.txt &&\ mkdir build && \
mkdir build && cd build && \ cd build && \
cmake -DCMAKE_BUILD_TYPE=Release .. && \ cmake .. && \
make -j $(getconf _NPROCESSORS_ONLN) && \ make -j $(getconf _NPROCESSORS_ONLN) && \
make -j $(getconf _NPROCESSORS_ONLN) install && \ make -j $(getconf _NPROCESSORS_ONLN) install && \
echo 'trusted = true' >> /usr/local/pgsql/share/extension/kq_imcx.control && \ echo 'trusted = true' >> /usr/local/pgsql/share/extension/kq_imcx.control
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /after.txt &&\
mkdir -p /extensions/kq_imcx && cp /usr/local/pgsql/share/extension/kq_imcx.control /extensions/kq_imcx && \
sort -o /before.txt /before.txt && sort -o /after.txt /after.txt && \
comm -13 /before.txt /after.txt | tar --directory=/usr/local/pgsql --zstd -cf /extensions/kq_imcx.tar.zst -T -
######################################################################################### #########################################################################################
# #
@@ -459,126 +432,6 @@ RUN wget https://github.com/citusdata/pg_cron/archive/refs/tags/v1.5.2.tar.gz -O
make -j $(getconf _NPROCESSORS_ONLN) install && \ make -j $(getconf _NPROCESSORS_ONLN) install && \
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_cron.control echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_cron.control
#########################################################################################
#
# Layer "rdkit-pg-build"
# compile rdkit extension
#
#########################################################################################
FROM build-deps AS rdkit-pg-build
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
RUN apt-get update && \
apt-get install -y \
cmake \
libboost-iostreams1.74-dev \
libboost-regex1.74-dev \
libboost-serialization1.74-dev \
libboost-system1.74-dev \
libeigen3-dev \
libfreetype6-dev
ENV PATH "/usr/local/pgsql/bin/:/usr/local/pgsql/:$PATH"
RUN wget https://github.com/rdkit/rdkit/archive/refs/tags/Release_2023_03_1.tar.gz -O rdkit.tar.gz && \
echo "db346afbd0ba52c843926a2a62f8a38c7b774ffab37eaf382d789a824f21996c rdkit.tar.gz" | sha256sum --check && \
mkdir rdkit-src && cd rdkit-src && tar xvzf ../rdkit.tar.gz --strip-components=1 -C . && \
cmake \
-D RDK_BUILD_CAIRO_SUPPORT=OFF \
-D RDK_BUILD_INCHI_SUPPORT=ON \
-D RDK_BUILD_AVALON_SUPPORT=ON \
-D RDK_BUILD_PYTHON_WRAPPERS=OFF \
-D RDK_BUILD_DESCRIPTORS3D=OFF \
-D RDK_BUILD_FREESASA_SUPPORT=OFF \
-D RDK_BUILD_COORDGEN_SUPPORT=ON \
-D RDK_BUILD_MOLINTERCHANGE_SUPPORT=OFF \
-D RDK_BUILD_YAEHMOP_SUPPORT=OFF \
-D RDK_BUILD_STRUCTCHECKER_SUPPORT=OFF \
-D RDK_USE_URF=OFF \
-D RDK_BUILD_PGSQL=ON \
-D RDK_PGSQL_STATIC=ON \
-D PostgreSQL_CONFIG=pg_config \
-D PostgreSQL_INCLUDE_DIR=`pg_config --includedir` \
-D PostgreSQL_TYPE_INCLUDE_DIR=`pg_config --includedir-server` \
-D PostgreSQL_LIBRARY_DIR=`pg_config --libdir` \
-D RDK_INSTALL_INTREE=OFF \
-D CMAKE_BUILD_TYPE=Release \
. && \
make -j $(getconf _NPROCESSORS_ONLN) && \
make -j $(getconf _NPROCESSORS_ONLN) install && \
echo 'trusted = true' >> /usr/local/pgsql/share/extension/rdkit.control
#########################################################################################
#
# Layer "pg-uuidv7-pg-build"
# compile pg_uuidv7 extension
#
#########################################################################################
FROM build-deps AS pg-uuidv7-pg-build
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
ENV PATH "/usr/local/pgsql/bin/:$PATH"
RUN wget https://github.com/fboulnois/pg_uuidv7/archive/refs/tags/v1.0.1.tar.gz -O pg_uuidv7.tar.gz && \
echo "0d0759ab01b7fb23851ecffb0bce27822e1868a4a5819bfd276101c716637a7a pg_uuidv7.tar.gz" | sha256sum --check && \
mkdir pg_uuidv7-src && cd pg_uuidv7-src && tar xvzf ../pg_uuidv7.tar.gz --strip-components=1 -C . && \
make -j $(getconf _NPROCESSORS_ONLN) && \
make -j $(getconf _NPROCESSORS_ONLN) install && \
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_uuidv7.control
#########################################################################################
#
# Layer "pg-roaringbitmap-pg-build"
# compile pg_roaringbitmap extension
#
#########################################################################################
FROM build-deps AS pg-roaringbitmap-pg-build
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
ENV PATH "/usr/local/pgsql/bin/:$PATH"
RUN wget https://github.com/ChenHuajun/pg_roaringbitmap/archive/refs/tags/v0.5.4.tar.gz -O pg_roaringbitmap.tar.gz && \
echo "b75201efcb1c2d1b014ec4ae6a22769cc7a224e6e406a587f5784a37b6b5a2aa pg_roaringbitmap.tar.gz" | sha256sum --check && \
mkdir pg_roaringbitmap-src && cd pg_roaringbitmap-src && tar xvzf ../pg_roaringbitmap.tar.gz --strip-components=1 -C . && \
make -j $(getconf _NPROCESSORS_ONLN) && \
make -j $(getconf _NPROCESSORS_ONLN) install && \
echo 'trusted = true' >> /usr/local/pgsql/share/extension/roaringbitmap.control
#########################################################################################
#
# Layer "pg-embedding-pg-build"
# compile pg_embedding extension
#
#########################################################################################
FROM build-deps AS pg-embedding-pg-build
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
ENV PATH "/usr/local/pgsql/bin/:$PATH"
RUN wget https://github.com/neondatabase/pg_embedding/archive/refs/tags/0.3.1.tar.gz -O pg_embedding.tar.gz && \
echo "c4ae84eef36fa8ec5868f6e061f39812f19ee5ba3604d428d40935685c7be512 pg_embedding.tar.gz" | sha256sum --check && \
mkdir pg_embedding-src && cd pg_embedding-src && tar xvzf ../pg_embedding.tar.gz --strip-components=1 -C . && \
make -j $(getconf _NPROCESSORS_ONLN) && \
make -j $(getconf _NPROCESSORS_ONLN) install && \
echo 'trusted = true' >> /usr/local/pgsql/share/extension/embedding.control
#########################################################################################
#
# Layer "pg-anon-pg-build"
# compile anon extension
#
#########################################################################################
FROM build-deps AS pg-anon-pg-build
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
ENV PATH "/usr/local/pgsql/bin/:$PATH"
RUN wget https://gitlab.com/dalibo/postgresql_anonymizer/-/archive/1.1.0/postgresql_anonymizer-1.1.0.tar.gz -O pg_anon.tar.gz && \
echo "08b09d2ff9b962f96c60db7e6f8e79cf7253eb8772516998fc35ece08633d3ad pg_anon.tar.gz" | sha256sum --check && \
mkdir pg_anon-src && cd pg_anon-src && tar xvzf ../pg_anon.tar.gz --strip-components=1 -C . && \
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /before.txt &&\
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
echo 'trusted = true' >> /usr/local/pgsql/share/extension/anon.control && \
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /after.txt &&\
mkdir -p /extensions/anon && cp /usr/local/pgsql/share/extension/anon.control /extensions/anon && \
sort -o /before.txt /before.txt && sort -o /after.txt /after.txt && \
comm -13 /before.txt /after.txt | tar --directory=/usr/local/pgsql --zstd -cf /extensions/anon.tar.zst -T -
######################################################################################### #########################################################################################
# #
# Layer "rust extensions" # Layer "rust extensions"
@@ -687,7 +540,6 @@ RUN wget https://github.com/pksunkara/pgx_ulid/archive/refs/tags/v0.1.0.tar.gz -
# #
######################################################################################### #########################################################################################
FROM build-deps AS neon-pg-ext-build FROM build-deps AS neon-pg-ext-build
# Public extensions
COPY --from=postgis-build /usr/local/pgsql/ /usr/local/pgsql/ COPY --from=postgis-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=postgis-build /sfcgal/* / COPY --from=postgis-build /sfcgal/* /
COPY --from=plv8-build /usr/local/pgsql/ /usr/local/pgsql/ COPY --from=plv8-build /usr/local/pgsql/ /usr/local/pgsql/
@@ -712,10 +564,6 @@ COPY --from=pg-hint-plan-pg-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=kq-imcx-pg-build /usr/local/pgsql/ /usr/local/pgsql/ COPY --from=kq-imcx-pg-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=pg-cron-pg-build /usr/local/pgsql/ /usr/local/pgsql/ COPY --from=pg-cron-pg-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=pg-pgx-ulid-build /usr/local/pgsql/ /usr/local/pgsql/ COPY --from=pg-pgx-ulid-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=rdkit-pg-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=pg-uuidv7-pg-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=pg-roaringbitmap-pg-build /usr/local/pgsql/ /usr/local/pgsql/
COPY --from=pg-embedding-pg-build /usr/local/pgsql/ /usr/local/pgsql/
COPY pgxn/ pgxn/ COPY pgxn/ pgxn/
RUN make -j $(getconf _NPROCESSORS_ONLN) \ RUN make -j $(getconf _NPROCESSORS_ONLN) \
@@ -737,9 +585,6 @@ RUN make -j $(getconf _NPROCESSORS_ONLN) \
# #
######################################################################################### #########################################################################################
FROM $REPOSITORY/$IMAGE:$TAG AS compute-tools FROM $REPOSITORY/$IMAGE:$TAG AS compute-tools
ARG BUILD_TAG
ENV BUILD_TAG=$BUILD_TAG
USER nonroot USER nonroot
# Copy entire project to get Cargo.* files with proper dependencies for the whole project # Copy entire project to get Cargo.* files with proper dependencies for the whole project
COPY --chown=nonroot . . COPY --chown=nonroot . .
@@ -764,29 +609,6 @@ RUN rm -r /usr/local/pgsql/include
# if they were to be used by other libraries. # if they were to be used by other libraries.
RUN rm /usr/local/pgsql/lib/lib*.a RUN rm /usr/local/pgsql/lib/lib*.a
#########################################################################################
#
# Extenstion only
#
#########################################################################################
FROM python:3.9-slim-bullseye AS generate-ext-index
ARG PG_VERSION
ARG BUILD_TAG
RUN apt update && apt install -y zstd
# copy the control files here
COPY --from=kq-imcx-pg-build /extensions/ /extensions/
COPY --from=pg-anon-pg-build /extensions/ /extensions/
COPY --from=postgis-build /extensions/ /extensions/
COPY scripts/combine_control_files.py ./combine_control_files.py
RUN python3 ./combine_control_files.py ${PG_VERSION} ${BUILD_TAG} --public_extensions="anon,postgis"
FROM scratch AS postgres-extensions
# After the transition this layer will include all extensitons.
# As for now, it's only a couple for testing purposses
COPY --from=generate-ext-index /extensions/*.tar.zst /extensions/
COPY --from=generate-ext-index /ext_index.json /ext_index.json
######################################################################################### #########################################################################################
# #
# Final layer # Final layer
@@ -815,19 +637,14 @@ COPY --from=compute-tools --chown=postgres /home/nonroot/target/release-line-deb
# libgeos, libgdal, libsfcgal1, libproj and libprotobuf-c1 for PostGIS # libgeos, libgdal, libsfcgal1, libproj and libprotobuf-c1 for PostGIS
# libxml2, libxslt1.1 for xml2 # libxml2, libxslt1.1 for xml2
# libzstd1 for zstd # libzstd1 for zstd
# libboost*, libfreetype6, and zlib1g for rdkit
RUN apt update && \ RUN apt update && \
apt install --no-install-recommends -y \ apt install --no-install-recommends -y \
gdb \ gdb \
locales \
libicu67 \ libicu67 \
liblz4-1 \ liblz4-1 \
libreadline8 \ libreadline8 \
libboost-iostreams1.74.0 \
libboost-regex1.74.0 \
libboost-serialization1.74.0 \
libboost-system1.74.0 \
libossp-uuid16 \ libossp-uuid16 \
libfreetype6 \
libgeos-c1v5 \ libgeos-c1v5 \
libgdal28 \ libgdal28 \
libproj19 \ libproj19 \
@@ -837,9 +654,7 @@ RUN apt update && \
libxslt1.1 \ libxslt1.1 \
libzstd1 \ libzstd1 \
libcurl4-openssl-dev \ libcurl4-openssl-dev \
locales \ procps && \
procps \
zlib1g && \
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

View File

@@ -3,7 +3,6 @@
ARG REPOSITORY=neondatabase ARG REPOSITORY=neondatabase
ARG IMAGE=rust ARG IMAGE=rust
ARG TAG=pinned ARG TAG=pinned
ARG BUILD_TAG
FROM $REPOSITORY/$IMAGE:$TAG AS rust-build FROM $REPOSITORY/$IMAGE:$TAG AS rust-build
WORKDIR /home/nonroot WORKDIR /home/nonroot
@@ -17,8 +16,6 @@ ENV CACHEPOT_S3_KEY_PREFIX=cachepot
ARG CACHEPOT_BUCKET=neon-github-dev ARG CACHEPOT_BUCKET=neon-github-dev
#ARG AWS_ACCESS_KEY_ID #ARG AWS_ACCESS_KEY_ID
#ARG AWS_SECRET_ACCESS_KEY #ARG AWS_SECRET_ACCESS_KEY
ARG BUILD_TAG
ENV BUILD_TAG=$BUILD_TAG
COPY . . COPY . .

View File

@@ -108,8 +108,6 @@ postgres-%: postgres-configure-% \
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/pg_buffercache install $(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/pg_buffercache install
+@echo "Compiling pageinspect $*" +@echo "Compiling pageinspect $*"
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/pageinspect install $(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/pageinspect install
+@echo "Compiling amcheck $*"
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/amcheck install
.PHONY: postgres-clean-% .PHONY: postgres-clean-%
postgres-clean-%: postgres-clean-%:

View File

@@ -132,13 +132,13 @@ Python (3.9 or higher), and install python3 packages using `./scripts/pysync` (r
# Create repository in .neon with proper paths to binaries and data # Create repository in .neon with proper paths to binaries and data
# Later that would be responsibility of a package install script # Later that would be responsibility of a package install script
> cargo neon init > cargo neon init
Initializing pageserver node 1 at '127.0.0.1:64000' in ".neon" Starting pageserver at '127.0.0.1:64000' in '.neon'.
# start pageserver, safekeeper, and broker for their intercommunication # start pageserver, safekeeper, and broker for their intercommunication
> cargo neon start > cargo neon start
Starting neon broker at 127.0.0.1:50051. Starting neon broker at 127.0.0.1:50051
storage_broker started, pid: 2918372 storage_broker started, pid: 2918372
Starting pageserver node 1 at '127.0.0.1:64000' in ".neon". Starting pageserver at '127.0.0.1:64000' in '.neon'.
pageserver started, pid: 2918386 pageserver started, pid: 2918386
Starting safekeeper at '127.0.0.1:5454' in '.neon/safekeepers/sk1'. Starting safekeeper at '127.0.0.1:5454' in '.neon/safekeepers/sk1'.
safekeeper 1 started, pid: 2918437 safekeeper 1 started, pid: 2918437
@@ -152,7 +152,8 @@ Setting tenant 9ef87a5bf0d92544f6fafeeb3239695c as a default one
# start postgres compute node # start postgres compute node
> cargo neon endpoint start main > cargo neon endpoint start main
Starting new endpoint main (PostgreSQL v14) on timeline de200bd42b49cc1814412c7e592dd6e9 ... Starting new endpoint main (PostgreSQL v14) on timeline de200bd42b49cc1814412c7e592dd6e9 ...
Starting postgres at 'postgresql://cloud_admin@127.0.0.1:55432/postgres' Extracting base backup to create postgres instance: path=.neon/pgdatadirs/tenants/9ef87a5bf0d92544f6fafeeb3239695c/main port=55432
Starting postgres at 'host=127.0.0.1 port=55432 user=cloud_admin dbname=postgres'
# check list of running postgres instances # check list of running postgres instances
> cargo neon endpoint list > cargo neon endpoint list
@@ -188,17 +189,18 @@ Created timeline 'b3b863fa45fa9e57e615f9f2d944e601' at Lsn 0/16F9A00 for tenant:
# start postgres on that branch # start postgres on that branch
> cargo neon endpoint start migration_check --branch-name migration_check > cargo neon endpoint start migration_check --branch-name migration_check
Starting new endpoint migration_check (PostgreSQL v14) on timeline b3b863fa45fa9e57e615f9f2d944e601 ... Starting new endpoint migration_check (PostgreSQL v14) on timeline b3b863fa45fa9e57e615f9f2d944e601 ...
Starting postgres at 'postgresql://cloud_admin@127.0.0.1:55434/postgres' Extracting base backup to create postgres instance: path=.neon/pgdatadirs/tenants/9ef87a5bf0d92544f6fafeeb3239695c/migration_check port=55433
Starting postgres at 'host=127.0.0.1 port=55433 user=cloud_admin dbname=postgres'
# check the new list of running postgres instances # check the new list of running postgres instances
> cargo neon endpoint list > cargo neon endpoint list
ENDPOINT ADDRESS TIMELINE BRANCH NAME LSN STATUS ENDPOINT ADDRESS TIMELINE BRANCH NAME LSN STATUS
main 127.0.0.1:55432 de200bd42b49cc1814412c7e592dd6e9 main 0/16F9A38 running main 127.0.0.1:55432 de200bd42b49cc1814412c7e592dd6e9 main 0/16F9A38 running
migration_check 127.0.0.1:55434 b3b863fa45fa9e57e615f9f2d944e601 migration_check 0/16F9A70 running migration_check 127.0.0.1:55433 b3b863fa45fa9e57e615f9f2d944e601 migration_check 0/16F9A70 running
# this new postgres instance will have all the data from 'main' postgres, # this new postgres instance will have all the data from 'main' postgres,
# but all modifications would not affect data in original postgres # but all modifications would not affect data in original postgres
> psql -p55434 -h 127.0.0.1 -U cloud_admin postgres > psql -p55433 -h 127.0.0.1 -U cloud_admin postgres
postgres=# select * from t; postgres=# select * from t;
key | value key | value
-----+------- -----+-------

3210
alek_ext/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

21
alek_ext/Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[package]
name = "alek_ext"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.71"
aws-config = { version = "0.55", default-features = false, features=["rustls"] }
aws-sdk-s3 = "0.27"
aws-smithy-http = "0.55"
aws-credential-types = "0.55"
aws-types = "0.55"
remote_storage = { version = "0.1", path = "../libs/remote_storage/" }
tokio = "1.28.2"
toml_edit = "0.19.10"
tracing = "0.1.37"
tracing-subscriber = "0.3.17"
[workspace]

View File

@@ -0,0 +1,6 @@
# fuzzystrmatch extension
comment = 'determine similarities and distance between strings'
default_version = '1.2'
module_pathname = '$libdir/fuzzystrmatch'
relocatable = true
trusted = true

5
alek_ext/pg_cron.control Normal file
View File

@@ -0,0 +1,5 @@
comment = 'Job scheduler for PostgreSQL'
default_version = '1.5'
module_pathname = '$libdir/pg_cron'
relocatable = false
schema = pg_catalog

33
alek_ext/src/awsmwe_v1.rs Normal file
View File

@@ -0,0 +1,33 @@
/*
* This is a MWE of using the aws-sdk-s3 to download a file from an S3 bucket
* */
use aws_sdk_s3::{self, config::Region, Error};
use aws_config::{self, meta::region::RegionProviderChain};
#[tokio::main]
async fn main() -> Result<(), Error> {
let region_provider = RegionProviderChain::first_try(Region::new("eu-central-1"))
.or_default_provider()
.or_else(Region::new("eu-central-1"));
let shared_config = aws_config::from_env().region(region_provider).load().await;
let client = aws_sdk_s3::Client::new(&shared_config);
let bucket_name = "neon-dev-extensions";
let object_key = "fuzzystrmatch.control";
let response = client
.get_object()
.bucket(bucket_name)
.key(object_key)
.send()
.await?;
let stuff = response.body;
let data = stuff.collect().await.expect("error reading data").to_vec();
println!("data: {:?}", std::str::from_utf8(&data));
Ok(())
}

View File

@@ -0,0 +1,52 @@
/* This is a MWE of using our RemoteStorage API to call the aws stuff and download a file
*
*/
use remote_storage::*;
use std::path::Path;
use std::fs::File;
use std::io::{BufWriter, Write};
use toml_edit;
use anyhow;
use tokio::io::AsyncReadExt;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let from_path = "fuzzystrmatch.control";
let remote_from_path = RemotePath::new(Path::new(from_path))?;
println!("{:?}", remote_from_path.clone());
// read configurations from `pageserver.toml`
let cfg_file_path = Path::new("./../.neon/pageserver.toml");
let cfg_file_contents = std::fs::read_to_string(cfg_file_path).unwrap();
let toml = cfg_file_contents
.parse::<toml_edit::Document>()
.expect("Error parsing toml");
let remote_storage_data = toml.get("remote_storage")
.expect("field should be present");
let remote_storage_config = RemoteStorageConfig::from_toml(remote_storage_data)
.expect("error parsing toml")
.expect("error parsing toml");
// query S3 bucket
let remote_storage = GenericRemoteStorage::from_config(&remote_storage_config)?;
let from_path = "fuzzystrmatch.control";
let remote_from_path = RemotePath::new(Path::new(from_path))?;
println!("{:?}", remote_from_path.clone());
// if let GenericRemoteStorage::AwsS3(mybucket) = remote_storage {
// println!("{:?}",mybucket.relative_path_to_s3_object(&remote_from_path));
// }
let mut data = remote_storage.download(&remote_from_path).await.expect("data yay");
let mut write_data_buffer = Vec::new();
data.download_stream.read_to_end(&mut write_data_buffer).await?;
let f = File::create("alek.out").expect("problem creating file");
let mut f = BufWriter::new(f);
f.write_all(&mut write_data_buffer).expect("error writing data");
// let stuff = response.body;
// let data = stuff.collect().await.expect("error reading data").to_vec();
// println!("data: {:?}", std::str::from_utf8(&data));
Ok(())
}

View File

@@ -0,0 +1,53 @@
/*
* This is a MWE of "downloading" a local file from a fake local bucket
* */
use remote_storage::*;
use std::path::Path;
use std::fs::File;
use std::io::{BufWriter, Write};
use toml_edit;
use anyhow;
use tokio::io::AsyncReadExt;
async fn download_file() -> anyhow::Result<()> {
// read configurations from `pageserver.toml`
let cfg_file_path = Path::new("./../.neon/pageserver.toml");
let cfg_file_contents = std::fs::read_to_string(cfg_file_path).unwrap();
let toml = cfg_file_contents
.parse::<toml_edit::Document>()
.expect("Error parsing toml");
let remote_storage_data = toml.get("remote_storage")
.expect("field should be present");
let remote_storage_config = RemoteStorageConfig::from_toml(remote_storage_data)
.expect("error parsing toml")
.expect("error parsing toml");
// query S3 bucket
let remote_storage = GenericRemoteStorage::from_config(&remote_storage_config)?;
let from_path = "neon-dev-extensions/fuzzystrmatch.control";
let remote_from_path = RemotePath::new(Path::new(from_path))?;
println!("im fine");
println!("{:?}",remote_storage_config);
let mut data = remote_storage.download(&remote_from_path).await.expect("data yay");
let mut write_data_buffer = Vec::new();
data.download_stream.read_to_end(&mut write_data_buffer).await?;
// write `data` to a file locally
let f = File::create("alek.out").expect("problem creating file");
let mut f = BufWriter::new(f);
f.write_all(&mut write_data_buffer).expect("error writing data");
Ok(())
}
#[tokio::main]
async fn main() {
match download_file().await {
Err(_)=>println!("Err"),
_ => println!("SUCEECESS")
}
}

53
alek_ext/src/main.rs Normal file
View File

@@ -0,0 +1,53 @@
/*
* This is a MWE of using the RemoteStorage API to list and download files from aws
*/
macro_rules! alek { ($expression:expr) => { println!("{:?}", $expression); }; }
use remote_storage::*;
use std::path::Path;
use std::fs::File;
use std::io::{BufWriter, Write};
use toml_edit;
use anyhow::{self, Context};
use tokio::io::AsyncReadExt;
use tracing::*;
use tracing_subscriber;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let subscriber = tracing_subscriber::FmtSubscriber::new();
tracing::subscriber::set_global_default(subscriber)?;
// TODO: read configs from a different place!
let cfg_file_path = Path::new("./../.neon/pageserver.toml");
let cfg_file_contents = std::fs::read_to_string(cfg_file_path)
.with_context(|| format!( "Failed to read pageserver config at '{}'", cfg_file_path.display()))?;
let toml = cfg_file_contents
.parse::<toml_edit::Document>()
.with_context(|| format!( "Failed to parse '{}' as pageserver config", cfg_file_path.display()))?;
let remote_storage_data = toml.get("remote_storage")
.context("field should be present")?;
let remote_storage_config = RemoteStorageConfig::from_toml(remote_storage_data)?
.context("error configuring remote storage")?;
let remote_storage = GenericRemoteStorage::from_config(&remote_storage_config)?;
let folder = RemotePath::new(Path::new("public_extensions"))?;
// lists all the files in the public_extensions folder
let from_paths = remote_storage.list_files(Some(&folder)).await?;
alek!(from_paths);
for remote_from_path in from_paths {
if remote_from_path.extension() == Some("control") {
let file_name = remote_from_path.object_name().expect("it must exist");
info!("{:?}", file_name);
alek!(&remote_from_path);
// download the file
let mut download = remote_storage.download(&remote_from_path).await?;
// write the file to a local location
let mut write_data_buffer = Vec::new();
download.download_stream.read_to_end(&mut write_data_buffer).await?;
let mut output_file = BufWriter::new(File::create(file_name)?);
output_file.write_all(&mut write_data_buffer)?;
}
}
Ok(())
}

View File

@@ -0,0 +1,65 @@
/*
**WIP**
* This is a MWE of using our RemoteStorage API to call the aws stuff and download multiple files
*/
#![allow(unused_imports)]
use remote_storage::*;
use std::path::Path;
use std::fs::File;
use std::io::{BufWriter, Write};
use toml_edit;
use anyhow::{self, Context};
use tokio::io::AsyncReadExt;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
/* me trying to hack RemotePath into submission */
let cfg_file_path = Path::new("./../.neon/pageserver.toml");
let cfg_file_contents = std::fs::read_to_string(cfg_file_path)
.expect("couldn't find pageserver.toml; make sure you are in neon/alek_ext");
let toml = cfg_file_contents
.parse::<toml_edit::Document>()
.expect("Error parsing toml");
let remote_storage_data = toml.get("remote_storage")
.expect("field should be present");
let remote_storage_config = RemoteStorageConfig::from_toml(remote_storage_data)
.expect("error parsing toml")
.expect("error parsing toml");
let remote_storage = GenericRemoteStorage::from_config(&remote_storage_config)?;
if let GenericRemoteStorage::AwsS3(mybucket) = remote_storage {
let resp = mybucket
.client
.list_objects_v2()
.bucket("neon-dev-extensions")
.set_prefix(Some("public_extensions".to_string()))
.delimiter("/".to_string())
.send().await?;
let z = resp.common_prefixes.unwrap();
for yy in z {
println!("plzplz: {:?}",yy);
}
let mut i = 0;
for remote_from_path in from_paths {
i += 1;
println!("{:?}", &remote_from_path);
if remote_from_path.extension() == Some("control") {
let mut data = remote_storage.download(&remote_from_path).await?;
// write `data` to a file locally
// TODO: I think that the way I'm doing this is not optimal;
// It should be possible to write the data directly to a file
// rather than first writing it to a vector...
let mut write_data_buffer = Vec::new();
data.download_stream.read_to_end(&mut write_data_buffer).await?;
let f = File::create("alek{i}.out").expect("problem creating file");
let mut f = BufWriter::new(f);
f.write_all(&mut write_data_buffer).expect("error writing data");
}
}
}
Ok(())
}

View File

@@ -6,10 +6,8 @@ license.workspace = true
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
async-compression.workspace = true
chrono.workspace = true chrono.workspace = true
clap.workspace = true clap.workspace = true
flate2.workspace = true
futures.workspace = true futures.workspace = true
hyper = { workspace = true, features = ["full"] } hyper = { workspace = true, features = ["full"] }
notify.workspace = true notify.workspace = true
@@ -32,3 +30,5 @@ url.workspace = true
compute_api.workspace = true compute_api.workspace = true
utils.workspace = true utils.workspace = true
workspace_hack.workspace = true workspace_hack.workspace = true
remote_storage = { version = "0.1", path = "../libs/remote_storage/" }
toml_edit.workspace = true

View File

@@ -53,17 +53,20 @@ use compute_tools::logger::*;
use compute_tools::monitor::launch_monitor; use compute_tools::monitor::launch_monitor;
use compute_tools::params::*; use compute_tools::params::*;
use compute_tools::spec::*; use compute_tools::spec::*;
use compute_tools::extensions::*;
const BUILD_TAG_DEFAULT: &str = "local"; #[tokio::main]
async fn main() -> Result<()> {
fn main() -> Result<()> {
init_tracing_and_logging(DEFAULT_LOG_LEVEL)?; init_tracing_and_logging(DEFAULT_LOG_LEVEL)?;
let build_tag = option_env!("BUILD_TAG").unwrap_or(BUILD_TAG_DEFAULT);
info!("build_tag: {build_tag}");
let matches = cli().get_matches(); let matches = cli().get_matches();
let config = get_s3_config(&matches)
.expect("Hopefully get_s3_config works");
download_extension(&config, ExtensionType::Shared)
.await
.expect("Assume downloads can't error.");
// let mut file = File::create("alek.txt")?;
// file.write_all(b"success?")?;
let http_port = *matches let http_port = *matches
.get_one::<u16>("http-port") .get_one::<u16>("http-port")
@@ -193,13 +196,6 @@ fn main() -> Result<()> {
if !spec_set { if !spec_set {
// No spec provided, hang waiting for it. // No spec provided, hang waiting for it.
info!("no compute spec provided, waiting"); info!("no compute spec provided, waiting");
// TODO this can stall startups in the unlikely event that we bind
// this compute node while it's busy prewarming. It's not too
// bad because it's just 100ms and unlikely, but it's an
// avoidable problem.
compute.prewarm_postgres()?;
let mut state = compute.state.lock().unwrap(); let mut state = compute.state.lock().unwrap();
while state.status != ComputeStatus::ConfigurationPending { while state.status != ComputeStatus::ConfigurationPending {
state = compute.state_changed.wait(state).unwrap(); state = compute.state_changed.wait(state).unwrap();
@@ -215,6 +211,9 @@ fn main() -> Result<()> {
// We got all we need, update the state. // We got all we need, update the state.
let mut state = compute.state.lock().unwrap(); let mut state = compute.state.lock().unwrap();
// Now we have the spec, and also the tenant id, so we can download the user's personal extensions
// download_extension(&config, ExtensionType::Tenant(FIXME tenant_id.into()));
// Record for how long we slept waiting for the spec. // Record for how long we slept waiting for the spec.
state.metrics.wait_for_spec_ms = Utc::now() state.metrics.wait_for_spec_ms = Utc::now()
.signed_duration_since(state.start_time) .signed_duration_since(state.start_time)
@@ -230,9 +229,13 @@ fn main() -> Result<()> {
drop(state); drop(state);
// Launch remaining service threads // Launch remaining service threads
let _monitor_handle = launch_monitor(&compute); let _monitor_handle = launch_monitor(&compute).expect("cannot launch compute monitor thread");
let _configurator_handle = launch_configurator(&compute); let _configurator_handle =
launch_configurator(&compute).expect("cannot launch configurator thread");
// Now we are ready to download library extensions
// download_extension(&config, ExtensionType::Library(FIXME library_name.into()));
// Start Postgres // Start Postgres
let mut delay_exit = false; let mut delay_exit = false;
let mut exit_code = None; let mut exit_code = None;
@@ -262,16 +265,6 @@ fn main() -> Result<()> {
exit_code = ecode.code() exit_code = ecode.code()
} }
// Maybe sync safekeepers again, to speed up next startup
let compute_state = compute.state.lock().unwrap().clone();
let pspec = compute_state.pspec.as_ref().expect("spec must be set");
if matches!(pspec.spec.mode, compute_api::spec::ComputeMode::Primary) {
info!("syncing safekeepers on shutdown");
let storage_auth_token = pspec.storage_auth_token.clone();
let lsn = compute.sync_safekeepers(storage_auth_token)?;
info!("synced safekeepers at lsn {lsn}");
}
if let Err(err) = compute.check_for_core_dumps() { if let Err(err) = compute.check_for_core_dumps() {
error!("error while checking for core dumps: {err:?}"); error!("error while checking for core dumps: {err:?}");
} }

View File

@@ -1,5 +1,4 @@
use std::fs; use std::fs;
use std::io::BufRead;
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::path::Path; use std::path::Path;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
@@ -8,22 +7,18 @@ use std::sync::{Condvar, Mutex};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use postgres::{Client, NoTls}; use postgres::{Client, NoTls};
use tokio_postgres; use tokio_postgres;
use tracing::{error, info, instrument, warn}; use tracing::{info, instrument, warn};
use utils::id::{TenantId, TimelineId}; use utils::id::{TenantId, TimelineId};
use utils::lsn::Lsn; use utils::lsn::Lsn;
use compute_api::responses::{ComputeMetrics, ComputeStatus}; use compute_api::responses::{ComputeMetrics, ComputeStatus};
use compute_api::spec::{ComputeMode, ComputeSpec}; use compute_api::spec::{ComputeMode, ComputeSpec};
use utils::measured_stream::MeasuredReader;
use crate::config; use crate::config;
use crate::pg_helpers::*; use crate::pg_helpers::*;
use crate::spec::*; use crate::spec::*;
use crate::sync_sk::{check_if_synced, ping_safekeeper};
/// Compute node info shared across several `compute_ctl` threads. /// Compute node info shared across several `compute_ctl` threads.
pub struct ComputeNode { pub struct ComputeNode {
@@ -89,7 +84,6 @@ pub struct ParsedSpec {
pub tenant_id: TenantId, pub tenant_id: TenantId,
pub timeline_id: TimelineId, pub timeline_id: TimelineId,
pub pageserver_connstr: String, pub pageserver_connstr: String,
pub safekeeper_connstrings: Vec<String>,
pub storage_auth_token: Option<String>, pub storage_auth_token: Option<String>,
} }
@@ -107,21 +101,6 @@ impl TryFrom<ComputeSpec> for ParsedSpec {
.clone() .clone()
.or_else(|| spec.cluster.settings.find("neon.pageserver_connstring")) .or_else(|| spec.cluster.settings.find("neon.pageserver_connstring"))
.ok_or("pageserver connstr should be provided")?; .ok_or("pageserver connstr should be provided")?;
let safekeeper_connstrings = if spec.safekeeper_connstrings.is_empty() {
if matches!(spec.mode, ComputeMode::Primary) {
spec.cluster
.settings
.find("neon.safekeepers")
.ok_or("safekeeper connstrings should be provided")?
.split(',')
.map(|str| str.to_string())
.collect()
} else {
vec![]
}
} else {
spec.safekeeper_connstrings.clone()
};
let storage_auth_token = spec.storage_auth_token.clone(); let storage_auth_token = spec.storage_auth_token.clone();
let tenant_id: TenantId = if let Some(tenant_id) = spec.tenant_id { let tenant_id: TenantId = if let Some(tenant_id) = spec.tenant_id {
tenant_id tenant_id
@@ -147,7 +126,6 @@ impl TryFrom<ComputeSpec> for ParsedSpec {
Ok(ParsedSpec { Ok(ParsedSpec {
spec, spec,
pageserver_connstr, pageserver_connstr,
safekeeper_connstrings,
storage_auth_token, storage_auth_token,
tenant_id, tenant_id,
timeline_id, timeline_id,
@@ -155,84 +133,6 @@ impl TryFrom<ComputeSpec> for ParsedSpec {
} }
} }
/// Create special neon_superuser role, that's a slightly nerfed version of a real superuser
/// that we give to customers
fn create_neon_superuser(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
let roles = spec
.cluster
.roles
.iter()
.map(|r| escape_literal(&r.name))
.collect::<Vec<_>>();
let dbs = spec
.cluster
.databases
.iter()
.map(|db| escape_literal(&db.name))
.collect::<Vec<_>>();
let roles_decl = if roles.is_empty() {
String::from("roles text[] := NULL;")
} else {
format!(
r#"
roles text[] := ARRAY(SELECT rolname
FROM pg_catalog.pg_roles
WHERE rolname IN ({}));"#,
roles.join(", ")
)
};
let database_decl = if dbs.is_empty() {
String::from("dbs text[] := NULL;")
} else {
format!(
r#"
dbs text[] := ARRAY(SELECT datname
FROM pg_catalog.pg_database
WHERE datname IN ({}));"#,
dbs.join(", ")
)
};
// ALL PRIVILEGES grants CREATE, CONNECT, and TEMPORARY on all databases
// (see https://www.postgresql.org/docs/current/ddl-priv.html)
let query = format!(
r#"
DO $$
DECLARE
r text;
{}
{}
BEGIN
IF NOT EXISTS (
SELECT FROM pg_catalog.pg_roles WHERE rolname = 'neon_superuser')
THEN
CREATE ROLE neon_superuser CREATEDB CREATEROLE NOLOGIN IN ROLE pg_read_all_data, pg_write_all_data;
IF array_length(roles, 1) IS NOT NULL THEN
EXECUTE format('GRANT neon_superuser TO %s',
array_to_string(ARRAY(SELECT quote_ident(x) FROM unnest(roles) as x), ', '));
FOREACH r IN ARRAY roles LOOP
EXECUTE format('ALTER ROLE %s CREATEROLE CREATEDB', quote_ident(r));
END LOOP;
END IF;
IF array_length(dbs, 1) IS NOT NULL THEN
EXECUTE format('GRANT ALL PRIVILEGES ON DATABASE %s TO neon_superuser',
array_to_string(ARRAY(SELECT quote_ident(x) FROM unnest(dbs) as x), ', '));
END IF;
END IF;
END
$$;"#,
roles_decl, database_decl,
);
info!("Neon superuser created:\n{}", &query);
client
.simple_query(&query)
.map_err(|e| anyhow::anyhow!(e).context(query))?;
Ok(())
}
impl ComputeNode { impl ComputeNode {
pub fn set_status(&self, status: ComputeStatus) { pub fn set_status(&self, status: ComputeStatus) {
let mut state = self.state.lock().unwrap(); let mut state = self.state.lock().unwrap();
@@ -257,7 +157,7 @@ impl ComputeNode {
// Get basebackup from the libpq connection to pageserver using `connstr` and // Get basebackup from the libpq connection to pageserver using `connstr` and
// unarchive it to `pgdata` directory overriding all its previous content. // unarchive it to `pgdata` directory overriding all its previous content.
#[instrument(skip_all, fields(%lsn))] #[instrument(skip(self, compute_state))]
fn get_basebackup(&self, compute_state: &ComputeState, lsn: Lsn) -> Result<()> { fn get_basebackup(&self, compute_state: &ComputeState, lsn: Lsn) -> Result<()> {
let spec = compute_state.pspec.as_ref().expect("spec must be set"); let spec = compute_state.pspec.as_ref().expect("spec must be set");
let start_time = Utc::now(); let start_time = Utc::now();
@@ -275,52 +175,20 @@ impl ComputeNode {
let mut client = config.connect(NoTls)?; let mut client = config.connect(NoTls)?;
let basebackup_cmd = match lsn { let basebackup_cmd = match lsn {
// HACK We don't use compression on first start (Lsn(0)) because there's no API for it Lsn(0) => format!("basebackup {} {}", spec.tenant_id, spec.timeline_id), // First start of the compute
Lsn(0) => format!("basebackup {} {}", spec.tenant_id, spec.timeline_id), _ => format!("basebackup {} {} {}", spec.tenant_id, spec.timeline_id, lsn),
_ => format!(
"basebackup {} {} {} --gzip",
spec.tenant_id, spec.timeline_id, lsn
),
}; };
let copyreader = client.copy_out(basebackup_cmd.as_str())?; let copyreader = client.copy_out(basebackup_cmd.as_str())?;
let mut measured_reader = MeasuredReader::new(copyreader);
// Check the magic number to see if it's a gzip or not. Even though
// we might explicitly ask for gzip, an old pageserver with no implementation
// of gzip compression might send us uncompressed data. After some time
// passes we can assume all pageservers know how to compress and we can
// delete this check.
//
// If the data is not gzip, it will be tar. It will not be mistakenly
// recognized as gzip because tar starts with an ascii encoding of a filename,
// and 0x1f and 0x8b are unlikely first characters for any filename. Moreover,
// we send the "global" directory first from the pageserver, so it definitely
// won't be recognized as gzip.
let mut bufreader = std::io::BufReader::new(&mut measured_reader);
let gzip = {
let peek = bufreader.fill_buf().unwrap();
peek[0] == 0x1f && peek[1] == 0x8b
};
// Read the archive directly from the `CopyOutReader` // Read the archive directly from the `CopyOutReader`
// //
// Set `ignore_zeros` so that unpack() reads all the Copy data and // Set `ignore_zeros` so that unpack() reads all the Copy data and
// doesn't stop at the end-of-archive marker. Otherwise, if the server // doesn't stop at the end-of-archive marker. Otherwise, if the server
// sends an Error after finishing the tarball, we will not notice it. // sends an Error after finishing the tarball, we will not notice it.
if gzip { let mut ar = tar::Archive::new(copyreader);
let mut ar = tar::Archive::new(flate2::read::GzDecoder::new(&mut bufreader)); ar.set_ignore_zeros(true);
ar.set_ignore_zeros(true); ar.unpack(&self.pgdata)?;
ar.unpack(&self.pgdata)?;
} else {
let mut ar = tar::Archive::new(&mut bufreader);
ar.set_ignore_zeros(true);
ar.unpack(&self.pgdata)?;
};
// Report metrics
self.state.lock().unwrap().metrics.basebackup_bytes =
measured_reader.get_byte_count() as u64;
self.state.lock().unwrap().metrics.basebackup_ms = Utc::now() self.state.lock().unwrap().metrics.basebackup_ms = Utc::now()
.signed_duration_since(start_time) .signed_duration_since(start_time)
.to_std() .to_std()
@@ -329,106 +197,10 @@ impl ComputeNode {
Ok(()) Ok(())
} }
pub async fn check_safekeepers_synced_async(
&self,
compute_state: &ComputeState,
) -> Result<Option<Lsn>> {
// Construct a connection config for each safekeeper
let pspec: ParsedSpec = compute_state
.pspec
.as_ref()
.expect("spec must be set")
.clone();
let sk_connstrs: Vec<String> = pspec.safekeeper_connstrings.clone();
let sk_configs = sk_connstrs.into_iter().map(|connstr| {
// Format connstr
let id = connstr.clone();
let connstr = format!("postgresql://no_user@{}", connstr);
let options = format!(
"-c timeline_id={} tenant_id={}",
pspec.timeline_id, pspec.tenant_id
);
// Construct client
let mut config = tokio_postgres::Config::from_str(&connstr).unwrap();
config.options(&options);
if let Some(storage_auth_token) = pspec.storage_auth_token.clone() {
config.password(storage_auth_token);
}
(id, config)
});
// Create task set to query all safekeepers
let mut tasks = FuturesUnordered::new();
let quorum = sk_configs.len() / 2 + 1;
for (id, config) in sk_configs {
let timeout = tokio::time::Duration::from_millis(100);
let task = tokio::time::timeout(timeout, ping_safekeeper(id, config));
tasks.push(tokio::spawn(task));
}
// Get a quorum of responses or errors
let mut responses = Vec::new();
let mut join_errors = Vec::new();
let mut task_errors = Vec::new();
let mut timeout_errors = Vec::new();
while let Some(response) = tasks.next().await {
match response {
Ok(Ok(Ok(r))) => responses.push(r),
Ok(Ok(Err(e))) => task_errors.push(e),
Ok(Err(e)) => timeout_errors.push(e),
Err(e) => join_errors.push(e),
};
if responses.len() >= quorum {
break;
}
if join_errors.len() + task_errors.len() + timeout_errors.len() >= quorum {
break;
}
}
// In case of error, log and fail the check, but don't crash.
// We're playing it safe because these errors could be transient
// and we don't yet retry. Also being careful here allows us to
// be backwards compatible with safekeepers that don't have the
// TIMELINE_STATUS API yet.
if responses.len() < quorum {
error!(
"failed sync safekeepers check {:?} {:?} {:?}",
join_errors, task_errors, timeout_errors
);
return Ok(None);
}
Ok(check_if_synced(responses))
}
// Fast path for sync_safekeepers. If they're already synced we get the lsn
// in one roundtrip. If not, we should do a full sync_safekeepers.
pub fn check_safekeepers_synced(&self, compute_state: &ComputeState) -> Result<Option<Lsn>> {
let start_time = Utc::now();
// Run actual work with new tokio runtime
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to create rt");
let result = rt.block_on(self.check_safekeepers_synced_async(compute_state));
// Record runtime
self.state.lock().unwrap().metrics.sync_sk_check_ms = Utc::now()
.signed_duration_since(start_time)
.to_std()
.unwrap()
.as_millis() as u64;
result
}
// Run `postgres` in a special mode with `--sync-safekeepers` argument // Run `postgres` in a special mode with `--sync-safekeepers` argument
// and return the reported LSN back to the caller. // and return the reported LSN back to the caller.
#[instrument(skip_all)] #[instrument(skip(self, storage_auth_token))]
pub fn sync_safekeepers(&self, storage_auth_token: Option<String>) -> Result<Lsn> { fn sync_safekeepers(&self, storage_auth_token: Option<String>) -> Result<Lsn> {
let start_time = Utc::now(); let start_time = Utc::now();
let sync_handle = Command::new(&self.pgbin) let sync_handle = Command::new(&self.pgbin)
@@ -472,7 +244,7 @@ impl ComputeNode {
/// Do all the preparations like PGDATA directory creation, configuration, /// Do all the preparations like PGDATA directory creation, configuration,
/// safekeepers sync, basebackup, etc. /// safekeepers sync, basebackup, etc.
#[instrument(skip_all)] #[instrument(skip(self, compute_state))]
pub fn prepare_pgdata(&self, compute_state: &ComputeState) -> Result<()> { pub fn prepare_pgdata(&self, compute_state: &ComputeState) -> Result<()> {
let pspec = compute_state.pspec.as_ref().expect("spec must be set"); let pspec = compute_state.pspec.as_ref().expect("spec must be set");
let spec = &pspec.spec; let spec = &pspec.spec;
@@ -487,14 +259,10 @@ impl ComputeNode {
// cannot sync safekeepers. // cannot sync safekeepers.
let lsn = match spec.mode { let lsn = match spec.mode {
ComputeMode::Primary => { ComputeMode::Primary => {
info!("checking if safekeepers are synced"); info!("starting safekeepers syncing");
let lsn = if let Ok(Some(lsn)) = self.check_safekeepers_synced(compute_state) { let lsn = self
lsn .sync_safekeepers(pspec.storage_auth_token.clone())
} else { .with_context(|| "failed to sync safekeepers")?;
info!("starting safekeepers syncing");
self.sync_safekeepers(pspec.storage_auth_token.clone())
.with_context(|| "failed to sync safekeepers")?
};
info!("safekeepers synced at LSN {}", lsn); info!("safekeepers synced at LSN {}", lsn);
lsn lsn
} }
@@ -532,53 +300,9 @@ impl ComputeNode {
Ok(()) Ok(())
} }
/// Start and stop a postgres process to warm up the VM for startup.
pub fn prewarm_postgres(&self) -> Result<()> {
info!("prewarming");
// Create pgdata
let pgdata = &format!("{}.warmup", self.pgdata);
create_pgdata(pgdata)?;
// Run initdb to completion
info!("running initdb");
let initdb_bin = Path::new(&self.pgbin).parent().unwrap().join("initdb");
Command::new(initdb_bin)
.args(["-D", pgdata])
.output()
.expect("cannot start initdb process");
// Write conf
use std::io::Write;
let conf_path = Path::new(pgdata).join("postgresql.conf");
let mut file = std::fs::File::create(conf_path)?;
writeln!(file, "shared_buffers=65536")?;
writeln!(file, "port=51055")?; // Nobody should be connecting
writeln!(file, "shared_preload_libraries = 'neon'")?;
// Start postgres
info!("starting postgres");
let mut pg = Command::new(&self.pgbin)
.args(["-D", pgdata])
.spawn()
.expect("cannot start postgres process");
// Stop it when it's ready
info!("waiting for postgres");
wait_for_postgres(&mut pg, Path::new(pgdata))?;
pg.kill()?;
info!("sent kill signal");
pg.wait()?;
info!("done prewarming");
// clean up
let _ok = fs::remove_dir_all(pgdata);
Ok(())
}
/// Start Postgres as a child process and manage DBs/roles. /// Start Postgres as a child process and manage DBs/roles.
/// After that this will hang waiting on the postmaster process to exit. /// After that this will hang waiting on the postmaster process to exit.
#[instrument(skip_all)] #[instrument(skip(self))]
pub fn start_postgres( pub fn start_postgres(
&self, &self,
storage_auth_token: Option<String>, storage_auth_token: Option<String>,
@@ -602,7 +326,7 @@ impl ComputeNode {
} }
/// Do initial configuration of the already started Postgres. /// Do initial configuration of the already started Postgres.
#[instrument(skip_all)] #[instrument(skip(self, compute_state))]
pub fn apply_config(&self, compute_state: &ComputeState) -> Result<()> { pub fn apply_config(&self, compute_state: &ComputeState) -> Result<()> {
// If connection fails, // If connection fails,
// it may be the old node with `zenith_admin` superuser. // it may be the old node with `zenith_admin` superuser.
@@ -623,8 +347,6 @@ impl ComputeNode {
.map_err(|_| anyhow::anyhow!("invalid connstr"))?; .map_err(|_| anyhow::anyhow!("invalid connstr"))?;
let mut client = Client::connect(zenith_admin_connstr.as_str(), NoTls)?; let mut client = Client::connect(zenith_admin_connstr.as_str(), NoTls)?;
// Disable forwarding so that users don't get a cloud_admin role
client.simple_query("SET neon.forward_ddl = false")?;
client.simple_query("CREATE USER cloud_admin WITH SUPERUSER")?; client.simple_query("CREATE USER cloud_admin WITH SUPERUSER")?;
client.simple_query("GRANT zenith_admin TO cloud_admin")?; client.simple_query("GRANT zenith_admin TO cloud_admin")?;
drop(client); drop(client);
@@ -635,28 +357,31 @@ impl ComputeNode {
Ok(client) => client, Ok(client) => client,
}; };
// Proceed with post-startup configuration. Note, that order of operations is important.
// Disable DDL forwarding because control plane already knows about these roles/databases. // Disable DDL forwarding because control plane already knows about these roles/databases.
client.simple_query("SET neon.forward_ddl = false")?; client.simple_query("SET neon.forward_ddl = false")?;
// Proceed with post-startup configuration. Note, that order of operations is important.
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;
create_neon_superuser(spec, &mut client)?;
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)?;
handle_grants(spec, self.connstr.as_str())?; handle_grants(spec, self.connstr.as_str(), &mut client)?;
handle_extensions(spec, &mut client)?; handle_extensions(spec, &mut client)?;
// 'Close' connection // 'Close' connection
drop(client); drop(client);
info!(
"finished configuration of compute for project {}",
spec.cluster.cluster_id.as_deref().unwrap_or("None")
);
Ok(()) Ok(())
} }
// We could've wrapped this around `pg_ctl reload`, but right now we don't use // We could've wrapped this around `pg_ctl reload`, but right now we don't use
// `pg_ctl` for start / stop, so this just seems much easier to do as we already // `pg_ctl` for start / stop, so this just seems much easier to do as we already
// have opened connection to Postgres and superuser access. // have opened connection to Postgres and superuser access.
#[instrument(skip_all)] #[instrument(skip(self, client))]
fn pg_reload_conf(&self, client: &mut Client) -> Result<()> { fn pg_reload_conf(&self, client: &mut Client) -> Result<()> {
client.simple_query("SELECT pg_reload_conf()")?; client.simple_query("SELECT pg_reload_conf()")?;
Ok(()) Ok(())
@@ -664,7 +389,7 @@ impl ComputeNode {
/// Similar to `apply_config()`, but does a bit different sequence of operations, /// Similar to `apply_config()`, but does a bit different sequence of operations,
/// as it's used to reconfigure a previously started and configured Postgres node. /// as it's used to reconfigure a previously started and configured Postgres node.
#[instrument(skip_all)] #[instrument(skip(self))]
pub fn reconfigure(&self) -> Result<()> { pub fn reconfigure(&self) -> Result<()> {
let spec = self.state.lock().unwrap().pspec.clone().unwrap().spec; let spec = self.state.lock().unwrap().pspec.clone().unwrap().spec;
@@ -682,7 +407,7 @@ impl ComputeNode {
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)?;
handle_grants(&spec, self.connstr.as_str())?; handle_grants(&spec, self.connstr.as_str(), &mut client)?;
handle_extensions(&spec, &mut client)?; handle_extensions(&spec, &mut client)?;
} }
@@ -699,38 +424,33 @@ impl ComputeNode {
Ok(()) Ok(())
} }
#[instrument(skip_all)] #[instrument(skip(self))]
pub fn start_compute(&self) -> Result<std::process::Child> { pub fn start_compute(&self) -> Result<std::process::Child> {
let compute_state = self.state.lock().unwrap().clone(); let compute_state = self.state.lock().unwrap().clone();
let pspec = compute_state.pspec.as_ref().expect("spec must be set"); let spec = compute_state.pspec.as_ref().expect("spec must be set");
info!( info!(
"starting compute for project {}, operation {}, tenant {}, timeline {}", "starting compute for project {}, operation {}, tenant {}, timeline {}",
pspec.spec.cluster.cluster_id.as_deref().unwrap_or("None"), spec.spec.cluster.cluster_id.as_deref().unwrap_or("None"),
pspec.spec.operation_uuid.as_deref().unwrap_or("None"), spec.spec.operation_uuid.as_deref().unwrap_or("None"),
pspec.tenant_id, spec.tenant_id,
pspec.timeline_id, spec.timeline_id,
); );
self.prepare_pgdata(&compute_state)?; self.prepare_pgdata(&compute_state)?;
let start_time = Utc::now(); let start_time = Utc::now();
let pg = self.start_postgres(pspec.storage_auth_token.clone())?;
let config_time = Utc::now(); let pg = self.start_postgres(spec.storage_auth_token.clone())?;
if pspec.spec.mode == ComputeMode::Primary && !pspec.spec.skip_pg_catalog_updates {
if spec.spec.mode == ComputeMode::Primary {
self.apply_config(&compute_state)?; self.apply_config(&compute_state)?;
} }
let startup_end_time = Utc::now(); let startup_end_time = Utc::now();
{ {
let mut state = self.state.lock().unwrap(); let mut state = self.state.lock().unwrap();
state.metrics.start_postgres_ms = config_time
.signed_duration_since(start_time)
.to_std()
.unwrap()
.as_millis() as u64;
state.metrics.config_ms = startup_end_time state.metrics.config_ms = startup_end_time
.signed_duration_since(config_time) .signed_duration_since(start_time)
.to_std() .to_std()
.unwrap() .unwrap()
.as_millis() as u64; .as_millis() as u64;
@@ -742,18 +462,6 @@ impl ComputeNode {
} }
self.set_status(ComputeStatus::Running); self.set_status(ComputeStatus::Running);
info!(
"finished configuration of compute for project {}",
pspec.spec.cluster.cluster_id.as_deref().unwrap_or("None")
);
// Log metrics so that we can search for slow operations in logs
let metrics = {
let state = self.state.lock().unwrap();
state.metrics.clone()
};
info!(?metrics, "compute start finished");
Ok(pg) Ok(pg)
} }

View File

@@ -47,22 +47,30 @@ pub fn write_postgres_conf(path: &Path, spec: &ComputeSpec) -> Result<()> {
// Add options for connecting to storage // Add options for connecting to storage
writeln!(file, "# Neon storage settings")?; writeln!(file, "# Neon storage settings")?;
if let Some(s) = &spec.pageserver_connstring { if let Some(s) = &spec.pageserver_connstring {
writeln!(file, "neon.pageserver_connstring={}", escape_conf_value(s))?; writeln!(
file,
"neon.pageserver_connstring='{}'",
escape_conf_value(s)
)?;
} }
if !spec.safekeeper_connstrings.is_empty() { if !spec.safekeeper_connstrings.is_empty() {
writeln!( writeln!(
file, file,
"neon.safekeepers={}", "neon.safekeepers='{}'",
escape_conf_value(&spec.safekeeper_connstrings.join(",")) escape_conf_value(&spec.safekeeper_connstrings.join(","))
)?; )?;
} }
if let Some(s) = &spec.tenant_id { if let Some(s) = &spec.tenant_id {
writeln!(file, "neon.tenant_id={}", escape_conf_value(&s.to_string()))?; writeln!(
file,
"neon.tenant_id='{}'",
escape_conf_value(&s.to_string())
)?;
} }
if let Some(s) = &spec.timeline_id { if let Some(s) = &spec.timeline_id {
writeln!( writeln!(
file, file,
"neon.timeline_id={}", "neon.timeline_id='{}'",
escape_conf_value(&s.to_string()) escape_conf_value(&s.to_string())
)?; )?;
} }

View File

@@ -1,13 +1,14 @@
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::thread;
use anyhow::Result;
use tracing::{error, info, instrument}; use tracing::{error, info, instrument};
use compute_api::responses::ComputeStatus; use compute_api::responses::ComputeStatus;
use crate::compute::ComputeNode; use crate::compute::ComputeNode;
#[instrument(skip_all)] #[instrument(skip(compute))]
fn configurator_main_loop(compute: &Arc<ComputeNode>) { fn configurator_main_loop(compute: &Arc<ComputeNode>) {
info!("waiting for reconfiguration requests"); info!("waiting for reconfiguration requests");
loop { loop {
@@ -41,14 +42,13 @@ fn configurator_main_loop(compute: &Arc<ComputeNode>) {
} }
} }
pub fn launch_configurator(compute: &Arc<ComputeNode>) -> thread::JoinHandle<()> { pub fn launch_configurator(compute: &Arc<ComputeNode>) -> Result<thread::JoinHandle<()>> {
let compute = Arc::clone(compute); let compute = Arc::clone(compute);
thread::Builder::new() Ok(thread::Builder::new()
.name("compute-configurator".into()) .name("compute-configurator".into())
.spawn(move || { .spawn(move || {
configurator_main_loop(&compute); configurator_main_loop(&compute);
info!("configurator thread is exited"); info!("configurator thread is exited");
}) })?)
.expect("cannot launch configurator thread")
} }

View File

@@ -0,0 +1,96 @@
// This is some code for downloading postgres extensions from AWS s3
use std::path::Path;
use std::fs::File;
use std::io::Write;
use clap::{ArgMatches};
use toml_edit;
use remote_storage::*;
fn get_pg_config(argument: &str) -> String {
// FIXME: this function panics if it runs into any issues
let config_output = std::process::Command::new("pg_config")
.arg(argument)
.output()
.expect("pg_config should be installed");
assert!(config_output.status.success());
let stdout = std::str::from_utf8(&config_output.stdout).unwrap();
stdout.trim().to_string()
}
fn download_helper(remote_storage: &GenericRemoteStorage, remote_from_path: &RemotePath, to_path: &str) -> anyhow::Result<()> {
let file_name = remote_from_path.object_name().expect("it must exist");
info!("Downloading {:?}",file_name);
let mut download = remote_storage.download(&remote_from_path).await?;
let mut write_data_buffer = Vec::new();
download.download_stream.read_to_end(&mut write_data_buffer).await?;
let mut output_file = BufWriter::new(File::create(file_name)?);
output_file.write_all(&mut write_data_buffer)?;
Ok(())
}
pub enum ExtensionType {
Shared,
Tenant(String),
Library(String)
}
pub async fn download_extension(config: &RemoteStorageConfig, ext_type: ExtensionType) -> anyhow::Result<()>{
let sharedir = get_pg_config("--sharedir");
let sharedir = format!("{}/extension", sharedir);
let libdir = get_pg_config("--libdir");
let remote_storage = GenericRemoteStorage::from_config(config)?;
match ext_type {
ExtensionType::Shared => {
// 1. Download control files from s3-bucket/public/*.control to SHAREDIR/extension
// We can do this step even before we have spec,
// because public extensions are common for all projects.
let folder = RemotePath::new(Path::new("public_extensions"))?;
let from_paths = remote_storage.list_files(Some(&folder)).await?;
for remote_from_path in from_paths {
if remote_from_path.extension() == Some("control") {
// FIXME: CAUTION: if you run this, it will actually write stuff to my postgress directory
// but atm that stuff that it is going to write is not good.
// don't run atm without changing path
download_helper(&remote_storage, &remote_from_path, &sharedir)?;
}
}
}
ExtensionType::Tenant(tenant_id) => {
// 2. After we have spec, before project start
// Download control files from s3-bucket/[tenant-id]/*.control to SHAREDIR/extension
let folder = RemotePath::new(Path::new(format!("{tenant_id}")))?;
let from_paths = remote_storage.list_files(Some(&folder)).await?;
for remote_from_path in from_paths {
if remote_from_path.extension() == Some("control") {
download_helper(&remote_storage, &remote_from_path, &sharedir)?;
}
}
}
ExtensionType::Library(library_name) => {
// 3. After we have spec, before postgres start
// Download preload_shared_libraries from s3-bucket/public/[library-name].control into LIBDIR/
let from_path = format!("neon-dev-extensions/public/{library_name}.control");
let remote_from_path = RemotePath::new(Path::new(&from_path))?;
download_helper(&remote_storage, &remote_from_path, &libdir)?;
}
}
Ok(())
}
pub fn get_s3_config(arg_matches: &ArgMatches) -> anyhow::Result<RemoteStorageConfig> {
// TODO: Right now we are using the same config parameters as pageserver; but should we have our own configs?
// TODO: Should we read the s3_config from CLI arguments?
let cfg_file_path = Path::new("./../.neon/pageserver.toml");
let cfg_file_contents = std::fs::read_to_string(cfg_file_path)
.with_context(|| format!( "Failed to read pageserver config at '{}'", cfg_file_path.display()))?;
let toml = cfg_file_contents
.parse::<toml_edit::Document>()
.with_context(|| format!( "Failed to parse '{}' as pageserver config", cfg_file_path.display()))?;
let remote_storage_data = toml.get("remote_storage")
.context("field should be present")?;
let remote_storage_config = RemoteStorageConfig::from_toml(remote_storage_data)?
.context("error configuring remote storage")?;
Ok(remote_storage_config)
}

View File

@@ -13,4 +13,4 @@ pub mod monitor;
pub mod params; pub mod params;
pub mod pg_helpers; pub mod pg_helpers;
pub mod spec; pub mod spec;
pub mod sync_sk; pub mod extensions;

View File

@@ -18,7 +18,6 @@ pub fn init_tracing_and_logging(default_log_level: &str) -> anyhow::Result<()> {
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_log_level)); .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_log_level));
let fmt_layer = tracing_subscriber::fmt::layer() let fmt_layer = tracing_subscriber::fmt::layer()
.with_ansi(false)
.with_target(false) .with_target(false)
.with_writer(std::io::stderr); .with_writer(std::io::stderr);

View File

@@ -1,6 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use std::{thread, time}; use std::{thread, time};
use anyhow::Result;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use postgres::{Client, NoTls}; use postgres::{Client, NoTls};
use tracing::{debug, info}; use tracing::{debug, info};
@@ -104,11 +105,10 @@ fn watch_compute_activity(compute: &ComputeNode) {
} }
/// Launch a separate compute monitor thread and return its `JoinHandle`. /// Launch a separate compute monitor thread and return its `JoinHandle`.
pub fn launch_monitor(state: &Arc<ComputeNode>) -> thread::JoinHandle<()> { pub fn launch_monitor(state: &Arc<ComputeNode>) -> Result<thread::JoinHandle<()>> {
let state = Arc::clone(state); let state = Arc::clone(state);
thread::Builder::new() Ok(thread::Builder::new()
.name("compute-monitor".into()) .name("compute-monitor".into())
.spawn(move || watch_compute_activity(&state)) .spawn(move || watch_compute_activity(&state))?)
.expect("cannot launch compute monitor thread")
} }

View File

@@ -16,26 +16,15 @@ use compute_api::spec::{Database, GenericOption, GenericOptions, PgIdent, Role};
const POSTGRES_WAIT_TIMEOUT: Duration = Duration::from_millis(60 * 1000); // milliseconds const POSTGRES_WAIT_TIMEOUT: Duration = Duration::from_millis(60 * 1000); // milliseconds
/// Escape a string for including it in a SQL literal. Wrapping the result /// Escape a string for including it in a SQL literal
/// with `E'{}'` or `'{}'` is not required, as it returns a ready-to-use fn escape_literal(s: &str) -> String {
/// SQL string literal, e.g. `'db'''` or `E'db\\'`. s.replace('\'', "''").replace('\\', "\\\\")
/// See <https://github.com/postgres/postgres/blob/da98d005cdbcd45af563d0c4ac86d0e9772cd15f/src/backend/utils/adt/quote.c#L47>
/// for the original implementation.
pub fn escape_literal(s: &str) -> String {
let res = s.replace('\'', "''").replace('\\', "\\\\");
if res.contains('\\') {
format!("E'{}'", res)
} else {
format!("'{}'", res)
}
} }
/// Escape a string so that it can be used in postgresql.conf. Wrapping the result /// Escape a string so that it can be used in postgresql.conf.
/// with `'{}'` is not required, as it returns a ready-to-use config string. /// Same as escape_literal, currently.
pub fn escape_conf_value(s: &str) -> String { pub fn escape_conf_value(s: &str) -> String {
let res = s.replace('\'', "''").replace('\\', "\\\\"); s.replace('\'', "''").replace('\\', "\\\\")
format!("'{}'", res)
} }
trait GenericOptionExt { trait GenericOptionExt {
@@ -48,7 +37,7 @@ impl GenericOptionExt for GenericOption {
fn to_pg_option(&self) -> String { fn to_pg_option(&self) -> String {
if let Some(val) = &self.value { if let Some(val) = &self.value {
match self.vartype.as_ref() { match self.vartype.as_ref() {
"string" => format!("{} {}", self.name, escape_literal(val)), "string" => format!("{} '{}'", self.name, escape_literal(val)),
_ => format!("{} {}", self.name, val), _ => format!("{} {}", self.name, val),
} }
} else { } else {
@@ -60,7 +49,7 @@ impl GenericOptionExt for GenericOption {
fn to_pg_setting(&self) -> String { fn to_pg_setting(&self) -> String {
if let Some(val) = &self.value { if let Some(val) = &self.value {
match self.vartype.as_ref() { match self.vartype.as_ref() {
"string" => format!("{} = {}", self.name, escape_conf_value(val)), "string" => format!("{} = '{}'", self.name, escape_conf_value(val)),
_ => format!("{} = {}", self.name, val), _ => format!("{} = {}", self.name, val),
} }
} else { } else {
@@ -226,7 +215,7 @@ pub fn get_existing_dbs(client: &mut Client) -> Result<Vec<Database>> {
/// Wait for Postgres to become ready to accept connections. It's ready to /// Wait for Postgres to become ready to accept connections. It's ready to
/// accept connections when the state-field in `pgdata/postmaster.pid` says /// accept connections when the state-field in `pgdata/postmaster.pid` says
/// 'ready'. /// 'ready'.
#[instrument(skip_all, fields(pgdata = %pgdata.display()))] #[instrument(skip(pg))]
pub fn wait_for_postgres(pg: &mut Child, pgdata: &Path) -> Result<()> { pub fn wait_for_postgres(pg: &mut Child, pgdata: &Path) -> Result<()> {
let pid_path = pgdata.join("postmaster.pid"); let pid_path = pgdata.join("postmaster.pid");

View File

@@ -269,13 +269,17 @@ pub fn handle_roles(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
xact.execute(query.as_str(), &[])?; xact.execute(query.as_str(), &[])?;
} }
RoleAction::Create => { RoleAction::Create => {
let mut query: String = format!( let mut query: String = format!("CREATE ROLE {} ", name.pg_quote());
"CREATE ROLE {} CREATEROLE CREATEDB IN ROLE neon_superuser",
name.pg_quote()
);
info!("role create query: '{}'", &query); info!("role create query: '{}'", &query);
query.push_str(&role.to_pg_options()); query.push_str(&role.to_pg_options());
xact.execute(query.as_str(), &[])?; xact.execute(query.as_str(), &[])?;
let grant_query = format!(
"GRANT pg_read_all_data, pg_write_all_data TO {}",
name.pg_quote()
);
xact.execute(grant_query.as_str(), &[])?;
info!("role grant query: '{}'", &grant_query);
} }
} }
@@ -397,44 +401,10 @@ pub fn handle_databases(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
// We do not check either DB exists or not, // We do not check either DB exists or not,
// Postgres will take care of it for us // Postgres will take care of it for us
"delete_db" => { "delete_db" => {
// In Postgres we can't drop a database if it is a template. let query: String = format!("DROP DATABASE IF EXISTS {}", &op.name.pg_quote());
// So we need to unset the template flag first, but it could
// be a retry, so we could've already dropped the database.
// Check that database exists first to make it idempotent.
let unset_template_query: String = format!(
"
DO $$
BEGIN
IF EXISTS(
SELECT 1
FROM pg_catalog.pg_database
WHERE datname = {}
)
THEN
ALTER DATABASE {} is_template false;
END IF;
END
$$;",
escape_literal(&op.name),
&op.name.pg_quote()
);
// Use FORCE to drop database even if there are active connections.
// We run this from `cloud_admin`, so it should have enough privileges.
// NB: there could be other db states, which prevent us from dropping
// the database. For example, if db is used by any active subscription
// or replication slot.
// TODO: deal with it once we allow logical replication. Proper fix should
// involve returning an error code to the control plane, so it could
// figure out that this is a non-retryable error, return it to the user
// and fail operation permanently.
let drop_db_query: String = format!(
"DROP DATABASE IF EXISTS {} WITH (FORCE)",
&op.name.pg_quote()
);
warn!("deleting database '{}'", &op.name); warn!("deleting database '{}'", &op.name);
client.execute(unset_template_query.as_str(), &[])?; client.execute(query.as_str(), &[])?;
client.execute(drop_db_query.as_str(), &[])?;
} }
"rename_db" => { "rename_db" => {
let new_name = op.new_name.as_ref().unwrap(); let new_name = op.new_name.as_ref().unwrap();
@@ -506,11 +476,6 @@ pub fn handle_databases(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
query.push_str(&db.to_pg_options()); query.push_str(&db.to_pg_options());
let _guard = info_span!("executing", query).entered(); let _guard = info_span!("executing", query).entered();
client.execute(query.as_str(), &[])?; client.execute(query.as_str(), &[])?;
let grant_query: String = format!(
"GRANT ALL PRIVILEGES ON DATABASE {} TO neon_superuser",
name.pg_quote()
);
client.execute(grant_query.as_str(), &[])?;
} }
}; };
@@ -530,9 +495,35 @@ pub fn handle_databases(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
/// Grant CREATE ON DATABASE to the database owner and do some other alters and grants /// Grant CREATE ON DATABASE to the database owner and do some other alters and grants
/// to allow users creating trusted extensions and re-creating `public` schema, for example. /// to allow users creating trusted extensions and re-creating `public` schema, for example.
#[instrument(skip_all)] #[instrument(skip_all)]
pub fn handle_grants(spec: &ComputeSpec, connstr: &str) -> Result<()> { pub fn handle_grants(spec: &ComputeSpec, connstr: &str, client: &mut Client) -> Result<()> {
info!("cluster spec grants:"); info!("cluster spec grants:");
// We now have a separate `web_access` role to connect to the database
// via the web interface and proxy link auth. And also we grant a
// read / write all data privilege to every role. So also grant
// create to everyone.
// XXX: later we should stop messing with Postgres ACL in such horrible
// ways.
let roles = spec
.cluster
.roles
.iter()
.map(|r| r.name.pg_quote())
.collect::<Vec<_>>();
for db in &spec.cluster.databases {
let dbname = &db.name;
let query: String = format!(
"GRANT CREATE ON DATABASE {} TO {}",
dbname.pg_quote(),
roles.join(", ")
);
info!("grant query {}", &query);
client.execute(query.as_str(), &[])?;
}
// Do some per-database access adjustments. We'd better do this at db creation time, // Do some per-database access adjustments. We'd better do this at db creation time,
// but CREATE DATABASE isn't transactional. So we cannot create db + do some grants // but CREATE DATABASE isn't transactional. So we cannot create db + do some grants
// atomically. // atomically.

View File

@@ -1,98 +0,0 @@
// Utils for running sync_safekeepers
use anyhow::Result;
use tracing::info;
use utils::lsn::Lsn;
#[derive(Copy, Clone, Debug)]
pub enum TimelineStatusResponse {
NotFound,
Ok(TimelineStatusOkResponse),
}
#[derive(Copy, Clone, Debug)]
pub struct TimelineStatusOkResponse {
flush_lsn: Lsn,
commit_lsn: Lsn,
}
/// Get a safekeeper's metadata for our timeline. The id is only used for logging
pub async fn ping_safekeeper(
id: String,
config: tokio_postgres::Config,
) -> Result<TimelineStatusResponse> {
// TODO add retries
// Connect
info!("connecting to {}", id);
let (client, conn) = config.connect(tokio_postgres::NoTls).await?;
tokio::spawn(async move {
if let Err(e) = conn.await {
eprintln!("connection error: {}", e);
}
});
// Query
info!("querying {}", id);
let result = client.simple_query("TIMELINE_STATUS").await?;
// Parse result
info!("done with {}", id);
if let postgres::SimpleQueryMessage::Row(row) = &result[0] {
use std::str::FromStr;
let response = TimelineStatusResponse::Ok(TimelineStatusOkResponse {
flush_lsn: Lsn::from_str(row.get("flush_lsn").unwrap())?,
commit_lsn: Lsn::from_str(row.get("commit_lsn").unwrap())?,
});
Ok(response)
} else {
// Timeline doesn't exist
Ok(TimelineStatusResponse::NotFound)
}
}
/// Given a quorum of responses, check if safekeepers are synced at some Lsn
pub fn check_if_synced(responses: Vec<TimelineStatusResponse>) -> Option<Lsn> {
// Check if all responses are ok
let ok_responses: Vec<TimelineStatusOkResponse> = responses
.iter()
.filter_map(|r| match r {
TimelineStatusResponse::Ok(ok_response) => Some(ok_response),
_ => None,
})
.cloned()
.collect();
if ok_responses.len() < responses.len() {
info!(
"not synced. Only {} out of {} know about this timeline",
ok_responses.len(),
responses.len()
);
return None;
}
// Get the min and the max of everything
let commit: Vec<Lsn> = ok_responses.iter().map(|r| r.commit_lsn).collect();
let flush: Vec<Lsn> = ok_responses.iter().map(|r| r.flush_lsn).collect();
let commit_max = commit.iter().max().unwrap();
let commit_min = commit.iter().min().unwrap();
let flush_max = flush.iter().max().unwrap();
let flush_min = flush.iter().min().unwrap();
// Check that all values are equal
if commit_min != commit_max {
info!("not synced. {:?} {:?}", commit_min, commit_max);
return None;
}
if flush_min != flush_max {
info!("not synced. {:?} {:?}", flush_min, flush_max);
return None;
}
// Check that commit == flush
if commit_max != flush_max {
info!("not synced. {:?} {:?}", commit_max, flush_max);
return None;
}
Some(*commit_max)
}

View File

@@ -89,12 +89,4 @@ test.escaping = 'here''s a backslash \\ and a quote '' and a double-quote " hoor
assert_eq!(none_generic_options.find("missed_value"), None); assert_eq!(none_generic_options.find("missed_value"), None);
assert_eq!(none_generic_options.find("invalid_value"), None); assert_eq!(none_generic_options.find("invalid_value"), None);
} }
#[test]
fn test_escape_literal() {
assert_eq!(escape_literal("test"), "'test'");
assert_eq!(escape_literal("test'"), "'test'''");
assert_eq!(escape_literal("test\\'"), "E'test\\\\'''");
assert_eq!(escape_literal("test\\'\\'"), "E'test\\\\''\\\\'''");
}
} }

View File

@@ -10,7 +10,7 @@
//! (non-Neon binaries don't necessarily follow our pidfile conventions). //! (non-Neon binaries don't necessarily follow our pidfile conventions).
//! The pid stored in the file is later used to stop the service. //! The pid stored in the file is later used to stop the service.
//! //!
//! See the [`lock_file`](utils::lock_file) module for more info. //! See [`lock_file`] module for more info.
use std::ffi::OsStr; use std::ffi::OsStr;
use std::io::Write; use std::io::Write;
@@ -180,11 +180,6 @@ pub fn stop_process(immediate: bool, process_name: &str, pid_file: &Path) -> any
} }
// Wait until process is gone // Wait until process is gone
wait_until_stopped(process_name, pid)?;
Ok(())
}
pub fn wait_until_stopped(process_name: &str, pid: Pid) -> anyhow::Result<()> {
for retries in 0..RETRIES { for retries in 0..RETRIES {
match process_has_stopped(pid) { match process_has_stopped(pid) {
Ok(true) => { Ok(true) => {

View File

@@ -308,8 +308,7 @@ fn handle_init(init_match: &ArgMatches) -> anyhow::Result<LocalEnv> {
let mut env = let mut env =
LocalEnv::parse_config(&toml_file).context("Failed to create neon configuration")?; LocalEnv::parse_config(&toml_file).context("Failed to create neon configuration")?;
let force = init_match.get_flag("force"); env.init(pg_version)
env.init(pg_version, force)
.context("Failed to initialize neon repository")?; .context("Failed to initialize neon repository")?;
// Initialize pageserver, create initial tenant and timeline. // Initialize pageserver, create initial tenant and timeline.
@@ -1014,13 +1013,6 @@ fn cli() -> Command {
.help("If set, the node will be a hot replica on the specified timeline") .help("If set, the node will be a hot replica on the specified timeline")
.required(false); .required(false);
let force_arg = Arg::new("force")
.value_parser(value_parser!(bool))
.long("force")
.action(ArgAction::SetTrue)
.help("Force initialization even if the repository is not empty")
.required(false);
Command::new("Neon CLI") Command::new("Neon CLI")
.arg_required_else_help(true) .arg_required_else_help(true)
.version(GIT_VERSION) .version(GIT_VERSION)
@@ -1036,7 +1028,6 @@ fn cli() -> Command {
.value_name("config"), .value_name("config"),
) )
.arg(pg_version_arg.clone()) .arg(pg_version_arg.clone())
.arg(force_arg)
) )
.subcommand( .subcommand(
Command::new("timeline") Command::new("timeline")

View File

@@ -2,9 +2,8 @@
//! //!
//! In the local test environment, the data for each safekeeper is stored in //! In the local test environment, the data for each safekeeper is stored in
//! //!
//! ```text
//! .neon/safekeepers/<safekeeper id> //! .neon/safekeepers/<safekeeper id>
//! ``` //!
use anyhow::Context; use anyhow::Context;
use std::path::PathBuf; use std::path::PathBuf;

View File

@@ -2,9 +2,7 @@
//! //!
//! In the local test environment, the data for each endpoint is stored in //! In the local test environment, the data for each endpoint is stored in
//! //!
//! ```text
//! .neon/endpoints/<endpoint id> //! .neon/endpoints/<endpoint id>
//! ```
//! //!
//! Some basic information about the endpoint, like the tenant and timeline IDs, //! Some basic information about the endpoint, like the tenant and timeline IDs,
//! are stored in the `endpoint.json` file. The `endpoint.json` file is created //! are stored in the `endpoint.json` file. The `endpoint.json` file is created
@@ -24,7 +22,7 @@
//! //!
//! Directory contents: //! Directory contents:
//! //!
//! ```text //! ```ignore
//! .neon/endpoints/main/ //! .neon/endpoints/main/
//! compute.log - log output of `compute_ctl` and `postgres` //! compute.log - log output of `compute_ctl` and `postgres`
//! endpoint.json - serialized `EndpointConf` struct //! endpoint.json - serialized `EndpointConf` struct
@@ -69,7 +67,6 @@ pub struct EndpointConf {
pg_port: u16, pg_port: u16,
http_port: u16, http_port: u16,
pg_version: u32, pg_version: u32,
skip_pg_catalog_updates: bool,
} }
// //
@@ -138,7 +135,6 @@ impl ComputeControlPlane {
mode, mode,
tenant_id, tenant_id,
pg_version, pg_version,
skip_pg_catalog_updates: false,
}); });
ep.create_endpoint_dir()?; ep.create_endpoint_dir()?;
@@ -152,7 +148,6 @@ impl ComputeControlPlane {
http_port, http_port,
pg_port, pg_port,
pg_version, pg_version,
skip_pg_catalog_updates: false,
})?, })?,
)?; )?;
std::fs::write( std::fs::write(
@@ -188,9 +183,6 @@ pub struct Endpoint {
// the endpoint runs in. // the endpoint runs in.
pub env: LocalEnv, pub env: LocalEnv,
pageserver: Arc<PageServerNode>, pageserver: Arc<PageServerNode>,
// Optimizations
skip_pg_catalog_updates: bool,
} }
impl Endpoint { impl Endpoint {
@@ -224,7 +216,6 @@ impl Endpoint {
mode: conf.mode, mode: conf.mode,
tenant_id: conf.tenant_id, tenant_id: conf.tenant_id,
pg_version: conf.pg_version, pg_version: conf.pg_version,
skip_pg_catalog_updates: conf.skip_pg_catalog_updates,
}) })
} }
@@ -289,7 +280,7 @@ impl Endpoint {
.env .env
.safekeepers .safekeepers
.iter() .iter()
.map(|sk| format!("localhost:{}", sk.get_compute_port())) .map(|sk| format!("localhost:{}", sk.pg_port))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(","); .join(",");
conf.append("neon.safekeepers", &safekeepers); conf.append("neon.safekeepers", &safekeepers);
@@ -318,7 +309,7 @@ impl Endpoint {
.env .env
.safekeepers .safekeepers
.iter() .iter()
.map(|x| x.get_compute_port().to_string()) .map(|x| x.pg_port.to_string())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(","); .join(",");
let sk_hosts = vec!["localhost"; self.env.safekeepers.len()].join(","); let sk_hosts = vec!["localhost"; self.env.safekeepers.len()].join(",");
@@ -407,16 +398,6 @@ impl Endpoint {
String::from_utf8_lossy(&pg_ctl.stderr), String::from_utf8_lossy(&pg_ctl.stderr),
); );
} }
// Also wait for the compute_ctl process to die. It might have some cleanup
// work to do after postgres stops, like syncing safekeepers, etc.
//
// TODO use background_process::stop_process instead
let pidfile_path = self.endpoint_path().join("compute_ctl.pid");
let pid: u32 = std::fs::read_to_string(pidfile_path)?.parse()?;
let pid = nix::unistd::Pid::from_raw(pid as i32);
crate::background_process::wait_until_stopped("compute_ctl", pid)?;
Ok(()) Ok(())
} }
@@ -463,13 +444,12 @@ impl Endpoint {
.iter() .iter()
.find(|node| node.id == sk_id) .find(|node| node.id == sk_id)
.ok_or_else(|| anyhow!("safekeeper {sk_id} does not exist"))?; .ok_or_else(|| anyhow!("safekeeper {sk_id} does not exist"))?;
safekeeper_connstrings.push(format!("127.0.0.1:{}", sk.get_compute_port())); safekeeper_connstrings.push(format!("127.0.0.1:{}", sk.pg_port));
} }
} }
// Create spec file // Create spec file
let spec = ComputeSpec { let spec = ComputeSpec {
skip_pg_catalog_updates: self.skip_pg_catalog_updates,
format_version: 1.0, format_version: 1.0,
operation_uuid: None, operation_uuid: None,
cluster: Cluster { cluster: Cluster {
@@ -519,13 +499,7 @@ impl Endpoint {
.stdin(std::process::Stdio::null()) .stdin(std::process::Stdio::null())
.stderr(logfile.try_clone()?) .stderr(logfile.try_clone()?)
.stdout(logfile); .stdout(logfile);
let child = cmd.spawn()?; let _child = cmd.spawn()?;
// Write down the pid so we can wait for it when we want to stop
// TODO use background_process::start_process instead
let pid = child.id();
let pidfile_path = self.endpoint_path().join("compute_ctl.pid");
std::fs::write(pidfile_path, pid.to_string())?;
// Wait for it to start // Wait for it to start
let mut attempt = 0; let mut attempt = 0;
@@ -564,7 +538,9 @@ impl Endpoint {
} }
Err(e) => { Err(e) => {
if attempt == MAX_ATTEMPTS { if attempt == MAX_ATTEMPTS {
return Err(e).context("timed out waiting to connect to compute_ctl HTTP"); return Err(e).context(
"timed out waiting to connect to compute_ctl HTTP; last error: {e}",
);
} }
} }
} }

View File

@@ -137,7 +137,6 @@ impl Default for PageServerConf {
pub struct SafekeeperConf { pub struct SafekeeperConf {
pub id: NodeId, pub id: NodeId,
pub pg_port: u16, pub pg_port: u16,
pub pg_tenant_only_port: Option<u16>,
pub http_port: u16, pub http_port: u16,
pub sync: bool, pub sync: bool,
pub remote_storage: Option<String>, pub remote_storage: Option<String>,
@@ -150,7 +149,6 @@ impl Default for SafekeeperConf {
Self { Self {
id: NodeId(0), id: NodeId(0),
pg_port: 0, pg_port: 0,
pg_tenant_only_port: None,
http_port: 0, http_port: 0,
sync: true, sync: true,
remote_storage: None, remote_storage: None,
@@ -160,14 +158,6 @@ impl Default for SafekeeperConf {
} }
} }
impl SafekeeperConf {
/// Compute is served by port on which only tenant scoped tokens allowed, if
/// it is configured.
pub fn get_compute_port(&self) -> u16 {
self.pg_tenant_only_port.unwrap_or(self.pg_port)
}
}
impl LocalEnv { impl LocalEnv {
pub fn pg_distrib_dir_raw(&self) -> PathBuf { pub fn pg_distrib_dir_raw(&self) -> PathBuf {
self.pg_distrib_dir.clone() self.pg_distrib_dir.clone()
@@ -374,7 +364,7 @@ impl LocalEnv {
// //
// Initialize a new Neon repository // Initialize a new Neon repository
// //
pub fn init(&mut self, pg_version: u32, force: bool) -> anyhow::Result<()> { pub fn init(&mut self, pg_version: u32) -> anyhow::Result<()> {
// check if config already exists // check if config already exists
let base_path = &self.base_data_dir; let base_path = &self.base_data_dir;
ensure!( ensure!(
@@ -382,29 +372,11 @@ impl LocalEnv {
"repository base path is missing" "repository base path is missing"
); );
if base_path.exists() { ensure!(
if force { !base_path.exists(),
println!("removing all contents of '{}'", base_path.display()); "directory '{}' already exists. Perhaps already initialized?",
// instead of directly calling `remove_dir_all`, we keep the original dir but removing base_path.display()
// all contents inside. This helps if the developer symbol links another directory (i.e., );
// S3 local SSD) to the `.neon` base directory.
for entry in std::fs::read_dir(base_path)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
fs::remove_dir_all(&path)?;
} else {
fs::remove_file(&path)?;
}
}
} else {
bail!(
"directory '{}' already exists. Perhaps already initialized? (Hint: use --force to remove all contents)",
base_path.display()
);
}
}
if !self.pg_bin_dir(pg_version)?.join("postgres").exists() { if !self.pg_bin_dir(pg_version)?.join("postgres").exists() {
bail!( bail!(
"Can't find postgres binary at {}", "Can't find postgres binary at {}",
@@ -420,9 +392,7 @@ impl LocalEnv {
} }
} }
if !base_path.exists() { fs::create_dir(base_path)?;
fs::create_dir(base_path)?;
}
// Generate keypair for JWT. // Generate keypair for JWT.
// //

View File

@@ -2,9 +2,8 @@
//! //!
//! In the local test environment, the data for each safekeeper is stored in //! In the local test environment, the data for each safekeeper is stored in
//! //!
//! ```text
//! .neon/safekeepers/<safekeeper id> //! .neon/safekeepers/<safekeeper id>
//! ``` //!
use std::io::Write; use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Child; use std::process::Child;
@@ -120,55 +119,45 @@ impl SafekeeperNode {
let availability_zone = format!("sk-{}", id_string); let availability_zone = format!("sk-{}", id_string);
let mut args = vec![ let mut args = vec![
"-D".to_owned(), "-D",
datadir datadir.to_str().with_context(|| {
.to_str() format!("Datadir path {datadir:?} cannot be represented as a unicode string")
.with_context(|| { })?,
format!("Datadir path {datadir:?} cannot be represented as a unicode string") "--id",
})? &id_string,
.to_owned(), "--listen-pg",
"--id".to_owned(), &listen_pg,
id_string, "--listen-http",
"--listen-pg".to_owned(), &listen_http,
listen_pg, "--availability-zone",
"--listen-http".to_owned(), &availability_zone,
listen_http,
"--availability-zone".to_owned(),
availability_zone,
]; ];
if let Some(pg_tenant_only_port) = self.conf.pg_tenant_only_port {
let listen_pg_tenant_only = format!("127.0.0.1:{}", pg_tenant_only_port);
args.extend(["--listen-pg-tenant-only".to_owned(), listen_pg_tenant_only]);
}
if !self.conf.sync { if !self.conf.sync {
args.push("--no-sync".to_owned()); args.push("--no-sync");
} }
let broker_endpoint = format!("{}", self.env.broker.client_url()); let broker_endpoint = format!("{}", self.env.broker.client_url());
args.extend(["--broker-endpoint".to_owned(), broker_endpoint]); args.extend(["--broker-endpoint", &broker_endpoint]);
let mut backup_threads = String::new(); let mut backup_threads = String::new();
if let Some(threads) = self.conf.backup_threads { if let Some(threads) = self.conf.backup_threads {
backup_threads = threads.to_string(); backup_threads = threads.to_string();
args.extend(["--backup-threads".to_owned(), backup_threads]); args.extend(["--backup-threads", &backup_threads]);
} else { } else {
drop(backup_threads); drop(backup_threads);
} }
if let Some(ref remote_storage) = self.conf.remote_storage { if let Some(ref remote_storage) = self.conf.remote_storage {
args.extend(["--remote-storage".to_owned(), remote_storage.clone()]); args.extend(["--remote-storage", remote_storage]);
} }
let key_path = self.env.base_data_dir.join("auth_public_key.pem"); let key_path = self.env.base_data_dir.join("auth_public_key.pem");
if self.conf.auth_enabled { if self.conf.auth_enabled {
args.extend([ args.extend([
"--auth-validation-public-key-path".to_owned(), "--auth-validation-public-key-path",
key_path key_path.to_str().with_context(|| {
.to_str() format!("Key path {key_path:?} cannot be represented as a unicode string")
.with_context(|| { })?,
format!("Key path {key_path:?} cannot be represented as a unicode string")
})?
.to_owned(),
]); ]);
} }

View File

@@ -189,7 +189,7 @@ services:
- "/bin/bash" - "/bin/bash"
- "-c" - "-c"
command: command:
- "until pg_isready -h compute -p 55433 -U cloud_admin ; do - "until pg_isready -h compute -p 55433 ; do
echo 'Waiting to start compute...' && sleep 1; echo 'Waiting to start compute...' && sleep 1;
done" done"
depends_on: depends_on:

View File

@@ -48,7 +48,6 @@ Creating docker-compose_storage_broker_1 ... done
2. connect compute node 2. connect compute node
``` ```
$ echo "localhost:55433:postgres:cloud_admin:cloud_admin" >> ~/.pgpass $ echo "localhost:55433:postgres:cloud_admin:cloud_admin" >> ~/.pgpass
$ chmod 600 ~/.pgpass
$ psql -h localhost -p 55433 -U cloud_admin $ psql -h localhost -p 55433 -U cloud_admin
postgres=# CREATE TABLE t(key int primary key, value text); postgres=# CREATE TABLE t(key int primary key, value text);
CREATE TABLE CREATE TABLE

View File

@@ -30,8 +30,8 @@ or similar, to wake up on shutdown.
In async Rust, futures can be "cancelled" at any await point, by In async Rust, futures can be "cancelled" at any await point, by
dropping the Future. For example, `tokio::select!` returns as soon as dropping the Future. For example, `tokio::select!` returns as soon as
one of the Futures returns, and drops the others. `tokio::time::timeout` one of the Futures returns, and drops the others. `tokio::timeout!` is
is another example. In the Rust ecosystem, some functions are another example. In the Rust ecosystem, some functions are
cancellation-safe, meaning they can be safely dropped without cancellation-safe, meaning they can be safely dropped without
side-effects, while others are not. See documentation of side-effects, while others are not. See documentation of
`tokio::select!` for examples. `tokio::select!` for examples.
@@ -42,9 +42,9 @@ function that you call cannot be assumed to be async
cancellation-safe, and must be polled to completion. cancellation-safe, and must be polled to completion.
The downside of non-cancellation safe code is that you have to be very The downside of non-cancellation safe code is that you have to be very
careful when using `tokio::select!`, `tokio::time::timeout`, and other careful when using `tokio::select!`, `tokio::timeout!`, and other such
such functions that can cause a Future to be dropped. They can only be functions that can cause a Future to be dropped. They can only be used
used with functions that are explicitly documented to be cancellation-safe, with functions that are explicitly documented to be cancellation-safe,
or you need to spawn a separate task to shield from the cancellation. or you need to spawn a separate task to shield from the cancellation.
At the entry points to the code, we also take care to poll futures to At the entry points to the code, we also take care to poll futures to

View File

@@ -1,84 +0,0 @@
# Postgres user and database management
(This supersedes the previous proposal that looked too complicated and desynchronization-prone)
We've accumulated a bunch of problems with our approach to role and database management, namely:
1. we don't allow role and database creation from Postgres, and users are complaining about that
2. fine-grained role management is not possible both from Postgres and console
Right now, we do store users and databases both in console and Postgres, and there are two main reasons for
that:
* we want to be able to authenticate users in proxy against the console without Postgres' involvement. Otherwise,
malicious brute force attempts will wake up Postgres (expensive) and may exhaust the Postgres connections limit (deny of service).
* it is handy when we can render console UI without waking up compute (e.g., show database list)
This RFC doesn't talk about giving root access to the database, which is blocked by a secure runtime setup.
## Overview
* Add Postgres extension that sends an HTTP request each time transaction that modifies users/databases is about to commit.
* Add user management API to internal console API. Also, the console should put a JWT token into the compute so that it can access management API.
## Postgres behavior
The default user role (@username) should have `CREATE ROLE`, `CREATE DB`, and `BYPASSRLS` privileges. We expose the Postgres port
to the open internet, so we need to check password strength. Now console generates strong passwords, so there is no risk of having dumb passwords. With user-provided passwords, such risks exist.
Since we store passwords in the console we should also send unencrypted password when role is created/changed. Hence communication with the console must be encrypted. Postgres also supports creating roles using hashes, in that case, we will not be able to get a raw password. So I can see the following options here:
* roles created via SQL will *not* have raw passwords in the console
* roles created via SQL will have raw passwords in the console, except ones that were created using hashes
I'm leaning towards the second option here as it is a bit more consistent one -- if raw password storage is enabled then we store passwords in all cases where we can store them.
To send data about roles and databases from Postgres to the console we can create the following Postgres extension:
* Intercept role/database changes in `ProcessUtility_hook`. Here we have access to the query statement with the raw password. The hook handler itself should not dial the console immediately and rather stash info in some hashmap for later use.
* When the transaction is about to commit we execute collected role modifications (all as one -- console should either accept all or reject all, and hence API shouldn't be REST-like). If the console request fails we can roll back the transaction. This way if the transaction is committed we know for sure that console has this information. We can use `XACT_EVENT_PRE_COMMIT` and `XACT_EVENT_PARALLEL_PRE_COMMIT` for that.
* Extension should be mindful of the fact that it is possible to create and delete roles within the transaction.
* We also need to track who is database owner, some coding around may be needed to get the current user when the database is created.
## Console user management API
The current public API has REST API for role management. We need to have some analog for the internal API (called mgmt API in the console code). But unlike public API here we want to have an atomic way to create several roles/databases (in cases when several roles were created in the same transaction). So something like that may work:
```
curl -X PATCH /api/v1/roles_and_databases -d '
[
{"op":"create", "type":"role", "name": "kurt", "password":"lYgT3BlbkFJ2vBZrqv"},
{"op":"drop", "type":"role", "name": "trout"},
{"op":"alter", "type":"role", "name": "kilgore", "password":"3BlbkFJ2vB"},
{"op":"create", "type":"database", "name": "db2", "owner": "eliot"},
]
'
```
Makes sense not to error out on duplicated create/delete operations (see failure modes)
## Managing users from the console
Now console puts a spec file with the list of databases/roles and delta operations in all the compute pods. `compute_ctl` then picks up that file and stubbornly executes deltas and checks data in the spec file is the same as in the Postgres. This way if the user creates a role in the UI we restart compute with a new spec file and during the start databases/roles are created. So if Postgres send an HTTP call each time role is created we need to break recursion in that case. We can do that based on application_name or some GUC or user (local == no HTTP hook).
Generally, we have several options when we are creating users via console:
1. restart compute with a new spec file, execute local SQL command; cut recursion in the extension
2. "push" spec files into running compute, execute local SQL command; cut recursion in the extension
3. "push" spec files into running compute, execute local SQL command; let extension create those roles in the console
4. avoid managing roles via spec files, send SQL commands to compute; let extension create those roles in the console
The last option is the most straightforward one, but with the raw password storage opt-out, we will not have the password to establish an SQL connection. Also, we need a spec for provisioning purposes and to address potential desync (but that is quite unlikely). So I think the easiest approach would be:
1. keep role management like it is now and cut the recursion in the extension when SQL is executed by compute_ctl
2. add "push" endpoint to the compute_ctl to avoid compute restart during the `apply_config` operation -- that can be done as a follow up to avoid increasing scope too much
## Failure modes
* during role creation via SQL role was created in the console but the connection was dropped before Postgres got acknowledgment or some error happened after acknowledgment (out of disk space, deadlock, etc):
in that case, Postgres won't have a role that exists in the console. Compute restart will heal it (due to the spec file). Also if the console allows repeated creation/deletion user can repeat the transaction.
# Scalability
On my laptop, I can create 4200 roles per second. That corresponds to 363 million roles per day. Since each role creation ends up in the console database we can add some limit to the number of roles (could be reasonably big to not run into it often -- like 1k or 10k).

View File

@@ -1,22 +0,0 @@
# Useful development tools
This readme contains some hints on how to set up some optional development tools.
## ccls
[ccls](https://github.com/MaskRay/ccls) is a c/c++ language server. It requires some setup
to work well. There are different ways to do it but here's what works for me:
1. Make a common parent directory for all your common neon projects. (for example, `~/src/neondatabase/`)
2. Go to `vendor/postgres-v15`
3. Run `make clean && ./configure`
4. Install [bear](https://github.com/rizsotto/Bear), and run `bear -- make -j4`
5. Copy the generated `compile_commands.json` to `~/src/neondatabase` (or equivalent)
6. Run `touch ~/src/neondatabase/.ccls-root` this will make the `compile_commands.json` file discoverable in all subdirectories
With this setup you will get decent lsp mileage inside the postgres repo, and also any postgres extensions that you put in `~/src/neondatabase/`, like `pg_embedding`, or inside `~/src/neondatabase/neon/pgxn` as well.
Some additional tips for various IDEs:
### Emacs
To improve performance: `(setq lsp-lens-enable nil)`

View File

@@ -0,0 +1,5 @@
this
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
lol

View File

@@ -0,0 +1,7 @@
this
ax mod p < p/2
its gonna be big!
coming soon at 4pm
lol

View File

@@ -0,0 +1,3 @@
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.

View File

@@ -70,10 +70,7 @@ where
pub struct ComputeMetrics { pub struct ComputeMetrics {
pub wait_for_spec_ms: u64, pub wait_for_spec_ms: u64,
pub sync_safekeepers_ms: u64, pub sync_safekeepers_ms: u64,
pub sync_sk_check_ms: u64,
pub basebackup_ms: u64, pub basebackup_ms: u64,
pub basebackup_bytes: u64,
pub start_postgres_ms: u64,
pub config_ms: u64, pub config_ms: u64,
pub total_startup_ms: u64, pub total_startup_ms: u64,
} }

View File

@@ -27,12 +27,6 @@ pub struct ComputeSpec {
pub cluster: Cluster, pub cluster: Cluster,
pub delta_operations: Option<Vec<DeltaOp>>, pub delta_operations: Option<Vec<DeltaOp>>,
/// An optinal hint that can be passed to speed up startup time if we know
/// that no pg catalog mutations (like role creation, database creation,
/// extension creation) need to be done on the actual database to start.
#[serde(default)] // Default false
pub skip_pg_catalog_updates: bool,
// Information needed to connect to the storage layer. // Information needed to connect to the storage layer.
// //
// `tenant_id`, `timeline_id` and `pageserver_connstring` are always needed. // `tenant_id`, `timeline_id` and `pageserver_connstring` are always needed.
@@ -148,14 +142,4 @@ mod tests {
let file = File::open("tests/cluster_spec.json").unwrap(); let file = File::open("tests/cluster_spec.json").unwrap();
let _spec: ComputeSpec = serde_json::from_reader(file).unwrap(); let _spec: ComputeSpec = serde_json::from_reader(file).unwrap();
} }
#[test]
fn parse_unknown_fields() {
// Forward compatibility test
let file = File::open("tests/cluster_spec.json").unwrap();
let mut json: serde_json::Value = serde_json::from_reader(file).unwrap();
let ob = json.as_object_mut().unwrap();
ob.insert("unknown_field_123123123".into(), "hello".into());
let _spec: ComputeSpec = serde_json::from_value(json).unwrap();
}
} }

View File

@@ -5,7 +5,7 @@ use chrono::{DateTime, Utc};
use rand::Rng; use rand::Rng;
use serde::Serialize; use serde::Serialize;
#[derive(Serialize, Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] #[derive(Serialize, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum EventType { pub enum EventType {
#[serde(rename = "absolute")] #[serde(rename = "absolute")]
@@ -17,32 +17,6 @@ pub enum EventType {
}, },
} }
impl EventType {
pub fn absolute_time(&self) -> Option<&DateTime<Utc>> {
use EventType::*;
match self {
Absolute { time } => Some(time),
_ => None,
}
}
pub fn incremental_timerange(&self) -> Option<std::ops::Range<&DateTime<Utc>>> {
// these can most likely be thought of as Range or RangeFull
use EventType::*;
match self {
Incremental {
start_time,
stop_time,
} => Some(start_time..stop_time),
_ => None,
}
}
pub fn is_incremental(&self) -> bool {
matches!(self, EventType::Incremental { .. })
}
}
#[derive(Serialize, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] #[derive(Serialize, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct Event<Extra> { pub struct Event<Extra> {
#[serde(flatten)] #[serde(flatten)]

View File

@@ -6,7 +6,6 @@ use once_cell::sync::Lazy;
use prometheus::core::{AtomicU64, Collector, GenericGauge, GenericGaugeVec}; use prometheus::core::{AtomicU64, Collector, GenericGauge, GenericGaugeVec};
pub use prometheus::opts; pub use prometheus::opts;
pub use prometheus::register; pub use prometheus::register;
pub use prometheus::Error;
pub use prometheus::{core, default_registry, proto}; pub use prometheus::{core, default_registry, proto};
pub use prometheus::{exponential_buckets, linear_buckets}; pub use prometheus::{exponential_buckets, linear_buckets};
pub use prometheus::{register_counter_vec, Counter, CounterVec}; pub use prometheus::{register_counter_vec, Counter, CounterVec};
@@ -24,7 +23,6 @@ use prometheus::{Registry, Result};
pub mod launch_timestamp; pub mod launch_timestamp;
mod wrappers; mod wrappers;
pub use wrappers::{CountedReader, CountedWriter}; pub use wrappers::{CountedReader, CountedWriter};
pub mod metric_vec_duration;
pub type UIntGauge = GenericGauge<AtomicU64>; pub type UIntGauge = GenericGauge<AtomicU64>;
pub type UIntGaugeVec = GenericGaugeVec<AtomicU64>; pub type UIntGaugeVec = GenericGaugeVec<AtomicU64>;

View File

@@ -1,23 +0,0 @@
//! Helpers for observing duration on `HistogramVec` / `CounterVec` / `GaugeVec` / `MetricVec<T>`.
use std::{future::Future, time::Instant};
pub trait DurationResultObserver {
fn observe_result<T, E>(&self, res: &Result<T, E>, duration: std::time::Duration);
}
pub async fn observe_async_block_duration_by_result<
T,
E,
F: Future<Output = Result<T, E>>,
O: DurationResultObserver,
>(
observer: &O,
block: F,
) -> Result<T, E> {
let start = Instant::now();
let result = block.await;
let duration = start.elapsed();
observer.observe_result(&result, duration);
result
}

View File

@@ -9,7 +9,6 @@ use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr}; use serde_with::{serde_as, DisplayFromStr};
use strum_macros; use strum_macros;
use utils::{ use utils::{
completion,
history_buffer::HistoryBufferWithDropCounter, history_buffer::HistoryBufferWithDropCounter,
id::{NodeId, TenantId, TimelineId}, id::{NodeId, TenantId, TimelineId},
lsn::Lsn, lsn::Lsn,
@@ -77,12 +76,7 @@ pub enum TenantState {
/// system is being shut down. /// system is being shut down.
/// ///
/// Transitions out of this state are possible through `set_broken()`. /// Transitions out of this state are possible through `set_broken()`.
Stopping { Stopping,
// Because of https://github.com/serde-rs/serde/issues/2105 this has to be a named field,
// otherwise it will not be skipped during deserialization
#[serde(skip)]
progress: completion::Barrier,
},
/// The 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. /// any operations.
/// ///
@@ -124,7 +118,7 @@ impl TenantState {
// Why is Stopping a Maybe case? Because, during pageserver shutdown, // Why is Stopping a Maybe case? Because, during pageserver shutdown,
// we set the Stopping state irrespective of whether the tenant // we set the Stopping state irrespective of whether the tenant
// has finished attaching or not. // has finished attaching or not.
Self::Stopping { .. } => Maybe, Self::Stopping => Maybe,
} }
} }
@@ -158,7 +152,7 @@ pub enum ActivatingFrom {
} }
/// A state of a timeline in pageserver's memory. /// A state of a timeline in pageserver's memory.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum TimelineState { pub enum TimelineState {
/// The timeline is recognized by the pageserver but is not yet operational. /// The timeline is recognized by the pageserver but is not yet operational.
/// In particular, the walreceiver connection loop is not running for this timeline. /// In particular, the walreceiver connection loop is not running for this timeline.
@@ -171,7 +165,7 @@ pub enum TimelineState {
/// It cannot transition back into any other state. /// It cannot transition back into any other state.
Stopping, Stopping,
/// The timeline is broken and not operational (previous states: Loading or Active). /// The timeline is broken and not operational (previous states: Loading or Active).
Broken { reason: String, backtrace: String }, Broken,
} }
#[serde_as] #[serde_as]
@@ -417,16 +411,12 @@ pub struct LayerResidenceEvent {
pub reason: LayerResidenceEventReason, pub reason: LayerResidenceEventReason,
} }
/// The reason for recording a given [`LayerResidenceEvent`]. /// The reason for recording a given [`ResidenceEvent`].
#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum LayerResidenceEventReason { pub enum LayerResidenceEventReason {
/// The layer map is being populated, e.g. during timeline load or attach. /// The layer map is being populated, e.g. during timeline load or attach.
/// This includes [`RemoteLayer`] objects created in [`reconcile_with_remote`]. /// This includes [`RemoteLayer`] objects created in [`reconcile_with_remote`].
/// We need to record such events because there is no persistent storage for the events. /// We need to record such events because there is no persistent storage for the events.
///
// https://github.com/rust-lang/rust/issues/74481
/// [`RemoteLayer`]: ../../tenant/storage_layer/struct.RemoteLayer.html
/// [`reconcile_with_remote`]: ../../tenant/struct.Timeline.html#method.reconcile_with_remote
LayerLoad, LayerLoad,
/// We just created the layer (e.g., freeze_and_flush or compaction). /// We just created the layer (e.g., freeze_and_flush or compaction).
/// Such layers are always [`LayerResidenceStatus::Resident`]. /// Such layers are always [`LayerResidenceStatus::Resident`].
@@ -934,13 +924,7 @@ mod tests {
"Activating", "Activating",
), ),
(line!(), TenantState::Active, "Active"), (line!(), TenantState::Active, "Active"),
( (line!(), TenantState::Stopping, "Stopping"),
line!(),
TenantState::Stopping {
progress: utils::completion::Barrier::default(),
},
"Stopping",
),
( (
line!(), line!(),
TenantState::Broken { TenantState::Broken {

View File

@@ -60,9 +60,8 @@ impl Ord for RelTag {
/// Display RelTag in the same format that's used in most PostgreSQL debug messages: /// Display RelTag in the same format that's used in most PostgreSQL debug messages:
/// ///
/// ```text
/// <spcnode>/<dbnode>/<relnode>[_fsm|_vm|_init] /// <spcnode>/<dbnode>/<relnode>[_fsm|_vm|_init]
/// ``` ///
impl fmt::Display for RelTag { impl fmt::Display for RelTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(forkname) = forknumber_to_name(self.forknum) { if let Some(forkname) = forknumber_to_name(self.forknum) {

View File

@@ -57,9 +57,9 @@ pub fn slru_may_delete_clogsegment(segpage: u32, cutoff_page: u32) -> bool {
// Multixact utils // Multixact utils
pub fn mx_offset_to_flags_offset(xid: MultiXactId) -> usize { pub fn mx_offset_to_flags_offset(xid: MultiXactId) -> usize {
((xid / pg_constants::MULTIXACT_MEMBERS_PER_MEMBERGROUP as u32) ((xid / pg_constants::MULTIXACT_MEMBERS_PER_MEMBERGROUP as u32) as u16
% pg_constants::MULTIXACT_MEMBERGROUPS_PER_PAGE as u32 % pg_constants::MULTIXACT_MEMBERGROUPS_PER_PAGE
* pg_constants::MULTIXACT_MEMBERGROUP_SIZE as u32) as usize * pg_constants::MULTIXACT_MEMBERGROUP_SIZE) as usize
} }
pub fn mx_offset_to_flags_bitshift(xid: MultiXactId) -> u16 { pub fn mx_offset_to_flags_bitshift(xid: MultiXactId) -> u16 {
@@ -81,41 +81,3 @@ fn mx_offset_to_member_page(xid: u32) -> u32 {
pub fn mx_offset_to_member_segment(xid: u32) -> i32 { pub fn mx_offset_to_member_segment(xid: u32) -> i32 {
(mx_offset_to_member_page(xid) / pg_constants::SLRU_PAGES_PER_SEGMENT) as i32 (mx_offset_to_member_page(xid) / pg_constants::SLRU_PAGES_PER_SEGMENT) as i32
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multixid_calc() {
// Check that the mx_offset_* functions produce the same values as the
// corresponding PostgreSQL C macros (MXOffsetTo*). These test values
// were generated by calling the PostgreSQL macros with a little C
// program.
assert_eq!(mx_offset_to_member_segment(0), 0);
assert_eq!(mx_offset_to_member_page(0), 0);
assert_eq!(mx_offset_to_flags_offset(0), 0);
assert_eq!(mx_offset_to_flags_bitshift(0), 0);
assert_eq!(mx_offset_to_member_offset(0), 4);
assert_eq!(mx_offset_to_member_segment(1), 0);
assert_eq!(mx_offset_to_member_page(1), 0);
assert_eq!(mx_offset_to_flags_offset(1), 0);
assert_eq!(mx_offset_to_flags_bitshift(1), 8);
assert_eq!(mx_offset_to_member_offset(1), 8);
assert_eq!(mx_offset_to_member_segment(123456789), 2358);
assert_eq!(mx_offset_to_member_page(123456789), 75462);
assert_eq!(mx_offset_to_flags_offset(123456789), 4780);
assert_eq!(mx_offset_to_flags_bitshift(123456789), 8);
assert_eq!(mx_offset_to_member_offset(123456789), 4788);
assert_eq!(mx_offset_to_member_segment(u32::MAX - 1), 82040);
assert_eq!(mx_offset_to_member_page(u32::MAX - 1), 2625285);
assert_eq!(mx_offset_to_flags_offset(u32::MAX - 1), 5160);
assert_eq!(mx_offset_to_flags_bitshift(u32::MAX - 1), 16);
assert_eq!(mx_offset_to_member_offset(u32::MAX - 1), 5172);
assert_eq!(mx_offset_to_member_segment(u32::MAX), 82040);
assert_eq!(mx_offset_to_member_page(u32::MAX), 2625285);
assert_eq!(mx_offset_to_flags_offset(u32::MAX), 5160);
assert_eq!(mx_offset_to_flags_bitshift(u32::MAX), 24);
assert_eq!(mx_offset_to_member_offset(u32::MAX), 5176);
}
}

View File

@@ -49,16 +49,14 @@ pub fn forknumber_to_name(forknum: u8) -> Option<&'static str> {
} }
} }
///
/// Parse a filename of a relation file. Returns (relfilenode, forknum, segno) tuple. /// Parse a filename of a relation file. Returns (relfilenode, forknum, segno) tuple.
/// ///
/// Formats: /// Formats:
///
/// ```text
/// <oid> /// <oid>
/// <oid>_<fork name> /// <oid>_<fork name>
/// <oid>.<segment number> /// <oid>.<segment number>
/// <oid>_<fork name>.<segment number> /// <oid>_<fork name>.<segment number>
/// ```
/// ///
/// See functions relpath() and _mdfd_segpath() in PostgreSQL sources. /// See functions relpath() and _mdfd_segpath() in PostgreSQL sources.
/// ///

View File

@@ -5,11 +5,11 @@
//! It is similar to what tokio_util::codec::Framed with appropriate codec //! It is similar to what tokio_util::codec::Framed with appropriate codec
//! provides, but `FramedReader` and `FramedWriter` read/write parts can be used //! provides, but `FramedReader` and `FramedWriter` read/write parts can be used
//! separately without using split from futures::stream::StreamExt (which //! separately without using split from futures::stream::StreamExt (which
//! allocates a [Box] in polling internally). tokio::io::split is used for splitting //! allocates box[1] in polling internally). tokio::io::split is used for splitting
//! instead. Plus we customize error messages more than a single type for all io //! instead. Plus we customize error messages more than a single type for all io
//! calls. //! calls.
//! //!
//! [Box]: https://docs.rs/futures-util/0.3.26/src/futures_util/lock/bilock.rs.html#107 //! [1] https://docs.rs/futures-util/0.3.26/src/futures_util/lock/bilock.rs.html#107
use bytes::{Buf, BytesMut}; use bytes::{Buf, BytesMut};
use std::{ use std::{
future::Future, future::Future,
@@ -117,7 +117,7 @@ impl<S: AsyncWrite + Unpin> Framed<S> {
impl<S: AsyncRead + AsyncWrite + Unpin> Framed<S> { impl<S: AsyncRead + AsyncWrite + Unpin> Framed<S> {
/// Split into owned read and write parts. Beware of potential issues with /// Split into owned read and write parts. Beware of potential issues with
/// using halves in different tasks on TLS stream: /// using halves in different tasks on TLS stream:
/// <https://github.com/tokio-rs/tls/issues/40> /// https://github.com/tokio-rs/tls/issues/40
pub fn split(self) -> (FramedReader<S>, FramedWriter<S>) { pub fn split(self) -> (FramedReader<S>, FramedWriter<S>) {
let (read_half, write_half) = tokio::io::split(self.stream); let (read_half, write_half) = tokio::io::split(self.stream);
let reader = FramedReader { let reader = FramedReader {

View File

@@ -179,7 +179,7 @@ pub struct FeExecuteMessage {
#[derive(Debug)] #[derive(Debug)]
pub struct FeCloseMessage; pub struct FeCloseMessage;
/// An error occurred while parsing or serializing raw stream into Postgres /// An error occured while parsing or serializing raw stream into Postgres
/// messages. /// messages.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum ProtocolError { pub enum ProtocolError {
@@ -934,15 +934,6 @@ impl<'a> BeMessage<'a> {
} }
} }
fn terminate_code(code: &[u8; 5]) -> [u8; 6] {
let mut terminated = [0; 6];
for (i, &elem) in code.iter().enumerate() {
terminated[i] = elem;
}
terminated
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -974,3 +965,12 @@ mod tests {
assert_eq!(split_options(&params), ["foo bar", " \\", "baz ", "lol"]); assert_eq!(split_options(&params), ["foo bar", " \\", "baz ", "lol"]);
} }
} }
fn terminate_code(code: &[u8; 5]) -> [u8; 6] {
let mut terminated = [0; 6];
for (i, &elem) in code.iter().enumerate() {
terminated[i] = elem;
}
terminated
}

View File

@@ -34,12 +34,12 @@ pub const DEFAULT_REMOTE_STORAGE_MAX_CONCURRENT_SYNCS: usize = 50;
pub const DEFAULT_REMOTE_STORAGE_MAX_SYNC_ERRORS: u32 = 10; pub const DEFAULT_REMOTE_STORAGE_MAX_SYNC_ERRORS: u32 = 10;
/// Currently, sync happens with AWS S3, that has two limits on requests per second: /// Currently, sync happens with AWS S3, that has two limits on requests per second:
/// ~200 RPS for IAM services /// ~200 RPS for IAM services
/// <https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.html> /// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.html
/// ~3500 PUT/COPY/POST/DELETE or 5500 GET/HEAD S3 requests /// ~3500 PUT/COPY/POST/DELETE or 5500 GET/HEAD S3 requests
/// <https://aws.amazon.com/premiumsupport/knowledge-center/s3-request-limit-avoid-throttling/> /// https://aws.amazon.com/premiumsupport/knowledge-center/s3-request-limit-avoid-throttling/
pub const DEFAULT_REMOTE_STORAGE_S3_CONCURRENCY_LIMIT: usize = 100; pub const DEFAULT_REMOTE_STORAGE_S3_CONCURRENCY_LIMIT: usize = 100;
/// No limits on the client side, which currenltly means 1000 for AWS S3. /// No limits on the client side, which currenltly means 1000 for AWS S3.
/// <https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html#API_ListObjectsV2_RequestSyntax> /// https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html#API_ListObjectsV2_RequestSyntax
pub const DEFAULT_MAX_KEYS_PER_LIST_RESPONSE: Option<i32> = None; pub const DEFAULT_MAX_KEYS_PER_LIST_RESPONSE: Option<i32> = None;
const REMOTE_STORAGE_PREFIX_SEPARATOR: char = '/'; const REMOTE_STORAGE_PREFIX_SEPARATOR: char = '/';
@@ -50,12 +50,6 @@ const REMOTE_STORAGE_PREFIX_SEPARATOR: char = '/';
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RemotePath(PathBuf); pub struct RemotePath(PathBuf);
impl std::fmt::Display for RemotePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.display())
}
}
impl RemotePath { impl RemotePath {
pub fn new(relative_path: &Path) -> anyhow::Result<Self> { pub fn new(relative_path: &Path) -> anyhow::Result<Self> {
anyhow::ensure!( anyhow::ensure!(
@@ -77,10 +71,6 @@ impl RemotePath {
Self(self.0.join(segment)) Self(self.0.join(segment))
} }
pub fn get_path(&self) -> &PathBuf {
&self.0
}
pub fn extension(&self) -> Option<&str> { pub fn extension(&self) -> Option<&str> {
self.0.extension()?.to_str() self.0.extension()?.to_str()
} }
@@ -100,18 +90,11 @@ pub trait RemoteStorage: Send + Sync + 'static {
prefix: Option<&RemotePath>, prefix: Option<&RemotePath>,
) -> Result<Vec<RemotePath>, DownloadError>; ) -> Result<Vec<RemotePath>, DownloadError>;
/// Lists all files in directory "recursively" /// Lists all files in a subdirectories
/// (not really recursively, because AWS has a flat namespace) async fn list_files(
/// Note: This is subtely different than list_prefixes, &self,
/// because it is for listing files instead of listing prefix: Option<&RemotePath>,
/// names sharing common prefixes. ) -> anyhow::Result<Vec<RemotePath>>;
/// For example,
/// list_files("foo/bar") = ["foo/bar/cat123.txt",
/// "foo/bar/cat567.txt", "foo/bar/dog123.txt", "foo/bar/dog456.txt"]
/// whereas,
/// list_prefixes("foo/bar/") = ["cat", "dog"]
/// See `test_real_s3.rs` for more details.
async fn list_files(&self, folder: Option<&RemotePath>) -> anyhow::Result<Vec<RemotePath>>;
/// Streams the local file contents into remote into the remote storage entry. /// Streams the local file contents into remote into the remote storage entry.
async fn upload( async fn upload(
@@ -138,8 +121,6 @@ pub trait RemoteStorage: Send + Sync + 'static {
) -> Result<Download, DownloadError>; ) -> Result<Download, DownloadError>;
async fn delete(&self, path: &RemotePath) -> anyhow::Result<()>; async fn delete(&self, path: &RemotePath) -> anyhow::Result<()>;
async fn delete_objects<'a>(&self, paths: &'a [RemotePath]) -> anyhow::Result<()>;
} }
pub struct Download { pub struct Download {
@@ -190,6 +171,23 @@ pub enum GenericRemoteStorage {
} }
impl GenericRemoteStorage { impl GenericRemoteStorage {
// A function for listing all the files in a "directory"
// Example:
// list_files("foo/bar") = ["foo/bar/a.txt", "foo/bar/b.txt"]
pub async fn list_files(
&self,
folder: Option<&RemotePath>
) -> anyhow::Result<Vec<RemotePath>>{
match self {
Self::LocalFs(s) => s.list_files(folder).await,
Self::AwsS3(s) => s.list_files(folder).await,
Self::Unreliable(s) => s.list_files(folder).await,
}
}
// lists common *prefixes*, if any of files
// Example:
// list_prefixes("foo123","foo567","bar123","bar432") = ["foo", "bar"]
pub async fn list_prefixes( pub async fn list_prefixes(
&self, &self,
prefix: Option<&RemotePath>, prefix: Option<&RemotePath>,
@@ -201,14 +199,6 @@ impl GenericRemoteStorage {
} }
} }
pub async fn list_files(&self, folder: Option<&RemotePath>) -> anyhow::Result<Vec<RemotePath>> {
match self {
Self::LocalFs(s) => s.list_files(folder).await,
Self::AwsS3(s) => s.list_files(folder).await,
Self::Unreliable(s) => s.list_files(folder).await,
}
}
pub async fn upload( pub async fn upload(
&self, &self,
from: impl io::AsyncRead + Unpin + Send + Sync + 'static, from: impl io::AsyncRead + Unpin + Send + Sync + 'static,
@@ -260,14 +250,6 @@ impl GenericRemoteStorage {
Self::Unreliable(s) => s.delete(path).await, Self::Unreliable(s) => s.delete(path).await,
} }
} }
pub async fn delete_objects<'a>(&self, paths: &'a [RemotePath]) -> anyhow::Result<()> {
match self {
Self::LocalFs(s) => s.delete_objects(paths).await,
Self::AwsS3(s) => s.delete_objects(paths).await,
Self::Unreliable(s) => s.delete_objects(paths).await,
}
}
} }
impl GenericRemoteStorage { impl GenericRemoteStorage {

View File

@@ -7,7 +7,6 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
future::Future, future::Future,
io::ErrorKind,
path::{Path, PathBuf}, path::{Path, PathBuf},
pin::Pin, pin::Pin,
}; };
@@ -18,7 +17,7 @@ use tokio::{
io::{self, AsyncReadExt, AsyncSeekExt, AsyncWriteExt}, io::{self, AsyncReadExt, AsyncSeekExt, AsyncWriteExt},
}; };
use tracing::*; use tracing::*;
use utils::{crashsafe::path_with_suffix_extension, fs_ext::is_directory_empty}; use utils::crashsafe::path_with_suffix_extension;
use crate::{Download, DownloadError, RemotePath}; use crate::{Download, DownloadError, RemotePath};
@@ -49,14 +48,6 @@ impl LocalFs {
Ok(Self { storage_root }) Ok(Self { storage_root })
} }
// mirrors S3Bucket::s3_object_to_relative_path
fn local_file_to_relative_path(&self, key: PathBuf) -> RemotePath {
let relative_path = key
.strip_prefix(&self.storage_root)
.expect("relative path must contain storage_root as prefix");
RemotePath(relative_path.into())
}
async fn read_storage_metadata( async fn read_storage_metadata(
&self, &self,
file_path: &Path, file_path: &Path,
@@ -110,57 +101,44 @@ impl RemoteStorage for LocalFs {
Some(prefix) => Cow::Owned(prefix.with_base(&self.storage_root)), Some(prefix) => Cow::Owned(prefix.with_base(&self.storage_root)),
None => Cow::Borrowed(&self.storage_root), None => Cow::Borrowed(&self.storage_root),
}; };
Ok(get_all_files(path.as_ref(), false)
let prefixes_to_filter = get_all_files(path.as_ref(), false)
.await .await
.map_err(DownloadError::Other)?; .map_err(DownloadError::Other)?
.into_iter()
let mut prefixes = Vec::with_capacity(prefixes_to_filter.len()); .map(|path| {
path.strip_prefix(&self.storage_root)
// filter out empty directories to mirror s3 behavior. .context("Failed to strip preifix")
for prefix in prefixes_to_filter {
if prefix.is_dir()
&& is_directory_empty(&prefix)
.await
.map_err(DownloadError::Other)?
{
continue;
}
prefixes.push(
prefix
.strip_prefix(&self.storage_root)
.context("Failed to strip prefix")
.and_then(RemotePath::new) .and_then(RemotePath::new)
.expect( .expect(
"We list files for storage root, hence should be able to remote the prefix", "We list files for storage root, hence should be able to remote the prefix",
), )
) })
} .collect())
Ok(prefixes)
} }
// recursively lists all files in a directory, async fn list_files(
// mirroring the `list_files` for `s3_bucket` &self,
async fn list_files(&self, folder: Option<&RemotePath>) -> anyhow::Result<Vec<RemotePath>> { folder: Option<&RemotePath>
let full_path = match folder { ) -> anyhow::Result<Vec<RemotePath>> {
/* Note: if you want, you can return a DownloadError instead of an anyhow::Error
as follows: replace all ?'s with:
.map_err(|e| DownloadError::Other(anyhow::Error::from(e)))?;
*/
let full_path = match folder.clone() {
Some(folder) => folder.with_base(&self.storage_root), Some(folder) => folder.with_base(&self.storage_root),
None => self.storage_root.clone(), None => self.storage_root.clone(),
}; };
let mut entries = fs::read_dir(full_path).await?;
let mut files = vec![]; let mut files = vec![];
let mut directory_queue = vec![full_path.clone()]; while let Some(entry) = entries.next_entry().await? {
let file_name: PathBuf = entry.file_name().into();
while let Some(cur_folder) = directory_queue.pop() { let file_type = entry.file_type().await?;
let mut entries = fs::read_dir(cur_folder.clone()).await?; if file_type.is_file() {
while let Some(entry) = entries.next_entry().await? { let mut file_remote_path = RemotePath::new(&file_name)?;
let file_name: PathBuf = entry.file_name().into(); if let Some(folder) = folder {
let full_file_name = cur_folder.clone().join(&file_name); file_remote_path = folder.join(&file_name);
let file_remote_path = self.local_file_to_relative_path(full_file_name.clone()); }
files.push(file_remote_path.clone()); files.push(file_remote_path);
if full_file_name.is_dir() {
directory_queue.push(full_file_name);
}
} }
} }
Ok(files) Ok(files)
@@ -341,22 +319,12 @@ impl RemoteStorage for LocalFs {
async fn delete(&self, path: &RemotePath) -> anyhow::Result<()> { async fn delete(&self, path: &RemotePath) -> anyhow::Result<()> {
let file_path = path.with_base(&self.storage_root); let file_path = path.with_base(&self.storage_root);
match fs::remove_file(&file_path).await { if file_path.exists() && file_path.is_file() {
Ok(()) => Ok(()), Ok(fs::remove_file(file_path).await?)
// The file doesn't exist. This shouldn't yield an error to mirror S3's behaviour. } else {
// See https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html bail!("File {file_path:?} either does not exist or is not a file")
// > If there isn't a null version, Amazon S3 does not remove any objects but will still respond that the command was successful.
Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
Err(e) => Err(anyhow::anyhow!(e)),
} }
} }
async fn delete_objects<'a>(&self, paths: &'a [RemotePath]) -> anyhow::Result<()> {
for path in paths {
self.delete(path).await?
}
Ok(())
}
} }
fn storage_metadata_path(original_path: &Path) -> PathBuf { fn storage_metadata_path(original_path: &Path) -> PathBuf {
@@ -380,7 +348,7 @@ where
let file_type = dir_entry.file_type().await?; let file_type = dir_entry.file_type().await?;
let entry_path = dir_entry.path(); let entry_path = dir_entry.path();
if file_type.is_symlink() { if file_type.is_symlink() {
debug!("{entry_path:?} is a symlink, skipping") debug!("{entry_path:?} us a symlink, skipping")
} else if file_type.is_dir() { } else if file_type.is_dir() {
if recursive { if recursive {
paths.extend(get_all_files(&entry_path, true).await?.into_iter()) paths.extend(get_all_files(&entry_path, true).await?.into_iter())
@@ -655,11 +623,15 @@ mod fs_tests {
storage.delete(&upload_target).await?; storage.delete(&upload_target).await?;
assert!(storage.list().await?.is_empty()); assert!(storage.list().await?.is_empty());
storage match storage.delete(&upload_target).await {
.delete(&upload_target) Ok(()) => panic!("Should not allow deleting non-existing storage files"),
.await Err(e) => {
.expect("Should allow deleting non-existing storage files"); let error_string = e.to_string();
assert!(error_string.contains("does not exist"));
let expected_path = upload_target.with_base(&storage.storage_root);
assert!(error_string.contains(expected_path.to_str().unwrap()));
}
}
Ok(()) Ok(())
} }

View File

@@ -17,7 +17,6 @@ use aws_sdk_s3::{
error::SdkError, error::SdkError,
operation::get_object::GetObjectError, operation::get_object::GetObjectError,
primitives::ByteStream, primitives::ByteStream,
types::{Delete, ObjectIdentifier},
Client, Client,
}; };
use aws_smithy_http::body::SdkBody; use aws_smithy_http::body::SdkBody;
@@ -34,8 +33,6 @@ use crate::{
Download, DownloadError, RemotePath, RemoteStorage, S3Config, REMOTE_STORAGE_PREFIX_SEPARATOR, Download, DownloadError, RemotePath, RemoteStorage, S3Config, REMOTE_STORAGE_PREFIX_SEPARATOR,
}; };
const MAX_DELETE_OBJECTS_REQUEST_SIZE: usize = 1000;
pub(super) mod metrics { pub(super) mod metrics {
use metrics::{register_int_counter_vec, IntCounterVec}; use metrics::{register_int_counter_vec, IntCounterVec};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@@ -84,24 +81,12 @@ pub(super) mod metrics {
.inc(); .inc();
} }
pub fn inc_delete_objects(count: u64) {
S3_REQUESTS_COUNT
.with_label_values(&["delete_object"])
.inc_by(count);
}
pub fn inc_delete_object_fail() { pub fn inc_delete_object_fail() {
S3_REQUESTS_FAIL_COUNT S3_REQUESTS_FAIL_COUNT
.with_label_values(&["delete_object"]) .with_label_values(&["delete_object"])
.inc(); .inc();
} }
pub fn inc_delete_objects_fail(count: u64) {
S3_REQUESTS_FAIL_COUNT
.with_label_values(&["delete_object"])
.inc_by(count);
}
pub fn inc_list_objects() { pub fn inc_list_objects() {
S3_REQUESTS_COUNT.with_label_values(&["list_objects"]).inc(); S3_REQUESTS_COUNT.with_label_values(&["list_objects"]).inc();
} }
@@ -200,17 +185,13 @@ impl S3Bucket {
) )
} }
pub fn relative_path_to_s3_object(&self, path: &RemotePath) -> String { fn relative_path_to_s3_object(&self, path: &RemotePath) -> String {
assert_eq!(std::path::MAIN_SEPARATOR, REMOTE_STORAGE_PREFIX_SEPARATOR); let mut full_path = self.prefix_in_bucket.clone().unwrap_or_default();
let path_string = path for segment in path.0.iter() {
.get_path() full_path.push(REMOTE_STORAGE_PREFIX_SEPARATOR);
.to_string_lossy() full_path.push_str(segment.to_str().unwrap_or_default());
.trim_end_matches(REMOTE_STORAGE_PREFIX_SEPARATOR)
.to_string();
match &self.prefix_in_bucket {
Some(prefix) => prefix.clone() + "/" + &path_string,
None => path_string,
} }
full_path
} }
async fn download_object(&self, request: GetObjectRequest) -> Result<Download, DownloadError> { async fn download_object(&self, request: GetObjectRequest) -> Result<Download, DownloadError> {
@@ -351,13 +332,13 @@ impl RemoteStorage for S3Bucket {
Ok(document_keys) Ok(document_keys)
} }
/// See the doc for `RemoteStorage::list_files` async fn list_files(
async fn list_files(&self, folder: Option<&RemotePath>) -> anyhow::Result<Vec<RemotePath>> { &self,
let folder_name = folder folder: Option<&RemotePath>
.map(|p| self.relative_path_to_s3_object(p)) ) -> anyhow::Result<Vec<RemotePath>>{
.or_else(|| self.prefix_in_bucket.clone()); let folder_name = folder.map(|x|
String::from(x.object_name().expect("invalid folder name"))
// AWS may need to break the response into several parts );
let mut continuation_token = None; let mut continuation_token = None;
let mut all_files = vec![]; let mut all_files = vec![];
loop { loop {
@@ -382,9 +363,9 @@ impl RemoteStorage for S3Bucket {
e e
}) })
.context("Failed to list files in S3 bucket")?; .context("Failed to list files in S3 bucket")?;
for object in response.contents().unwrap_or_default() { for object in response.contents().unwrap_or_default() {
let object_path = object.key().expect("response does not contain a key"); let object_path = object.key().unwrap();
let remote_path = self.s3_object_to_relative_path(object_path); let remote_path = self.s3_object_to_relative_path(object_path);
all_files.push(remote_path); all_files.push(remote_path);
} }
@@ -431,12 +412,10 @@ impl RemoteStorage for S3Bucket {
} }
async fn download(&self, from: &RemotePath) -> Result<Download, DownloadError> { async fn download(&self, from: &RemotePath) -> Result<Download, DownloadError> {
// if prefix is not none then download file `prefix/from`
// if prefix is none then download file `from`
self.download_object(GetObjectRequest { self.download_object(GetObjectRequest {
bucket: self.bucket_name.clone(), bucket: self.bucket_name.clone(),
key: self.relative_path_to_s3_object(from), key: self.relative_path_to_s3_object(from),
range: None, ..GetObjectRequest::default()
}) })
.await .await
} }
@@ -462,50 +441,6 @@ impl RemoteStorage for S3Bucket {
}) })
.await .await
} }
async fn delete_objects<'a>(&self, paths: &'a [RemotePath]) -> anyhow::Result<()> {
let _guard = self
.concurrency_limiter
.acquire()
.await
.context("Concurrency limiter semaphore got closed during S3 delete")?;
let mut delete_objects = Vec::with_capacity(paths.len());
for path in paths {
let obj_id = ObjectIdentifier::builder()
.set_key(Some(self.relative_path_to_s3_object(path)))
.build();
delete_objects.push(obj_id);
}
for chunk in delete_objects.chunks(MAX_DELETE_OBJECTS_REQUEST_SIZE) {
metrics::inc_delete_objects(chunk.len() as u64);
let resp = self
.client
.delete_objects()
.bucket(self.bucket_name.clone())
.delete(Delete::builder().set_objects(Some(chunk.to_vec())).build())
.send()
.await;
match resp {
Ok(resp) => {
if let Some(errors) = resp.errors {
metrics::inc_delete_objects_fail(errors.len() as u64);
return Err(anyhow::format_err!(
"Failed to delete {} objects",
errors.len()
));
}
}
Err(e) => {
metrics::inc_delete_objects_fail(chunk.len() as u64);
return Err(e.into());
}
}
}
Ok(())
}
async fn delete(&self, path: &RemotePath) -> anyhow::Result<()> { async fn delete(&self, path: &RemotePath) -> anyhow::Result<()> {
let _guard = self let _guard = self
@@ -529,63 +464,3 @@ impl RemoteStorage for S3Bucket {
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use std::num::NonZeroUsize;
use std::path::Path;
use crate::{RemotePath, S3Bucket, S3Config};
#[test]
fn relative_path() {
let all_paths = vec!["", "some/path", "some/path/"];
let all_paths: Vec<RemotePath> = all_paths
.iter()
.map(|x| RemotePath::new(Path::new(x)).expect("bad path"))
.collect();
let prefixes = [
None,
Some(""),
Some("test/prefix"),
Some("test/prefix/"),
Some("/test/prefix/"),
];
let expected_outputs = vec![
vec!["", "some/path", "some/path"],
vec!["/", "/some/path", "/some/path"],
vec![
"test/prefix/",
"test/prefix/some/path",
"test/prefix/some/path",
],
vec![
"test/prefix/",
"test/prefix/some/path",
"test/prefix/some/path",
],
vec![
"test/prefix/",
"test/prefix/some/path",
"test/prefix/some/path",
],
];
for (prefix_idx, prefix) in prefixes.iter().enumerate() {
let config = S3Config {
bucket_name: "bucket".to_owned(),
bucket_region: "region".to_owned(),
prefix_in_bucket: prefix.map(str::to_string),
endpoint: None,
concurrency_limit: NonZeroUsize::new(100).unwrap(),
max_keys_per_list_response: Some(5),
};
let storage = S3Bucket::new(&config).expect("remote storage init");
for (test_path_idx, test_path) in all_paths.iter().enumerate() {
let result = storage.relative_path_to_s3_object(test_path);
let expected = expected_outputs[prefix_idx][test_path_idx];
assert_eq!(result, expected);
}
}
}
}

View File

@@ -24,7 +24,6 @@ enum RemoteOp {
Upload(RemotePath), Upload(RemotePath),
Download(RemotePath), Download(RemotePath),
Delete(RemotePath), Delete(RemotePath),
DeleteObjects(Vec<RemotePath>),
} }
impl UnreliableWrapper { impl UnreliableWrapper {
@@ -83,7 +82,10 @@ impl RemoteStorage for UnreliableWrapper {
self.inner.list_prefixes(prefix).await self.inner.list_prefixes(prefix).await
} }
async fn list_files(&self, folder: Option<&RemotePath>) -> anyhow::Result<Vec<RemotePath>> { async fn list_files(
&self,
folder: Option<&RemotePath>
) -> anyhow::Result<Vec<RemotePath>>{
self.attempt(RemoteOp::ListPrefixes(folder.cloned()))?; self.attempt(RemoteOp::ListPrefixes(folder.cloned()))?;
self.inner.list_files(folder).await self.inner.list_files(folder).await
} }
@@ -125,21 +127,4 @@ impl RemoteStorage for UnreliableWrapper {
self.attempt(RemoteOp::Delete(path.clone()))?; self.attempt(RemoteOp::Delete(path.clone()))?;
self.inner.delete(path).await self.inner.delete(path).await
} }
async fn delete_objects<'a>(&self, paths: &'a [RemotePath]) -> anyhow::Result<()> {
self.attempt(RemoteOp::DeleteObjects(paths.to_vec()))?;
let mut error_counter = 0;
for path in paths {
if (self.delete(path).await).is_err() {
error_counter += 1;
}
}
if error_counter > 0 {
return Err(anyhow::anyhow!(
"failed to delete {} objects",
error_counter
));
}
Ok(())
}
} }

View File

@@ -0,0 +1,274 @@
use std::collections::HashSet;
use std::env;
use std::num::{NonZeroU32, NonZeroUsize};
use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::UNIX_EPOCH;
use anyhow::Context;
use remote_storage::{
GenericRemoteStorage, RemotePath, RemoteStorageConfig, RemoteStorageKind, S3Config,
};
use test_context::{test_context, AsyncTestContext};
use tokio::task::JoinSet;
use tracing::{debug, error, info};
const ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME: &str = "ENABLE_REAL_S3_REMOTE_STORAGE";
/// Tests that S3 client can list all prefixes, even if the response come paginated and requires multiple S3 queries.
/// Uses real S3 and requires [`ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME`] and related S3 cred env vars specified.
/// See the client creation in [`create_s3_client`] for details on the required env vars.
/// If real S3 tests are disabled, the test passes, skipping any real test run: currently, there's no way to mark the test ignored in runtime with the
/// deafult test framework, see https://github.com/rust-lang/rust/issues/68007 for details.
///
/// First, the test creates a set of S3 objects with keys `/${random_prefix_part}/${base_prefix_str}/sub_prefix_${i}/blob_${i}` in [`upload_s3_data`]
/// where
/// * `random_prefix_part` is set for the entire S3 client during the S3 client creation in [`create_s3_client`], to avoid multiple test runs interference
/// * `base_prefix_str` is a common prefix to use in the client requests: we would want to ensure that the client is able to list nested prefixes inside the bucket
///
/// Then, verifies that the client does return correct prefixes when queried:
/// * with no prefix, it lists everything after its `${random_prefix_part}/` — that should be `${base_prefix_str}` value only
/// * with `${base_prefix_str}/` prefix, it lists every `sub_prefix_${i}`
///
/// With the real S3 enabled and `#[cfg(test)]` Rust configuration used, the S3 client test adds a `max-keys` param to limit the response keys.
/// This way, we are able to test the pagination implicitly, by ensuring all results are returned from the remote storage and avoid uploading too many blobs to S3,
/// since current default AWS S3 pagination limit is 1000.
/// (see https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html#API_ListObjectsV2_RequestSyntax)
///
/// Lastly, the test attempts to clean up and remove all uploaded S3 files.
/// If any errors appear during the clean up, they get logged, but the test is not failed or stopped until clean up is finished.
#[test_context(MaybeEnabledS3)]
#[tokio::test]
async fn s3_pagination_should_work(ctx: &mut MaybeEnabledS3) -> anyhow::Result<()> {
let ctx = match ctx {
MaybeEnabledS3::Enabled(ctx) => ctx,
MaybeEnabledS3::Disabled => return Ok(()),
MaybeEnabledS3::UploadsFailed(e, _) => anyhow::bail!("S3 init failed: {e:?}"),
};
let test_client = Arc::clone(&ctx.client_with_excessive_pagination);
let expected_remote_prefixes = ctx.remote_prefixes.clone();
let base_prefix =
RemotePath::new(Path::new(ctx.base_prefix_str)).context("common_prefix construction")?;
let root_remote_prefixes = test_client
.list_prefixes(None)
.await
.context("client list root prefixes failure")?
.into_iter()
.collect::<HashSet<_>>();
assert_eq!(
root_remote_prefixes, HashSet::from([base_prefix.clone()]),
"remote storage root prefixes list mismatches with the uploads. Returned prefixes: {root_remote_prefixes:?}"
);
let nested_remote_prefixes = test_client
.list_prefixes(Some(&base_prefix))
.await
.context("client list nested prefixes failure")?
.into_iter()
.collect::<HashSet<_>>();
let remote_only_prefixes = nested_remote_prefixes
.difference(&expected_remote_prefixes)
.collect::<HashSet<_>>();
let missing_uploaded_prefixes = expected_remote_prefixes
.difference(&nested_remote_prefixes)
.collect::<HashSet<_>>();
assert_eq!(
remote_only_prefixes.len() + missing_uploaded_prefixes.len(), 0,
"remote storage nested prefixes list mismatches with the uploads. Remote only prefixes: {remote_only_prefixes:?}, missing uploaded prefixes: {missing_uploaded_prefixes:?}",
);
Ok(())
}
enum MaybeEnabledS3 {
Enabled(S3WithTestBlobs),
Disabled,
UploadsFailed(anyhow::Error, S3WithTestBlobs),
}
struct S3WithTestBlobs {
client_with_excessive_pagination: Arc<GenericRemoteStorage>,
base_prefix_str: &'static str,
remote_prefixes: HashSet<RemotePath>,
remote_blobs: HashSet<RemotePath>,
}
#[async_trait::async_trait]
impl AsyncTestContext for MaybeEnabledS3 {
async fn setup() -> Self {
utils::logging::init(
utils::logging::LogFormat::Test,
utils::logging::TracingErrorLayerEnablement::Disabled,
)
.expect("logging init failed");
if env::var(ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME).is_err() {
info!(
"`{}` env variable is not set, skipping the test",
ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME
);
return Self::Disabled;
}
let max_keys_in_list_response = 10;
let upload_tasks_count = 1 + (2 * usize::try_from(max_keys_in_list_response).unwrap());
let client_with_excessive_pagination = create_s3_client(max_keys_in_list_response)
.context("S3 client creation")
.expect("S3 client creation failed");
let base_prefix_str = "test/";
match upload_s3_data(
&client_with_excessive_pagination,
base_prefix_str,
upload_tasks_count,
)
.await
{
ControlFlow::Continue(uploads) => {
info!("Remote objects created successfully");
Self::Enabled(S3WithTestBlobs {
client_with_excessive_pagination,
base_prefix_str,
remote_prefixes: uploads.prefixes,
remote_blobs: uploads.blobs,
})
}
ControlFlow::Break(uploads) => Self::UploadsFailed(
anyhow::anyhow!("One or multiple blobs failed to upload to S3"),
S3WithTestBlobs {
client_with_excessive_pagination,
base_prefix_str,
remote_prefixes: uploads.prefixes,
remote_blobs: uploads.blobs,
},
),
}
}
async fn teardown(self) {
match self {
Self::Disabled => {}
Self::Enabled(ctx) | Self::UploadsFailed(_, ctx) => {
cleanup(&ctx.client_with_excessive_pagination, ctx.remote_blobs).await;
}
}
}
}
fn create_s3_client(max_keys_per_list_response: i32) -> anyhow::Result<Arc<GenericRemoteStorage>> {
let remote_storage_s3_bucket = env::var("REMOTE_STORAGE_S3_BUCKET")
.context("`REMOTE_STORAGE_S3_BUCKET` env var is not set, but real S3 tests are enabled")?;
let remote_storage_s3_region = env::var("REMOTE_STORAGE_S3_REGION")
.context("`REMOTE_STORAGE_S3_REGION` env var is not set, but real S3 tests are enabled")?;
let random_prefix_part = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("random s3 test prefix part calculation")?
.as_millis();
let remote_storage_config = RemoteStorageConfig {
max_concurrent_syncs: NonZeroUsize::new(100).unwrap(),
max_sync_errors: NonZeroU32::new(5).unwrap(),
storage: RemoteStorageKind::AwsS3(S3Config {
bucket_name: remote_storage_s3_bucket,
bucket_region: remote_storage_s3_region,
prefix_in_bucket: Some(format!("pagination_should_work_test_{random_prefix_part}/")),
endpoint: None,
concurrency_limit: NonZeroUsize::new(100).unwrap(),
max_keys_per_list_response: Some(max_keys_per_list_response),
}),
};
Ok(Arc::new(
GenericRemoteStorage::from_config(&remote_storage_config).context("remote storage init")?,
))
}
struct Uploads {
prefixes: HashSet<RemotePath>,
blobs: HashSet<RemotePath>,
}
async fn upload_s3_data(
client: &Arc<GenericRemoteStorage>,
base_prefix_str: &'static str,
upload_tasks_count: usize,
) -> ControlFlow<Uploads, Uploads> {
info!("Creating {upload_tasks_count} S3 files");
let mut upload_tasks = JoinSet::new();
for i in 1..upload_tasks_count + 1 {
let task_client = Arc::clone(client);
upload_tasks.spawn(async move {
let prefix = PathBuf::from(format!("{base_prefix_str}/sub_prefix_{i}/"));
let blob_prefix = RemotePath::new(&prefix)
.with_context(|| format!("{prefix:?} to RemotePath conversion"))?;
let blob_path = blob_prefix.join(Path::new(&format!("blob_{i}")));
debug!("Creating remote item {i} at path {blob_path:?}");
let data = format!("remote blob data {i}").into_bytes();
let data_len = data.len();
task_client
.upload(std::io::Cursor::new(data), data_len, &blob_path, None)
.await?;
Ok::<_, anyhow::Error>((blob_prefix, blob_path))
});
}
let mut upload_tasks_failed = false;
let mut uploaded_prefixes = HashSet::with_capacity(upload_tasks_count);
let mut uploaded_blobs = HashSet::with_capacity(upload_tasks_count);
while let Some(task_run_result) = upload_tasks.join_next().await {
match task_run_result
.context("task join failed")
.and_then(|task_result| task_result.context("upload task failed"))
{
Ok((upload_prefix, upload_path)) => {
uploaded_prefixes.insert(upload_prefix);
uploaded_blobs.insert(upload_path);
}
Err(e) => {
error!("Upload task failed: {e:?}");
upload_tasks_failed = true;
}
}
}
let uploads = Uploads {
prefixes: uploaded_prefixes,
blobs: uploaded_blobs,
};
if upload_tasks_failed {
ControlFlow::Break(uploads)
} else {
ControlFlow::Continue(uploads)
}
}
async fn cleanup(client: &Arc<GenericRemoteStorage>, objects_to_delete: HashSet<RemotePath>) {
info!(
"Removing {} objects from the remote storage during cleanup",
objects_to_delete.len()
);
let mut delete_tasks = JoinSet::new();
for object_to_delete in objects_to_delete {
let task_client = Arc::clone(client);
delete_tasks.spawn(async move {
debug!("Deleting remote item at path {object_to_delete:?}");
task_client
.delete(&object_to_delete)
.await
.with_context(|| format!("{object_to_delete:?} removal"))
});
}
while let Some(task_run_result) = delete_tasks.join_next().await {
match task_run_result {
Ok(task_result) => match task_result {
Ok(()) => {}
Err(e) => error!("Delete task failed: {e:?}"),
},
Err(join_err) => error!("Delete task did not finish correctly: {join_err}"),
}
}
}

View File

@@ -1,542 +0,0 @@
use std::collections::HashSet;
use std::env;
use std::num::{NonZeroU32, NonZeroUsize};
use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::UNIX_EPOCH;
use anyhow::Context;
use once_cell::sync::OnceCell;
use remote_storage::{
GenericRemoteStorage, RemotePath, RemoteStorageConfig, RemoteStorageKind, S3Config,
};
use test_context::{test_context, AsyncTestContext};
use tokio::task::JoinSet;
use tracing::{debug, error, info};
static LOGGING_DONE: OnceCell<()> = OnceCell::new();
const ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME: &str = "ENABLE_REAL_S3_REMOTE_STORAGE";
const BASE_PREFIX: &str = "test";
/// Tests that S3 client can list all prefixes, even if the response come paginated and requires multiple S3 queries.
/// Uses real S3 and requires [`ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME`] and related S3 cred env vars specified.
/// See the client creation in [`create_s3_client`] for details on the required env vars.
/// If real S3 tests are disabled, the test passes, skipping any real test run: currently, there's no way to mark the test ignored in runtime with the
/// deafult test framework, see https://github.com/rust-lang/rust/issues/68007 for details.
///
/// First, the test creates a set of S3 objects with keys `/${random_prefix_part}/${base_prefix_str}/sub_prefix_${i}/blob_${i}` in [`upload_s3_data`]
/// where
/// * `random_prefix_part` is set for the entire S3 client during the S3 client creation in [`create_s3_client`], to avoid multiple test runs interference
/// * `base_prefix_str` is a common prefix to use in the client requests: we would want to ensure that the client is able to list nested prefixes inside the bucket
///
/// Then, verifies that the client does return correct prefixes when queried:
/// * with no prefix, it lists everything after its `${random_prefix_part}/` — that should be `${base_prefix_str}` value only
/// * with `${base_prefix_str}/` prefix, it lists every `sub_prefix_${i}`
///
/// With the real S3 enabled and `#[cfg(test)]` Rust configuration used, the S3 client test adds a `max-keys` param to limit the response keys.
/// This way, we are able to test the pagination implicitly, by ensuring all results are returned from the remote storage and avoid uploading too many blobs to S3,
/// since current default AWS S3 pagination limit is 1000.
/// (see https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html#API_ListObjectsV2_RequestSyntax)
///
/// Lastly, the test attempts to clean up and remove all uploaded S3 files.
/// If any errors appear during the clean up, they get logged, but the test is not failed or stopped until clean up is finished.
#[test_context(MaybeEnabledS3WithTestBlobs)]
#[tokio::test]
async fn s3_pagination_should_work(ctx: &mut MaybeEnabledS3WithTestBlobs) -> anyhow::Result<()> {
let ctx = match ctx {
MaybeEnabledS3WithTestBlobs::Enabled(ctx) => ctx,
MaybeEnabledS3WithTestBlobs::Disabled => return Ok(()),
MaybeEnabledS3WithTestBlobs::UploadsFailed(e, _) => anyhow::bail!("S3 init failed: {e:?}"),
};
let test_client = Arc::clone(&ctx.enabled.client);
let expected_remote_prefixes = ctx.remote_prefixes.clone();
let base_prefix = RemotePath::new(Path::new(ctx.enabled.base_prefix))
.context("common_prefix construction")?;
let root_remote_prefixes = test_client
.list_prefixes(None)
.await
.context("client list root prefixes failure")?
.into_iter()
.collect::<HashSet<_>>();
assert_eq!(
root_remote_prefixes, HashSet::from([base_prefix.clone()]),
"remote storage root prefixes list mismatches with the uploads. Returned prefixes: {root_remote_prefixes:?}"
);
let nested_remote_prefixes = test_client
.list_prefixes(Some(&base_prefix))
.await
.context("client list nested prefixes failure")?
.into_iter()
.collect::<HashSet<_>>();
let remote_only_prefixes = nested_remote_prefixes
.difference(&expected_remote_prefixes)
.collect::<HashSet<_>>();
let missing_uploaded_prefixes = expected_remote_prefixes
.difference(&nested_remote_prefixes)
.collect::<HashSet<_>>();
assert_eq!(
remote_only_prefixes.len() + missing_uploaded_prefixes.len(), 0,
"remote storage nested prefixes list mismatches with the uploads. Remote only prefixes: {remote_only_prefixes:?}, missing uploaded prefixes: {missing_uploaded_prefixes:?}",
);
Ok(())
}
/// Tests that S3 client can list all files in a folder, even if the response comes paginated and requirees multiple S3 queries.
/// Uses real S3 and requires [`ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME`] and related S3 cred env vars specified. Test will skip real code and pass if env vars not set.
/// See `s3_pagination_should_work` for more information.
///
/// First, create a set of S3 objects with keys `random_prefix/folder{j}/blob_{i}.txt` in [`upload_s3_data`]
/// Then performs the following queries:
/// 1. `list_files(None)`. This should return all files `random_prefix/folder{j}/blob_{i}.txt`
/// 2. `list_files("folder1")`. This should return all files `random_prefix/folder1/blob_{i}.txt`
#[test_context(MaybeEnabledS3WithSimpleTestBlobs)]
#[tokio::test]
async fn s3_list_files_works(ctx: &mut MaybeEnabledS3WithSimpleTestBlobs) -> anyhow::Result<()> {
let ctx = match ctx {
MaybeEnabledS3WithSimpleTestBlobs::Enabled(ctx) => ctx,
MaybeEnabledS3WithSimpleTestBlobs::Disabled => return Ok(()),
MaybeEnabledS3WithSimpleTestBlobs::UploadsFailed(e, _) => {
anyhow::bail!("S3 init failed: {e:?}")
}
};
let test_client = Arc::clone(&ctx.enabled.client);
let base_prefix =
RemotePath::new(Path::new("folder1")).context("common_prefix construction")?;
let root_files = test_client
.list_files(None)
.await
.context("client list root files failure")?
.into_iter()
.collect::<HashSet<_>>();
assert_eq!(
root_files,
ctx.remote_blobs.clone(),
"remote storage list_files on root mismatches with the uploads."
);
let nested_remote_files = test_client
.list_files(Some(&base_prefix))
.await
.context("client list nested files failure")?
.into_iter()
.collect::<HashSet<_>>();
let trim_remote_blobs: HashSet<_> = ctx
.remote_blobs
.iter()
.map(|x| x.get_path().to_str().expect("must be valid name"))
.filter(|x| x.starts_with("folder1"))
.map(|x| RemotePath::new(Path::new(x)).expect("must be valid name"))
.collect();
assert_eq!(
nested_remote_files, trim_remote_blobs,
"remote storage list_files on subdirrectory mismatches with the uploads."
);
Ok(())
}
#[test_context(MaybeEnabledS3)]
#[tokio::test]
async fn s3_delete_non_exising_works(ctx: &mut MaybeEnabledS3) -> anyhow::Result<()> {
let ctx = match ctx {
MaybeEnabledS3::Enabled(ctx) => ctx,
MaybeEnabledS3::Disabled => return Ok(()),
};
let path = RemotePath::new(&PathBuf::from(format!(
"{}/for_sure_there_is_nothing_there_really",
ctx.base_prefix,
)))
.with_context(|| "RemotePath conversion")?;
ctx.client.delete(&path).await.expect("should succeed");
Ok(())
}
#[test_context(MaybeEnabledS3)]
#[tokio::test]
async fn s3_delete_objects_works(ctx: &mut MaybeEnabledS3) -> anyhow::Result<()> {
let ctx = match ctx {
MaybeEnabledS3::Enabled(ctx) => ctx,
MaybeEnabledS3::Disabled => return Ok(()),
};
let path1 = RemotePath::new(&PathBuf::from(format!("{}/path1", ctx.base_prefix,)))
.with_context(|| "RemotePath conversion")?;
let path2 = RemotePath::new(&PathBuf::from(format!("{}/path2", ctx.base_prefix,)))
.with_context(|| "RemotePath conversion")?;
let path3 = RemotePath::new(&PathBuf::from(format!("{}/path3", ctx.base_prefix,)))
.with_context(|| "RemotePath conversion")?;
let data1 = "remote blob data1".as_bytes();
let data1_len = data1.len();
let data2 = "remote blob data2".as_bytes();
let data2_len = data2.len();
let data3 = "remote blob data3".as_bytes();
let data3_len = data3.len();
ctx.client
.upload(std::io::Cursor::new(data1), data1_len, &path1, None)
.await?;
ctx.client
.upload(std::io::Cursor::new(data2), data2_len, &path2, None)
.await?;
ctx.client
.upload(std::io::Cursor::new(data3), data3_len, &path3, None)
.await?;
ctx.client.delete_objects(&[path1, path2]).await?;
let prefixes = ctx.client.list_prefixes(None).await?;
assert_eq!(prefixes.len(), 1);
ctx.client.delete_objects(&[path3]).await?;
Ok(())
}
fn ensure_logging_ready() {
LOGGING_DONE.get_or_init(|| {
utils::logging::init(
utils::logging::LogFormat::Test,
utils::logging::TracingErrorLayerEnablement::Disabled,
)
.expect("logging init failed");
});
}
struct EnabledS3 {
client: Arc<GenericRemoteStorage>,
base_prefix: &'static str,
}
impl EnabledS3 {
async fn setup(max_keys_in_list_response: Option<i32>) -> Self {
let client = create_s3_client(max_keys_in_list_response)
.context("S3 client creation")
.expect("S3 client creation failed");
EnabledS3 {
client,
base_prefix: BASE_PREFIX,
}
}
}
enum MaybeEnabledS3 {
Enabled(EnabledS3),
Disabled,
}
#[async_trait::async_trait]
impl AsyncTestContext for MaybeEnabledS3 {
async fn setup() -> Self {
ensure_logging_ready();
if env::var(ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME).is_err() {
info!(
"`{}` env variable is not set, skipping the test",
ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME
);
return Self::Disabled;
}
Self::Enabled(EnabledS3::setup(None).await)
}
}
enum MaybeEnabledS3WithTestBlobs {
Enabled(S3WithTestBlobs),
Disabled,
UploadsFailed(anyhow::Error, S3WithTestBlobs),
}
struct S3WithTestBlobs {
enabled: EnabledS3,
remote_prefixes: HashSet<RemotePath>,
remote_blobs: HashSet<RemotePath>,
}
#[async_trait::async_trait]
impl AsyncTestContext for MaybeEnabledS3WithTestBlobs {
async fn setup() -> Self {
ensure_logging_ready();
if env::var(ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME).is_err() {
info!(
"`{}` env variable is not set, skipping the test",
ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME
);
return Self::Disabled;
}
let max_keys_in_list_response = 10;
let upload_tasks_count = 1 + (2 * usize::try_from(max_keys_in_list_response).unwrap());
let enabled = EnabledS3::setup(Some(max_keys_in_list_response)).await;
match upload_s3_data(&enabled.client, enabled.base_prefix, upload_tasks_count).await {
ControlFlow::Continue(uploads) => {
info!("Remote objects created successfully");
Self::Enabled(S3WithTestBlobs {
enabled,
remote_prefixes: uploads.prefixes,
remote_blobs: uploads.blobs,
})
}
ControlFlow::Break(uploads) => Self::UploadsFailed(
anyhow::anyhow!("One or multiple blobs failed to upload to S3"),
S3WithTestBlobs {
enabled,
remote_prefixes: uploads.prefixes,
remote_blobs: uploads.blobs,
},
),
}
}
async fn teardown(self) {
match self {
Self::Disabled => {}
Self::Enabled(ctx) | Self::UploadsFailed(_, ctx) => {
cleanup(&ctx.enabled.client, ctx.remote_blobs).await;
}
}
}
}
// NOTE: the setups for the list_prefixes test and the list_files test are very similar
// However, they are not idential. The list_prefixes function is concerned with listing prefixes,
// whereas the list_files function is concerned with listing files.
// See `RemoteStorage::list_files` documentation for more details
enum MaybeEnabledS3WithSimpleTestBlobs {
Enabled(S3WithSimpleTestBlobs),
Disabled,
UploadsFailed(anyhow::Error, S3WithSimpleTestBlobs),
}
struct S3WithSimpleTestBlobs {
enabled: EnabledS3,
remote_blobs: HashSet<RemotePath>,
}
#[async_trait::async_trait]
impl AsyncTestContext for MaybeEnabledS3WithSimpleTestBlobs {
async fn setup() -> Self {
ensure_logging_ready();
if env::var(ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME).is_err() {
info!(
"`{}` env variable is not set, skipping the test",
ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME
);
return Self::Disabled;
}
let max_keys_in_list_response = 10;
let upload_tasks_count = 1 + (2 * usize::try_from(max_keys_in_list_response).unwrap());
let enabled = EnabledS3::setup(Some(max_keys_in_list_response)).await;
match upload_simple_s3_data(&enabled.client, upload_tasks_count).await {
ControlFlow::Continue(uploads) => {
info!("Remote objects created successfully");
Self::Enabled(S3WithSimpleTestBlobs {
enabled,
remote_blobs: uploads,
})
}
ControlFlow::Break(uploads) => Self::UploadsFailed(
anyhow::anyhow!("One or multiple blobs failed to upload to S3"),
S3WithSimpleTestBlobs {
enabled,
remote_blobs: uploads,
},
),
}
}
async fn teardown(self) {
match self {
Self::Disabled => {}
Self::Enabled(ctx) | Self::UploadsFailed(_, ctx) => {
cleanup(&ctx.enabled.client, ctx.remote_blobs).await;
}
}
}
}
fn create_s3_client(
max_keys_per_list_response: Option<i32>,
) -> anyhow::Result<Arc<GenericRemoteStorage>> {
let remote_storage_s3_bucket = env::var("REMOTE_STORAGE_S3_BUCKET")
.context("`REMOTE_STORAGE_S3_BUCKET` env var is not set, but real S3 tests are enabled")?;
let remote_storage_s3_region = env::var("REMOTE_STORAGE_S3_REGION")
.context("`REMOTE_STORAGE_S3_REGION` env var is not set, but real S3 tests are enabled")?;
let random_prefix_part = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("random s3 test prefix part calculation")?
.as_nanos();
let remote_storage_config = RemoteStorageConfig {
max_concurrent_syncs: NonZeroUsize::new(100).unwrap(),
max_sync_errors: NonZeroU32::new(5).unwrap(),
storage: RemoteStorageKind::AwsS3(S3Config {
bucket_name: remote_storage_s3_bucket,
bucket_region: remote_storage_s3_region,
prefix_in_bucket: Some(format!("pagination_should_work_test_{random_prefix_part}/")),
endpoint: None,
concurrency_limit: NonZeroUsize::new(100).unwrap(),
max_keys_per_list_response,
}),
};
Ok(Arc::new(
GenericRemoteStorage::from_config(&remote_storage_config).context("remote storage init")?,
))
}
struct Uploads {
prefixes: HashSet<RemotePath>,
blobs: HashSet<RemotePath>,
}
async fn upload_s3_data(
client: &Arc<GenericRemoteStorage>,
base_prefix_str: &'static str,
upload_tasks_count: usize,
) -> ControlFlow<Uploads, Uploads> {
info!("Creating {upload_tasks_count} S3 files");
let mut upload_tasks = JoinSet::new();
for i in 1..upload_tasks_count + 1 {
let task_client = Arc::clone(client);
upload_tasks.spawn(async move {
let prefix = PathBuf::from(format!("{base_prefix_str}/sub_prefix_{i}/"));
let blob_prefix = RemotePath::new(&prefix)
.with_context(|| format!("{prefix:?} to RemotePath conversion"))?;
let blob_path = blob_prefix.join(Path::new(&format!("blob_{i}")));
debug!("Creating remote item {i} at path {blob_path:?}");
let data = format!("remote blob data {i}").into_bytes();
let data_len = data.len();
task_client
.upload(std::io::Cursor::new(data), data_len, &blob_path, None)
.await?;
Ok::<_, anyhow::Error>((blob_prefix, blob_path))
});
}
let mut upload_tasks_failed = false;
let mut uploaded_prefixes = HashSet::with_capacity(upload_tasks_count);
let mut uploaded_blobs = HashSet::with_capacity(upload_tasks_count);
while let Some(task_run_result) = upload_tasks.join_next().await {
match task_run_result
.context("task join failed")
.and_then(|task_result| task_result.context("upload task failed"))
{
Ok((upload_prefix, upload_path)) => {
uploaded_prefixes.insert(upload_prefix);
uploaded_blobs.insert(upload_path);
}
Err(e) => {
error!("Upload task failed: {e:?}");
upload_tasks_failed = true;
}
}
}
let uploads = Uploads {
prefixes: uploaded_prefixes,
blobs: uploaded_blobs,
};
if upload_tasks_failed {
ControlFlow::Break(uploads)
} else {
ControlFlow::Continue(uploads)
}
}
async fn cleanup(client: &Arc<GenericRemoteStorage>, objects_to_delete: HashSet<RemotePath>) {
info!(
"Removing {} objects from the remote storage during cleanup",
objects_to_delete.len()
);
let mut delete_tasks = JoinSet::new();
for object_to_delete in objects_to_delete {
let task_client = Arc::clone(client);
delete_tasks.spawn(async move {
debug!("Deleting remote item at path {object_to_delete:?}");
task_client
.delete(&object_to_delete)
.await
.with_context(|| format!("{object_to_delete:?} removal"))
});
}
while let Some(task_run_result) = delete_tasks.join_next().await {
match task_run_result {
Ok(task_result) => match task_result {
Ok(()) => {}
Err(e) => error!("Delete task failed: {e:?}"),
},
Err(join_err) => error!("Delete task did not finish correctly: {join_err}"),
}
}
}
// Uploads files `folder{j}/blob{i}.txt`. See test description for more details.
async fn upload_simple_s3_data(
client: &Arc<GenericRemoteStorage>,
upload_tasks_count: usize,
) -> ControlFlow<HashSet<RemotePath>, HashSet<RemotePath>> {
info!("Creating {upload_tasks_count} S3 files");
let mut upload_tasks = JoinSet::new();
for i in 1..upload_tasks_count + 1 {
let task_client = Arc::clone(client);
upload_tasks.spawn(async move {
let blob_path = PathBuf::from(format!("folder{}/blob_{}.txt", i / 7, i));
let blob_path = RemotePath::new(&blob_path)
.with_context(|| format!("{blob_path:?} to RemotePath conversion"))?;
debug!("Creating remote item {i} at path {blob_path:?}");
let data = format!("remote blob data {i}").into_bytes();
let data_len = data.len();
task_client
.upload(std::io::Cursor::new(data), data_len, &blob_path, None)
.await?;
Ok::<_, anyhow::Error>(blob_path)
});
}
let mut upload_tasks_failed = false;
let mut uploaded_blobs = HashSet::with_capacity(upload_tasks_count);
while let Some(task_run_result) = upload_tasks.join_next().await {
match task_run_result
.context("task join failed")
.and_then(|task_result| task_result.context("upload task failed"))
{
Ok(upload_path) => {
uploaded_blobs.insert(upload_path);
}
Err(e) => {
error!("Upload task failed: {e:?}");
upload_tasks_failed = true;
}
}
}
if upload_tasks_failed {
ControlFlow::Break(uploaded_blobs)
} else {
ControlFlow::Continue(uploaded_blobs)
}
}

View File

@@ -21,7 +21,7 @@ use crate::{SegmentMethod, SegmentSizeResult, SizeResult, StorageModel};
// 2. D+C+a+b // 2. D+C+a+b
// 3. D+A+B // 3. D+A+B
/// `Segment` which has had its size calculated. /// [`Segment`] which has had it's size calculated.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct SegmentSize { struct SegmentSize {
method: SegmentMethod, method: SegmentMethod,

View File

@@ -33,7 +33,7 @@ pub enum OtelName<'a> {
/// directly into HTTP servers. However, I couldn't find one for Hyper, /// directly into HTTP servers. However, I couldn't find one for Hyper,
/// so I had to write our own. OpenTelemetry website has a registry of /// so I had to write our own. OpenTelemetry website has a registry of
/// instrumentation libraries at: /// instrumentation libraries at:
/// <https://opentelemetry.io/registry/?language=rust&component=instrumentation> /// https://opentelemetry.io/registry/?language=rust&component=instrumentation
/// If a Hyper crate appears, consider switching to that. /// If a Hyper crate appears, consider switching to that.
pub async fn tracing_handler<F, R>( pub async fn tracing_handler<F, R>(
req: Request<Body>, req: Request<Body>,

View File

@@ -5,6 +5,7 @@ edition.workspace = true
license.workspace = true license.workspace = true
[dependencies] [dependencies]
atty.workspace = true
sentry.workspace = true sentry.workspace = true
async-trait.workspace = true async-trait.workspace = true
anyhow.workspace = true anyhow.workspace = true
@@ -40,12 +41,6 @@ pq_proto.workspace = true
metrics.workspace = true metrics.workspace = true
workspace_hack.workspace = true workspace_hack.workspace = true
const_format.workspace = true
# to use tokio channels as streams, this is faster to compile than async_stream
# why is it only here? no other crate should use it, streams are rarely needed.
tokio-stream = { version = "0.1.14" }
[dev-dependencies] [dev-dependencies]
byteorder.workspace = true byteorder.workspace = true
bytes.workspace = true bytes.workspace = true

View File

@@ -16,7 +16,7 @@ use crate::id::TenantId;
/// Algorithm to use. We require EdDSA. /// Algorithm to use. We require EdDSA.
const STORAGE_TOKEN_ALGORITHM: Algorithm = Algorithm::EdDSA; const STORAGE_TOKEN_ALGORITHM: Algorithm = Algorithm::EdDSA;
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Scope { pub enum Scope {
// Provides access to all data for a specific tenant (specified in `struct Claims` below) // Provides access to all data for a specific tenant (specified in `struct Claims` below)

View File

@@ -12,13 +12,6 @@ pub struct Completion(mpsc::Sender<()>);
#[derive(Clone)] #[derive(Clone)]
pub struct Barrier(Arc<Mutex<mpsc::Receiver<()>>>); pub struct Barrier(Arc<Mutex<mpsc::Receiver<()>>>);
impl Default for Barrier {
fn default() -> Self {
let (_, rx) = channel();
rx
}
}
impl Barrier { impl Barrier {
pub async fn wait(self) { pub async fn wait(self) {
self.0.lock().await.recv().await; self.0.lock().await.recv().await;
@@ -31,15 +24,6 @@ impl Barrier {
} }
} }
impl PartialEq for Barrier {
fn eq(&self, other: &Self) -> bool {
// we don't use dyn so this is good
Arc::ptr_eq(&self.0, &other.0)
}
}
impl Eq for Barrier {}
/// Create new Guard and Barrier pair. /// Create new Guard and Barrier pair.
pub fn channel() -> (Completion, Barrier) { pub fn channel() -> (Completion, Barrier) {
let (tx, rx) = mpsc::channel::<()>(1); let (tx, rx) = mpsc::channel::<()>(1);

View File

@@ -1,111 +0,0 @@
/// Create a reporter for an error that outputs similar to [`anyhow::Error`] with Display with alternative setting.
///
/// It can be used with `anyhow::Error` as well.
///
/// Why would one use this instead of converting to `anyhow::Error` on the spot? Because
/// anyhow::Error would also capture a stacktrace on the spot, which you would later discard after
/// formatting.
///
/// ## Usage
///
/// ```rust
/// #[derive(Debug, thiserror::Error)]
/// enum MyCoolError {
/// #[error("should never happen")]
/// Bad(#[source] std::io::Error),
/// }
///
/// # fn failing_call() -> Result<(), MyCoolError> { Err(MyCoolError::Bad(std::io::ErrorKind::PermissionDenied.into())) }
///
/// # fn main() {
/// use utils::error::report_compact_sources;
///
/// if let Err(e) = failing_call() {
/// let e = report_compact_sources(&e);
/// assert_eq!(format!("{e}"), "should never happen: permission denied");
/// }
/// # }
/// ```
///
/// ## TODO
///
/// When we are able to describe return position impl trait in traits, this should of course be an
/// extension trait. Until then avoid boxing with this more ackward interface.
pub fn report_compact_sources<E: std::error::Error>(e: &E) -> impl std::fmt::Display + '_ {
struct AnyhowDisplayAlternateAlike<'a, E>(&'a E);
impl<E: std::error::Error> std::fmt::Display for AnyhowDisplayAlternateAlike<'_, E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)?;
// why is E a generic parameter here? hope that rustc will see through a default
// Error::source implementation and leave the following out if there cannot be any
// sources:
Sources(self.0.source()).try_for_each(|src| write!(f, ": {}", src))
}
}
struct Sources<'a>(Option<&'a (dyn std::error::Error + 'static)>);
impl<'a> Iterator for Sources<'a> {
type Item = &'a (dyn std::error::Error + 'static);
fn next(&mut self) -> Option<Self::Item> {
let rem = self.0;
let next = self.0.and_then(|x| x.source());
self.0 = next;
rem
}
}
AnyhowDisplayAlternateAlike(e)
}
#[cfg(test)]
mod tests {
use super::report_compact_sources;
#[test]
fn report_compact_sources_examples() {
use std::fmt::Write;
#[derive(Debug, thiserror::Error)]
enum EvictionError {
#[error("cannot evict a remote layer")]
CannotEvictRemoteLayer,
#[error("stat failed")]
StatFailed(#[source] std::io::Error),
#[error("layer was no longer part of LayerMap")]
LayerNotFound(#[source] anyhow::Error),
}
let examples = [
(
line!(),
EvictionError::CannotEvictRemoteLayer,
"cannot evict a remote layer",
),
(
line!(),
EvictionError::StatFailed(std::io::ErrorKind::PermissionDenied.into()),
"stat failed: permission denied",
),
(
line!(),
EvictionError::LayerNotFound(anyhow::anyhow!("foobar")),
"layer was no longer part of LayerMap: foobar",
),
];
let mut s = String::new();
for (line, example, expected) in examples {
s.clear();
write!(s, "{}", report_compact_sources(&example)).expect("string grows");
assert_eq!(s, expected, "example on line {line}");
}
}
}

View File

@@ -1,8 +1,6 @@
/// Extensions to `std::fs` types. /// Extensions to `std::fs` types.
use std::{fs, io, path::Path}; use std::{fs, io, path::Path};
use anyhow::Context;
pub trait PathExt { pub trait PathExt {
/// Returns an error if `self` is not a directory. /// Returns an error if `self` is not a directory.
fn is_empty_dir(&self) -> io::Result<bool>; fn is_empty_dir(&self) -> io::Result<bool>;
@@ -17,36 +15,10 @@ where
} }
} }
pub async fn is_directory_empty(path: impl AsRef<Path>) -> anyhow::Result<bool> {
let mut dir = tokio::fs::read_dir(&path)
.await
.context(format!("read_dir({})", path.as_ref().display()))?;
Ok(dir.next_entry().await?.is_none())
}
pub fn ignore_not_found(e: io::Error) -> io::Result<()> {
if e.kind() == io::ErrorKind::NotFound {
Ok(())
} else {
Err(e)
}
}
pub fn ignore_absent_files<F>(fs_operation: F) -> io::Result<()>
where
F: Fn() -> io::Result<()>,
{
fs_operation().or_else(ignore_not_found)
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::path::PathBuf; use std::path::PathBuf;
use crate::fs_ext::is_directory_empty;
use super::ignore_absent_files;
#[test] #[test]
fn is_empty_dir() { fn is_empty_dir() {
use super::PathExt; use super::PathExt;
@@ -70,43 +42,4 @@ mod test {
std::fs::remove_file(&file_path).unwrap(); std::fs::remove_file(&file_path).unwrap();
assert!(file_path.is_empty_dir().is_err()); assert!(file_path.is_empty_dir().is_err());
} }
#[tokio::test]
async fn is_empty_dir_async() {
let dir = tempfile::tempdir().unwrap();
let dir_path = dir.path();
// test positive case
assert!(
is_directory_empty(dir_path).await.expect("test failure"),
"new tempdir should be empty"
);
// invoke on a file to ensure it returns an error
let file_path: PathBuf = dir_path.join("testfile");
let f = std::fs::File::create(&file_path).unwrap();
drop(f);
assert!(is_directory_empty(&file_path).await.is_err());
// do it again on a path, we know to be nonexistent
std::fs::remove_file(&file_path).unwrap();
assert!(is_directory_empty(file_path).await.is_err());
}
#[test]
fn ignore_absent_files_works() {
let dir = tempfile::tempdir().unwrap();
let dir_path = dir.path();
let file_path: PathBuf = dir_path.join("testfile");
ignore_absent_files(|| std::fs::remove_file(&file_path)).expect("should execute normally");
let f = std::fs::File::create(&file_path).unwrap();
drop(f);
ignore_absent_files(|| std::fs::remove_file(&file_path)).expect("should execute normally");
assert!(!file_path.exists());
}
} }

View File

@@ -1,17 +1,19 @@
use crate::auth::{Claims, JwtAuth}; use crate::auth::{Claims, JwtAuth};
use crate::http::error::{api_error_handler, route_error_handler, ApiError}; use crate::http::error::{api_error_handler, route_error_handler, ApiError};
use anyhow::Context; use anyhow::{anyhow, Context};
use hyper::header::{HeaderName, AUTHORIZATION}; use hyper::header::{HeaderName, AUTHORIZATION};
use hyper::http::HeaderValue; use hyper::http::HeaderValue;
use hyper::Method; use hyper::Method;
use hyper::{header::CONTENT_TYPE, Body, Request, Response}; use hyper::{header::CONTENT_TYPE, Body, Request, Response, Server};
use metrics::{register_int_counter, Encoder, IntCounter, TextEncoder}; use metrics::{register_int_counter, Encoder, IntCounter, TextEncoder};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use routerify::ext::RequestExt; use routerify::ext::RequestExt;
use routerify::{Middleware, RequestInfo, Router, RouterBuilder}; use routerify::{Middleware, RequestInfo, Router, RouterBuilder, RouterService};
use tokio::task::JoinError;
use tracing::{self, debug, info, info_span, warn, Instrument}; use tracing::{self, debug, info, info_span, warn, Instrument};
use std::future::Future; use std::future::Future;
use std::net::TcpListener;
use std::str::FromStr; use std::str::FromStr;
static SERVE_METRICS_COUNT: Lazy<IntCounter> = Lazy::new(|| { static SERVE_METRICS_COUNT: Lazy<IntCounter> = Lazy::new(|| {
@@ -147,140 +149,26 @@ impl Drop for RequestCancelled {
} }
async fn prometheus_metrics_handler(_req: Request<Body>) -> Result<Response<Body>, ApiError> { async fn prometheus_metrics_handler(_req: Request<Body>) -> Result<Response<Body>, ApiError> {
use bytes::{Bytes, BytesMut};
use std::io::Write as _;
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
SERVE_METRICS_COUNT.inc(); SERVE_METRICS_COUNT.inc();
/// An [`std::io::Write`] implementation on top of a channel sending [`bytes::Bytes`] chunks. let mut buffer = vec![];
struct ChannelWriter {
buffer: BytesMut,
tx: mpsc::Sender<std::io::Result<Bytes>>,
written: usize,
}
impl ChannelWriter {
fn new(buf_len: usize, tx: mpsc::Sender<std::io::Result<Bytes>>) -> Self {
assert_ne!(buf_len, 0);
ChannelWriter {
// split about half off the buffer from the start, because we flush depending on
// capacity. first flush will come sooner than without this, but now resizes will
// have better chance of picking up the "other" half. not guaranteed of course.
buffer: BytesMut::with_capacity(buf_len).split_off(buf_len / 2),
tx,
written: 0,
}
}
fn flush0(&mut self) -> std::io::Result<usize> {
let n = self.buffer.len();
if n == 0 {
return Ok(0);
}
tracing::trace!(n, "flushing");
let ready = self.buffer.split().freeze();
// not ideal to call from blocking code to block_on, but we are sure that this
// operation does not spawn_blocking other tasks
let res: Result<(), ()> = tokio::runtime::Handle::current().block_on(async {
self.tx.send(Ok(ready)).await.map_err(|_| ())?;
// throttle sending to allow reuse of our buffer in `write`.
self.tx.reserve().await.map_err(|_| ())?;
// now the response task has picked up the buffer and hopefully started
// sending it to the client.
Ok(())
});
if res.is_err() {
return Err(std::io::ErrorKind::BrokenPipe.into());
}
self.written += n;
Ok(n)
}
fn flushed_bytes(&self) -> usize {
self.written
}
}
impl std::io::Write for ChannelWriter {
fn write(&mut self, mut buf: &[u8]) -> std::io::Result<usize> {
let remaining = self.buffer.capacity() - self.buffer.len();
let out_of_space = remaining < buf.len();
let original_len = buf.len();
if out_of_space {
let can_still_fit = buf.len() - remaining;
self.buffer.extend_from_slice(&buf[..can_still_fit]);
buf = &buf[can_still_fit..];
self.flush0()?;
}
// assume that this will often under normal operation just move the pointer back to the
// beginning of allocation, because previous split off parts are already sent and
// dropped.
self.buffer.extend_from_slice(buf);
Ok(original_len)
}
fn flush(&mut self) -> std::io::Result<()> {
self.flush0().map(|_| ())
}
}
let started_at = std::time::Instant::now();
let (tx, rx) = mpsc::channel(1);
let body = Body::wrap_stream(ReceiverStream::new(rx));
let mut writer = ChannelWriter::new(128 * 1024, tx);
let encoder = TextEncoder::new(); let encoder = TextEncoder::new();
let metrics = tokio::task::spawn_blocking(move || {
// Currently we take a lot of mutexes while collecting metrics, so it's
// better to spawn a blocking task to avoid blocking the event loop.
metrics::gather()
})
.await
.map_err(|e: JoinError| ApiError::InternalServerError(e.into()))?;
encoder.encode(&metrics, &mut buffer).unwrap();
let response = Response::builder() let response = Response::builder()
.status(200) .status(200)
.header(CONTENT_TYPE, encoder.format_type()) .header(CONTENT_TYPE, encoder.format_type())
.body(body) .body(Body::from(buffer))
.unwrap(); .unwrap();
let span = info_span!("blocking");
tokio::task::spawn_blocking(move || {
let _span = span.entered();
let metrics = metrics::gather();
let res = encoder
.encode(&metrics, &mut writer)
.and_then(|_| writer.flush().map_err(|e| e.into()));
match res {
Ok(()) => {
tracing::info!(
bytes = writer.flushed_bytes(),
elapsed_ms = started_at.elapsed().as_millis(),
"responded /metrics"
);
}
Err(e) => {
tracing::warn!("failed to write out /metrics response: {e:#}");
// semantics of this error are quite... unclear. we want to error the stream out to
// abort the response to somehow notify the client that we failed.
//
// though, most likely the reason for failure is that the receiver is already gone.
drop(
writer
.tx
.blocking_send(Err(std::io::ErrorKind::BrokenPipe.into())),
);
}
}
});
Ok(response) Ok(response)
} }
@@ -460,6 +348,40 @@ pub fn check_permission_with(
} }
} }
///
/// Start listening for HTTP requests on given socket.
///
/// 'shutdown_future' can be used to stop. If the Future becomes
/// ready, we stop listening for new requests, and the function returns.
///
pub fn serve_thread_main<S>(
router_builder: RouterBuilder<hyper::Body, ApiError>,
listener: TcpListener,
shutdown_future: S,
) -> anyhow::Result<()>
where
S: Future<Output = ()> + Send + Sync,
{
info!("Starting an HTTP endpoint at {}", listener.local_addr()?);
// Create a Service from the router above to handle incoming requests.
let service = RouterService::new(router_builder.build().map_err(|err| anyhow!(err))?).unwrap();
// Enter a single-threaded tokio runtime bound to the current thread
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
let _guard = runtime.enter();
let server = Server::from_tcp(listener)?
.serve(service)
.with_graceful_shutdown(shutdown_future);
runtime.block_on(server)?;
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@@ -1,6 +1,5 @@
use hyper::{header, Body, Response, StatusCode}; use hyper::{header, Body, Response, StatusCode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::error::Error as StdError;
use thiserror::Error; use thiserror::Error;
use tracing::error; use tracing::error;
@@ -16,13 +15,13 @@ pub enum ApiError {
Unauthorized(String), Unauthorized(String),
#[error("NotFound: {0}")] #[error("NotFound: {0}")]
NotFound(Box<dyn StdError + Send + Sync + 'static>), NotFound(anyhow::Error),
#[error("Conflict: {0}")] #[error("Conflict: {0}")]
Conflict(String), Conflict(String),
#[error("Precondition failed: {0}")] #[error("Precondition failed: {0}")]
PreconditionFailed(Box<str>), PreconditionFailed(&'static str),
#[error(transparent)] #[error(transparent)]
InternalServerError(anyhow::Error), InternalServerError(anyhow::Error),

View File

@@ -14,7 +14,7 @@ pub async fn json_request<T: for<'de> Deserialize<'de>>(
.map_err(ApiError::BadRequest) .map_err(ApiError::BadRequest)
} }
/// Will be removed as part of <https://github.com/neondatabase/neon/issues/4282> /// 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>>( pub async fn json_request_or_empty_body<T: for<'de> Deserialize<'de>>(
request: &mut Request<Body>, request: &mut Request<Body>,
) -> Result<Option<T>, ApiError> { ) -> Result<Option<T>, ApiError> {

View File

@@ -1,7 +1,5 @@
use std::ffi::OsStr;
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
use anyhow::Context;
use hex::FromHex; use hex::FromHex;
use rand::Rng; use rand::Rng;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -215,18 +213,6 @@ pub struct TimelineId(Id);
id_newtype!(TimelineId); id_newtype!(TimelineId);
impl TryFrom<Option<&OsStr>> for TimelineId {
type Error = anyhow::Error;
fn try_from(value: Option<&OsStr>) -> Result<Self, Self::Error> {
value
.and_then(OsStr::to_str)
.unwrap_or_default()
.parse::<TimelineId>()
.with_context(|| format!("Could not parse timeline id from {:?}", value))
}
}
/// Neon Tenant Id represents identifiar of a particular tenant. /// Neon Tenant Id represents identifiar of a particular tenant.
/// Is used for distinguishing requests and data belonging to different users. /// Is used for distinguishing requests and data belonging to different users.
/// ///

View File

@@ -63,9 +63,6 @@ pub mod rate_limit;
/// Simple once-barrier and a guard which keeps barrier awaiting. /// Simple once-barrier and a guard which keeps barrier awaiting.
pub mod completion; pub mod completion;
/// Reporting utilities
pub mod error;
mod failpoint_macro_helpers { mod failpoint_macro_helpers {
/// use with fail::cfg("$name", "return(2000)") /// use with fail::cfg("$name", "return(2000)")
@@ -112,16 +109,10 @@ pub use failpoint_macro_helpers::failpoint_sleep_helper;
/// * building in docker (either in CI or locally) /// * building in docker (either in CI or locally)
/// ///
/// One thing to note is that .git is not available in docker (and it is bad to include it there). /// One thing to note is that .git is not available in docker (and it is bad to include it there).
/// When building locally, the `git_version` is used to query .git. When building on CI and docker, /// So everything becides docker build is covered by git_version crate, and docker uses a `GIT_VERSION` argument to get the value required.
/// we don't build the actual PR branch commits, but always a "phantom" would be merge commit to /// It takes variable from build process env and puts it to the rustc env. And then we can retrieve it here by using env! macro.
/// the target branch -- the actual PR commit from which we build from is supplied as GIT_VERSION /// Git version received from environment variable used as a fallback in git_version invocation.
/// environment variable. /// And to avoid running buildscript every recompilation, we use rerun-if-env-changed option.
///
/// We ended up with this compromise between phantom would be merge commits vs. pull request branch
/// heads due to old logs becoming more reliable (github could gc the phantom merge commit
/// anytime) in #4641.
///
/// To avoid running buildscript every recompilation, we use rerun-if-env-changed option.
/// So the build script will be run only when GIT_VERSION envvar has changed. /// So the build script will be run only when GIT_VERSION envvar has changed.
/// ///
/// Why not to use buildscript to get git commit sha directly without procmacro from different crate? /// Why not to use buildscript to get git commit sha directly without procmacro from different crate?
@@ -133,36 +124,25 @@ pub use failpoint_macro_helpers::failpoint_sleep_helper;
/// Note that with git_version prefix is `git:` and in case of git version from env its `git-env:`. /// Note that with git_version prefix is `git:` and in case of git version from env its `git-env:`.
/// ///
/// ############################################################################################# /// #############################################################################################
/// TODO this macro is not the way the library is intended to be used, see <https://github.com/neondatabase/neon/issues/1565> for details. /// TODO this macro is not the way the library is intended to be used, see https://github.com/neondatabase/neon/issues/1565 for details.
/// We use `cachepot` to reduce our current CI build times: <https://github.com/neondatabase/cloud/pull/1033#issuecomment-1100935036> /// We use `cachepot` to reduce our current CI build times: https://github.com/neondatabase/cloud/pull/1033#issuecomment-1100935036
/// Yet, it seems to ignore the GIT_VERSION env variable, passed to Docker build, even with build.rs that contains /// Yet, it seems to ignore the GIT_VERSION env variable, passed to Docker build, even with build.rs that contains
/// `println!("cargo:rerun-if-env-changed=GIT_VERSION");` code for cachepot cache invalidation. /// `println!("cargo:rerun-if-env-changed=GIT_VERSION");` code for cachepot cache invalidation.
/// The problem needs further investigation and regular `const` declaration instead of a macro. /// The problem needs further investigation and regular `const` declaration instead of a macro.
#[macro_export] #[macro_export]
macro_rules! project_git_version { macro_rules! project_git_version {
($const_identifier:ident) => { ($const_identifier:ident) => {
// this should try GIT_VERSION first only then git_version::git_version! const $const_identifier: &str = git_version::git_version!(
const $const_identifier: &::core::primitive::str = { prefix = "git:",
const __COMMIT_FROM_GIT: &::core::primitive::str = git_version::git_version! { fallback = concat!(
prefix = "", "git-env:",
fallback = "unknown", env!("GIT_VERSION", "Missing GIT_VERSION envvar")
args = ["--abbrev=40", "--always", "--dirty=-modified"] // always use full sha ),
}; args = ["--abbrev=40", "--always", "--dirty=-modified"] // always use full sha
);
const __ARG: &[&::core::primitive::str; 2] = &match ::core::option_env!("GIT_VERSION") {
::core::option::Option::Some(x) => ["git-env:", x],
::core::option::Option::None => ["git:", __COMMIT_FROM_GIT],
};
$crate::__const_format::concatcp!(__ARG[0], __ARG[1])
};
}; };
} }
/// Re-export for `project_git_version` macro
#[doc(hidden)]
pub use const_format as __const_format;
/// Same as `assert!`, but evaluated during compilation and gets optimized out in runtime. /// Same as `assert!`, but evaluated during compilation and gets optimized out in runtime.
#[macro_export] #[macro_export]
macro_rules! const_assert { macro_rules! const_assert {

View File

@@ -1,10 +1,9 @@
//! A module to create and read lock files. //! A module to create and read lock files.
//! //!
//! File locking is done using [`fcntl::flock`] exclusive locks. //! File locking is done using [`fcntl::flock`] exclusive locks.
//! The only consumer of this module is currently //! The only consumer of this module is currently [`pid_file`].
//! [`pid_file`](crate::pid_file). See the module-level comment //! See the module-level comment there for potential pitfalls
//! there for potential pitfalls with lock files that are used //! with lock files that are used to store PIDs (pidfiles).
//! to store PIDs (pidfiles).
use std::{ use std::{
fs, fs,
@@ -82,7 +81,7 @@ pub fn create_exclusive(lock_file_path: &Path) -> anyhow::Result<UnwrittenLockFi
} }
/// Returned by [`read_and_hold_lock_file`]. /// Returned by [`read_and_hold_lock_file`].
/// Check out the [`pid_file`](crate::pid_file) module for what the variants mean /// Check out the [`pid_file`] module for what the variants mean
/// and potential caveats if the lock files that are used to store PIDs. /// and potential caveats if the lock files that are used to store PIDs.
pub enum LockFileRead { pub enum LockFileRead {
/// No file exists at the given path. /// No file exists at the given path.

View File

@@ -84,7 +84,7 @@ pub fn init(
let r = r.with({ let r = r.with({
let log_layer = tracing_subscriber::fmt::layer() let log_layer = tracing_subscriber::fmt::layer()
.with_target(false) .with_target(false)
.with_ansi(false) .with_ansi(atty::is(atty::Stream::Stdout))
.with_writer(std::io::stdout); .with_writer(std::io::stdout);
let log_layer = match log_format { let log_layer = match log_format {
LogFormat::Json => log_layer.json().boxed(), LogFormat::Json => log_layer.json().boxed(),
@@ -112,7 +112,7 @@ pub fn init(
/// ///
/// When the return value is dropped, the hook is reverted to std default hook (prints to stderr). /// When the return value is dropped, the hook is reverted to std default hook (prints to stderr).
/// If the assumptions about the initialization order are not held, use /// If the assumptions about the initialization order are not held, use
/// [`TracingPanicHookGuard::forget`] but keep in mind, if tracing is stopped, then panics will be /// [`TracingPanicHookGuard::disarm`] but keep in mind, if tracing is stopped, then panics will be
/// lost. /// lost.
#[must_use] #[must_use]
pub fn replace_panic_hook_with_tracing_panic_hook() -> TracingPanicHookGuard { pub fn replace_panic_hook_with_tracing_panic_hook() -> TracingPanicHookGuard {

View File

@@ -1,5 +1,4 @@
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use std::io::Read;
use std::pin::Pin; use std::pin::Pin;
use std::{io, task}; use std::{io, task};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
@@ -76,34 +75,3 @@ impl<S: AsyncWrite + Unpin, R, W: FnMut(usize)> AsyncWrite for MeasuredStream<S,
self.project().stream.poll_shutdown(context) self.project().stream.poll_shutdown(context)
} }
} }
/// Wrapper for a reader that counts bytes read.
///
/// Similar to MeasuredStream but it's one way and it's sync
pub struct MeasuredReader<R: Read> {
inner: R,
byte_count: usize,
}
impl<R: Read> MeasuredReader<R> {
pub fn new(reader: R) -> Self {
Self {
inner: reader,
byte_count: 0,
}
}
pub fn get_byte_count(&self) -> usize {
self.byte_count
}
}
impl<R: Read> Read for MeasuredReader<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let result = self.inner.read(buf);
if let Ok(n_bytes) = result {
self.byte_count += n_bytes
}
result
}
}

View File

@@ -23,9 +23,9 @@ pub enum SeqWaitError {
/// Monotonically increasing value /// Monotonically increasing value
/// ///
/// It is handy to store some other fields under the same mutex in `SeqWait<S>` /// It is handy to store some other fields under the same mutex in SeqWait<S>
/// (e.g. store prev_record_lsn). So we allow SeqWait to be parametrized with /// (e.g. store prev_record_lsn). So we allow SeqWait to be parametrized with
/// any type that can expose counter. `V` is the type of exposed counter. /// any type that can expose counter. <V> is the type of exposed counter.
pub trait MonotonicCounter<V> { pub trait MonotonicCounter<V> {
/// Bump counter value and check that it goes forward /// Bump counter value and check that it goes forward
/// N.B.: new_val is an actual new value, not a difference. /// N.B.: new_val is an actual new value, not a difference.
@@ -90,7 +90,7 @@ impl<T: Ord> Eq for Waiter<T> {}
/// [`wait_for`]: SeqWait::wait_for /// [`wait_for`]: SeqWait::wait_for
/// [`advance`]: SeqWait::advance /// [`advance`]: SeqWait::advance
/// ///
/// `S` means Storage, `V` is type of counter that this storage exposes. /// <S> means Storage, <V> is type of counter that this storage exposes.
/// ///
pub struct SeqWait<S, V> pub struct SeqWait<S, V>
where where

View File

@@ -1,15 +1,8 @@
//! Assert that the current [`tracing::Span`] has a given set of fields. //! Assert that the current [`tracing::Span`] has a given set of fields.
//! //!
//! Can only produce meaningful positive results when tracing has been configured as in example.
//! Absence of `tracing_error::ErrorLayer` is not detected yet.
//!
//! `#[cfg(test)]` code will get a pass when using the `check_fields_present` macro in case tracing
//! is completly unconfigured.
//!
//! # Usage //! # Usage
//! //!
//! ```rust //! ```
//! # fn main() {
//! use tracing_subscriber::prelude::*; //! use tracing_subscriber::prelude::*;
//! let registry = tracing_subscriber::registry() //! let registry = tracing_subscriber::registry()
//! .with(tracing_error::ErrorLayer::default()); //! .with(tracing_error::ErrorLayer::default());
@@ -27,18 +20,23 @@
//! //!
//! use utils::tracing_span_assert::{check_fields_present, MultiNameExtractor}; //! use utils::tracing_span_assert::{check_fields_present, MultiNameExtractor};
//! let extractor = MultiNameExtractor::new("TestExtractor", ["test", "test_id"]); //! let extractor = MultiNameExtractor::new("TestExtractor", ["test", "test_id"]);
//! if let Err(missing) = check_fields_present!([&extractor]) { //! match check_fields_present([&extractor]) {
//! // if you copypaste this to a custom assert method, remember to add #[track_caller] //! Ok(()) => {},
//! // to get the "user" code location for the panic. //! Err(missing) => {
//! panic!("Missing fields: {missing:?}"); //! panic!("Missing fields: {:?}", missing.into_iter().map(|f| f.name() ).collect::<Vec<_>>());
//! }
//! } //! }
//! # }
//! ``` //! ```
//! //!
//! Recommended reading: <https://docs.rs/tracing-subscriber/0.3.16/tracing_subscriber/layer/index.html#per-layer-filtering> //! Recommended reading: https://docs.rs/tracing-subscriber/0.3.16/tracing_subscriber/layer/index.html#per-layer-filtering
//! //!
#[derive(Debug)] use std::{
collections::HashSet,
fmt::{self},
hash::{Hash, Hasher},
};
pub enum ExtractionResult { pub enum ExtractionResult {
Present, Present,
Absent, Absent,
@@ -73,101 +71,49 @@ impl<const L: usize> Extractor for MultiNameExtractor<L> {
} }
} }
/// Checks that the given extractors are satisfied with the current span hierarchy. struct MemoryIdentity<'a>(&'a dyn Extractor);
///
/// This should not be called directly, but used through [`check_fields_present`] which allows
/// `Summary::Unconfigured` only when the calling crate is being `#[cfg(test)]` as a conservative default.
#[doc(hidden)]
pub fn check_fields_present0<const L: usize>(
must_be_present: [&dyn Extractor; L],
) -> Result<Summary, Vec<&dyn Extractor>> {
let mut missing = must_be_present.into_iter().collect::<Vec<_>>();
let trace = tracing_error::SpanTrace::capture();
trace.with_spans(|md, _formatted_fields| {
// when trying to understand the inner workings of how does the matching work, note that
// this closure might be called zero times if the span is disabled. normally it is called
// once per span hierarchy level.
missing.retain(|extractor| match extractor.extract(md.fields()) {
ExtractionResult::Present => false,
ExtractionResult::Absent => true,
});
// continue walking up until we've found all missing impl<'a> MemoryIdentity<'a> {
!missing.is_empty() fn as_ptr(&self) -> *const () {
}); self.0 as *const _ as *const ()
if missing.is_empty() { }
Ok(Summary::FoundEverything) }
} else if !tracing_subscriber_configured() { impl<'a> PartialEq for MemoryIdentity<'a> {
Ok(Summary::Unconfigured) fn eq(&self, other: &Self) -> bool {
} else { self.as_ptr() == other.as_ptr()
// we can still hit here if a tracing subscriber has been configured but the ErrorLayer is }
// missing, which can be annoying. for this case, we could probably use }
// SpanTrace::status(). impl<'a> Eq for MemoryIdentity<'a> {}
// impl<'a> Hash for MemoryIdentity<'a> {
// another way to end up here is with RUST_LOG=pageserver=off while configuring the fn hash<H: Hasher>(&self, state: &mut H) {
// logging, though I guess in that case the SpanTrace::status() == EMPTY would be valid. self.as_ptr().hash(state);
// this case is covered by test `not_found_if_tracing_error_subscriber_has_wrong_filter`. }
Err(missing) }
impl<'a> fmt::Debug for MemoryIdentity<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:p}: {}", self.as_ptr(), self.0.name())
} }
} }
/// Checks that the given extractors are satisfied with the current span hierarchy. /// The extractor names passed as keys to [`new`].
/// pub fn check_fields_present<const L: usize>(
/// The macro is the preferred way of checking if fields exist while passing checks if a test does must_be_present: [&dyn Extractor; L],
/// not have tracing configured. ) -> Result<(), Vec<&dyn Extractor>> {
/// let mut missing: HashSet<MemoryIdentity> =
/// Why mangled name? Because #[macro_export] will expose it at utils::__check_fields_present. HashSet::from_iter(must_be_present.into_iter().map(|r| MemoryIdentity(r)));
/// However we can game a module namespaced macro for `use` purposes by re-exporting the let trace = tracing_error::SpanTrace::capture();
/// #[macro_export] exported name with an alias (below). trace.with_spans(|md, _formatted_fields| {
#[doc(hidden)] missing.retain(|extractor| match extractor.0.extract(md.fields()) {
#[macro_export] ExtractionResult::Present => false,
macro_rules! __check_fields_present { ExtractionResult::Absent => true,
($extractors:expr) => {{ });
{ !missing.is_empty() // continue walking up until we've found all missing
use $crate::tracing_span_assert::{check_fields_present0, Summary::*, Extractor};
match check_fields_present0($extractors) {
Ok(FoundEverything) => Ok(()),
Ok(Unconfigured) if cfg!(test) => {
// allow unconfigured in tests
Ok(())
},
Ok(Unconfigured) => {
panic!("utils::tracing_span_assert: outside of #[cfg(test)] expected tracing to be configured with tracing_error::ErrorLayer")
},
Err(missing) => Err(missing)
}
}
}}
}
pub use crate::__check_fields_present as check_fields_present;
/// Explanation for why the check was deemed ok.
///
/// Mainly useful for testing, or configuring per-crate behaviour as in with
/// [`check_fields_present`].
#[derive(Debug)]
pub enum Summary {
/// All extractors were found.
///
/// Should only happen when tracing is properly configured.
FoundEverything,
/// Tracing has not been configured at all. This is ok for tests running without tracing set
/// up.
Unconfigured,
}
fn tracing_subscriber_configured() -> bool {
let mut noop_configured = false;
tracing::dispatcher::get_default(|d| {
// it is possible that this closure will not be invoked, but the current implementation
// always invokes it
noop_configured = d.is::<tracing::subscriber::NoSubscriber>();
}); });
if missing.is_empty() {
!noop_configured Ok(())
} else {
Err(missing.into_iter().map(|mi| mi.0).collect())
}
} }
#[cfg(test)] #[cfg(test)]
@@ -177,36 +123,6 @@ mod tests {
use super::*; use super::*;
use std::{
collections::HashSet,
fmt::{self},
hash::{Hash, Hasher},
};
struct MemoryIdentity<'a>(&'a dyn Extractor);
impl<'a> MemoryIdentity<'a> {
fn as_ptr(&self) -> *const () {
self.0 as *const _ as *const ()
}
}
impl<'a> PartialEq for MemoryIdentity<'a> {
fn eq(&self, other: &Self) -> bool {
self.as_ptr() == other.as_ptr()
}
}
impl<'a> Eq for MemoryIdentity<'a> {}
impl<'a> Hash for MemoryIdentity<'a> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_ptr().hash(state);
}
}
impl<'a> fmt::Debug for MemoryIdentity<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:p}: {}", self.as_ptr(), self.0.name())
}
}
struct Setup { struct Setup {
_current_thread_subscriber_guard: tracing::subscriber::DefaultGuard, _current_thread_subscriber_guard: tracing::subscriber::DefaultGuard,
tenant_extractor: MultiNameExtractor<2>, tenant_extractor: MultiNameExtractor<2>,
@@ -243,8 +159,7 @@ mod tests {
let setup = setup_current_thread(); let setup = setup_current_thread();
let span = tracing::info_span!("root", tenant_id = "tenant-1", timeline_id = "timeline-1"); let span = tracing::info_span!("root", tenant_id = "tenant-1", timeline_id = "timeline-1");
let _guard = span.enter(); let _guard = span.enter();
let res = check_fields_present0([&setup.tenant_extractor, &setup.timeline_extractor]); check_fields_present([&setup.tenant_extractor, &setup.timeline_extractor]).unwrap();
assert!(matches!(res, Ok(Summary::FoundEverything)), "{res:?}");
} }
#[test] #[test]
@@ -252,8 +167,8 @@ mod tests {
let setup = setup_current_thread(); let setup = setup_current_thread();
let span = tracing::info_span!("root", timeline_id = "timeline-1"); let span = tracing::info_span!("root", timeline_id = "timeline-1");
let _guard = span.enter(); let _guard = span.enter();
let missing = check_fields_present0([&setup.tenant_extractor, &setup.timeline_extractor]) let missing =
.unwrap_err(); check_fields_present([&setup.tenant_extractor, &setup.timeline_extractor]).unwrap_err();
assert_missing(missing, vec![&setup.tenant_extractor]); assert_missing(missing, vec![&setup.tenant_extractor]);
} }
@@ -270,8 +185,7 @@ mod tests {
let span = tracing::info_span!("grandchild", timeline_id = "timeline-1"); let span = tracing::info_span!("grandchild", timeline_id = "timeline-1");
let _guard = span.enter(); let _guard = span.enter();
let res = check_fields_present0([&setup.tenant_extractor, &setup.timeline_extractor]); check_fields_present([&setup.tenant_extractor, &setup.timeline_extractor]).unwrap();
assert!(matches!(res, Ok(Summary::FoundEverything)), "{res:?}");
} }
#[test] #[test]
@@ -284,7 +198,7 @@ mod tests {
let span = tracing::info_span!("child", timeline_id = "timeline-1"); let span = tracing::info_span!("child", timeline_id = "timeline-1");
let _guard = span.enter(); let _guard = span.enter();
let missing = check_fields_present0([&setup.tenant_extractor]).unwrap_err(); let missing = check_fields_present([&setup.tenant_extractor]).unwrap_err();
assert_missing(missing, vec![&setup.tenant_extractor]); assert_missing(missing, vec![&setup.tenant_extractor]);
} }
@@ -293,8 +207,7 @@ mod tests {
let setup = setup_current_thread(); let setup = setup_current_thread();
let span = tracing::info_span!("root", tenant_id = "tenant-1", timeline_id = "timeline-1"); let span = tracing::info_span!("root", tenant_id = "tenant-1", timeline_id = "timeline-1");
let _guard = span.enter(); let _guard = span.enter();
let res = check_fields_present0([&setup.tenant_extractor]); check_fields_present([&setup.tenant_extractor]).unwrap();
assert!(matches!(res, Ok(Summary::FoundEverything)), "{res:?}");
} }
#[test] #[test]
@@ -310,8 +223,7 @@ mod tests {
let span = tracing::info_span!("grandchild", timeline_id = "timeline-1"); let span = tracing::info_span!("grandchild", timeline_id = "timeline-1");
let _guard = span.enter(); let _guard = span.enter();
let res = check_fields_present0([&setup.tenant_extractor]); check_fields_present([&setup.tenant_extractor]).unwrap();
assert!(matches!(res, Ok(Summary::FoundEverything)), "{res:?}");
} }
#[test] #[test]
@@ -319,7 +231,7 @@ mod tests {
let setup = setup_current_thread(); let setup = setup_current_thread();
let span = tracing::info_span!("root", timeline_id = "timeline-1"); let span = tracing::info_span!("root", timeline_id = "timeline-1");
let _guard = span.enter(); let _guard = span.enter();
let missing = check_fields_present0([&setup.tenant_extractor]).unwrap_err(); let missing = check_fields_present([&setup.tenant_extractor]).unwrap_err();
assert_missing(missing, vec![&setup.tenant_extractor]); assert_missing(missing, vec![&setup.tenant_extractor]);
} }
@@ -333,107 +245,43 @@ mod tests {
let span = tracing::info_span!("child", timeline_id = "timeline-1"); let span = tracing::info_span!("child", timeline_id = "timeline-1");
let _guard = span.enter(); let _guard = span.enter();
let missing = check_fields_present0([&setup.tenant_extractor]).unwrap_err(); let missing = check_fields_present([&setup.tenant_extractor]).unwrap_err();
assert_missing(missing, vec![&setup.tenant_extractor]); assert_missing(missing, vec![&setup.tenant_extractor]);
} }
#[test] #[test]
fn tracing_error_subscriber_not_set_up_straight_line() { fn tracing_error_subscriber_not_set_up() {
// no setup // no setup
let span = tracing::info_span!("foo", e = "some value"); let span = tracing::info_span!("foo", e = "some value");
let _guard = span.enter(); let _guard = span.enter();
let extractor = MultiNameExtractor::new("E", ["e"]); let extractor = MultiNameExtractor::new("E", ["e"]);
let res = check_fields_present0([&extractor]); let missing = check_fields_present([&extractor]).unwrap_err();
assert!(matches!(res, Ok(Summary::Unconfigured)), "{res:?}"); assert_missing(missing, vec![&extractor]);
// similarly for a not found key
let extractor = MultiNameExtractor::new("F", ["foobar"]);
let res = check_fields_present0([&extractor]);
assert!(matches!(res, Ok(Summary::Unconfigured)), "{res:?}");
} }
#[test] #[test]
fn tracing_error_subscriber_not_set_up_with_instrument() { #[should_panic]
// no setup fn panics_if_tracing_error_subscriber_has_wrong_filter() {
// demo a case where span entering is used to establish a parent child connection, but
// when we re-enter the subspan SpanTrace::with_spans iterates over nothing.
let span = tracing::info_span!("foo", e = "some value");
let _guard = span.enter();
let subspan = tracing::info_span!("bar", f = "foobar");
drop(_guard);
// normally this would work, but without any tracing-subscriber configured, both
// check_field_present find nothing
let _guard = subspan.enter();
let extractors: [&dyn Extractor; 2] = [
&MultiNameExtractor::new("E", ["e"]),
&MultiNameExtractor::new("F", ["f"]),
];
let res = check_fields_present0(extractors);
assert!(matches!(res, Ok(Summary::Unconfigured)), "{res:?}");
// similarly for a not found key
let extractor = MultiNameExtractor::new("G", ["g"]);
let res = check_fields_present0([&extractor]);
assert!(matches!(res, Ok(Summary::Unconfigured)), "{res:?}");
}
#[test]
fn tracing_subscriber_configured() {
// this will fail if any utils::logging::init callers appear, but let's hope they do not
// appear.
assert!(!super::tracing_subscriber_configured());
let _g = setup_current_thread();
assert!(super::tracing_subscriber_configured());
}
#[test]
fn not_found_when_disabled_by_filter() {
let r = tracing_subscriber::registry().with({ let r = tracing_subscriber::registry().with({
tracing_error::ErrorLayer::default().with_filter(tracing_subscriber::filter::filter_fn( tracing_error::ErrorLayer::default().with_filter(
|md| !(md.is_span() && *md.level() == tracing::Level::INFO), tracing_subscriber::filter::dynamic_filter_fn(|md, _| {
)) if md.is_span() && *md.level() == tracing::Level::INFO {
return false;
}
true
}),
)
}); });
let _guard = tracing::subscriber::set_default(r); let _guard = tracing::subscriber::set_default(r);
// this test is a rather tricky one, it has a number of possible outcomes depending on the
// execution order when executed with other tests even if no test sets the global default
// subscriber.
let span = tracing::info_span!("foo", e = "some value"); let span = tracing::info_span!("foo", e = "some value");
let _guard = span.enter(); let _guard = span.enter();
let extractors: [&dyn Extractor; 1] = [&MultiNameExtractor::new("E", ["e"])]; let extractor = MultiNameExtractor::new("E", ["e"]);
let missing = check_fields_present([&extractor]).unwrap_err();
if span.is_disabled() { assert_missing(missing, vec![&extractor]);
// the tests are running single threaded, or we got lucky and no other tests subscriber
// was got to register their per-CALLSITE::META interest between `set_default` and
// creation of the span, thus the filter got to apply and registered interest of Never,
// so the span was never created.
//
// as the span is disabled, no keys were recorded to it, leading check_fields_present0
// to find an error.
let missing = check_fields_present0(extractors).unwrap_err();
assert_missing(missing, vec![extractors[0]]);
} else {
// when the span is enabled, it is because some other test is running at the same time,
// and that tests registry has filters which are interested in our above span.
//
// because the span is now enabled, all keys will be found for it. the
// tracing_error::SpanTrace does not consider layer filters during the span hierarchy
// walk (SpanTrace::with_spans), nor is the SpanTrace::status a reliable indicator in
// this test-induced issue.
let res = check_fields_present0(extractors);
assert!(matches!(res, Ok(Summary::FoundEverything)), "{res:?}");
}
} }
} }

View File

@@ -12,7 +12,6 @@ testing = ["fail/failpoints"]
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
async-compression.workspace = true
async-stream.workspace = true async-stream.workspace = true
async-trait.workspace = true async-trait.workspace = true
byteorder.workspace = true byteorder.workspace = true
@@ -25,7 +24,6 @@ consumption_metrics.workspace = true
crc32c.workspace = true crc32c.workspace = true
crossbeam-utils.workspace = true crossbeam-utils.workspace = true
either.workspace = true either.workspace = true
flate2.workspace = true
fail.workspace = true fail.workspace = true
futures.workspace = true futures.workspace = true
git-version.workspace = true git-version.workspace = true
@@ -35,8 +33,6 @@ humantime-serde.workspace = true
hyper.workspace = true hyper.workspace = true
itertools.workspace = true itertools.workspace = true
nix.workspace = true nix.workspace = true
# hack to get the number of worker threads tokio uses
num_cpus = { version = "1.15" }
num-traits.workspace = true num-traits.workspace = true
once_cell.workspace = true once_cell.workspace = true
pin-project-lite.workspace = true pin-project-lite.workspace = true
@@ -84,7 +80,6 @@ strum_macros.workspace = true
criterion.workspace = true criterion.workspace = true
hex-literal.workspace = true hex-literal.workspace = true
tempfile.workspace = true tempfile.workspace = true
tokio = { workspace = true, features = ["process", "sync", "fs", "rt", "io-util", "time", "test-util"] }
[[bench]] [[bench]]
name = "bench_layer_map" name = "bench_layer_map"

View File

@@ -1,23 +1,22 @@
use pageserver::keyspace::{KeyPartitioning, KeySpace}; use pageserver::keyspace::{KeyPartitioning, KeySpace};
use pageserver::repository::Key; use pageserver::repository::Key;
use pageserver::tenant::layer_map::LayerMap; use pageserver::tenant::layer_map::LayerMap;
use pageserver::tenant::storage_layer::LayerFileName; use pageserver::tenant::storage_layer::{Layer, LayerDescriptor, LayerFileName};
use pageserver::tenant::storage_layer::PersistentLayerDesc;
use rand::prelude::{SeedableRng, SliceRandom, StdRng}; use rand::prelude::{SeedableRng, SliceRandom, StdRng};
use std::cmp::{max, min}; use std::cmp::{max, min};
use std::fs::File; use std::fs::File;
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use utils::id::{TenantId, TimelineId};
use utils::lsn::Lsn; use utils::lsn::Lsn;
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn build_layer_map(filename_dump: PathBuf) -> LayerMap { fn build_layer_map(filename_dump: PathBuf) -> LayerMap<LayerDescriptor> {
let mut layer_map = LayerMap::default(); let mut layer_map = LayerMap::<LayerDescriptor>::default();
let mut min_lsn = Lsn(u64::MAX); let mut min_lsn = Lsn(u64::MAX);
let mut max_lsn = Lsn(0); let mut max_lsn = Lsn(0);
@@ -28,13 +27,13 @@ fn build_layer_map(filename_dump: PathBuf) -> LayerMap {
for fname in filenames { for fname in filenames {
let fname = fname.unwrap(); let fname = fname.unwrap();
let fname = LayerFileName::from_str(&fname).unwrap(); let fname = LayerFileName::from_str(&fname).unwrap();
let layer = PersistentLayerDesc::from(fname); let layer = LayerDescriptor::from(fname);
let lsn_range = layer.get_lsn_range(); let lsn_range = layer.get_lsn_range();
min_lsn = min(min_lsn, lsn_range.start); min_lsn = min(min_lsn, lsn_range.start);
max_lsn = max(max_lsn, Lsn(lsn_range.end.0 - 1)); max_lsn = max(max_lsn, Lsn(lsn_range.end.0 - 1));
updates.insert_historic(layer); updates.insert_historic(layer.get_persistent_layer_desc(), Arc::new(layer));
} }
println!("min: {min_lsn}, max: {max_lsn}"); println!("min: {min_lsn}, max: {max_lsn}");
@@ -44,7 +43,7 @@ fn build_layer_map(filename_dump: PathBuf) -> LayerMap {
} }
/// Construct a layer map query pattern for benchmarks /// Construct a layer map query pattern for benchmarks
fn uniform_query_pattern(layer_map: &LayerMap) -> Vec<(Key, Lsn)> { fn uniform_query_pattern(layer_map: &LayerMap<LayerDescriptor>) -> Vec<(Key, Lsn)> {
// For each image layer we query one of the pages contained, at LSN right // For each image layer we query one of the pages contained, at LSN right
// before the image layer was created. This gives us a somewhat uniform // before the image layer was created. This gives us a somewhat uniform
// coverage of both the lsn and key space because image layers have // coverage of both the lsn and key space because image layers have
@@ -70,7 +69,7 @@ fn uniform_query_pattern(layer_map: &LayerMap) -> Vec<(Key, Lsn)> {
// Construct a partitioning for testing get_difficulty map when we // Construct a partitioning for testing get_difficulty map when we
// don't have an exact result of `collect_keyspace` to work with. // don't have an exact result of `collect_keyspace` to work with.
fn uniform_key_partitioning(layer_map: &LayerMap, _lsn: Lsn) -> KeyPartitioning { fn uniform_key_partitioning(layer_map: &LayerMap<LayerDescriptor>, _lsn: Lsn) -> KeyPartitioning {
let mut parts = Vec::new(); let mut parts = Vec::new();
// We add a partition boundary at the start of each image layer, // We add a partition boundary at the start of each image layer,
@@ -210,15 +209,13 @@ fn bench_sequential(c: &mut Criterion) {
for i in 0..100_000 { for i in 0..100_000 {
let i32 = (i as u32) % 100; let i32 = (i as u32) % 100;
let zero = Key::from_hex("000000000000000000000000000000000000").unwrap(); let zero = Key::from_hex("000000000000000000000000000000000000").unwrap();
let layer = PersistentLayerDesc::new_img( let layer = LayerDescriptor {
TenantId::generate(), key: zero.add(10 * i32)..zero.add(10 * i32 + 1),
TimelineId::generate(), lsn: Lsn(i)..Lsn(i + 1),
zero.add(10 * i32)..zero.add(10 * i32 + 1), is_incremental: false,
Lsn(i), short_id: format!("Layer {}", i),
false, };
0, updates.insert_historic(layer.get_persistent_layer_desc(), Arc::new(layer));
);
updates.insert_historic(layer);
} }
updates.flush(); updates.flush();
println!("Finished layer map init in {:?}", now.elapsed()); println!("Finished layer map init in {:?}", now.elapsed());

View File

@@ -13,7 +13,6 @@ clap = { workspace = true, features = ["string"] }
git-version.workspace = true git-version.workspace = true
pageserver = { path = ".." } pageserver = { path = ".." }
postgres_ffi.workspace = true postgres_ffi.workspace = true
tokio.workspace = true
utils.workspace = true utils.workspace = true
svg_fmt.workspace = true svg_fmt.workspace = true
workspace_hack.workspace = true workspace_hack.workspace = true

View File

@@ -7,10 +7,10 @@
//! - The y axis represents LSN, growing upwards. //! - The y axis represents LSN, growing upwards.
//! //!
//! Coordinates in both axis are compressed for better readability. //! Coordinates in both axis are compressed for better readability.
//! (see <https://medium.com/algorithms-digest/coordinate-compression-2fff95326fb>) //! (see https://medium.com/algorithms-digest/coordinate-compression-2fff95326fb)
//! //!
//! Example use: //! Example use:
//! ```bash //! ```
//! $ 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 pagectl draw-timeline-dir > out.svg //! $ grep "__" | cargo run --release --bin pagectl draw-timeline-dir > out.svg
//! $ firefox out.svg //! $ firefox out.svg
@@ -20,7 +20,7 @@
//! or from pageserver log files. //! or from pageserver log files.
//! //!
//! TODO Consider shipping this as a grafana panel plugin: //! TODO Consider shipping this as a grafana panel plugin:
//! <https://grafana.com/tutorials/build-a-panel-plugin/> //! https://grafana.com/tutorials/build-a-panel-plugin/
use anyhow::Result; use anyhow::Result;
use pageserver::repository::Key; use pageserver::repository::Key;
use std::cmp::Ordering; use std::cmp::Ordering;
@@ -117,8 +117,7 @@ pub fn main() -> Result<()> {
let mut lsn_diff = (lsn_end - lsn_start) as f32; let mut lsn_diff = (lsn_end - lsn_start) as f32;
let mut fill = Fill::None; let mut fill = Fill::None;
let mut ymargin = 0.05 * lsn_diff; // Height-dependent margin to disambiguate overlapping deltas let mut margin = 0.05 * lsn_diff; // Height-dependent margin to disambiguate overlapping deltas
let xmargin = 0.05; // Height-dependent margin to disambiguate overlapping deltas
let mut lsn_offset = 0.0; let mut lsn_offset = 0.0;
// Fill in and thicken rectangle if it's an // Fill in and thicken rectangle if it's an
@@ -129,7 +128,7 @@ pub fn main() -> Result<()> {
num_images += 1; num_images += 1;
lsn_diff = 0.3; lsn_diff = 0.3;
lsn_offset = -lsn_diff / 2.0; lsn_offset = -lsn_diff / 2.0;
ymargin = 0.05; margin = 0.05;
fill = Fill::Color(rgb(0, 0, 0)); fill = Fill::Color(rgb(0, 0, 0));
} }
Ordering::Greater => panic!("Invalid lsn range {}-{}", lsn_start, lsn_end), Ordering::Greater => panic!("Invalid lsn range {}-{}", lsn_start, lsn_end),
@@ -138,10 +137,10 @@ pub fn main() -> Result<()> {
println!( println!(
" {}", " {}",
rectangle( rectangle(
key_start as f32 + stretch * xmargin, key_start as f32 + stretch * margin,
stretch * (lsn_max as f32 - (lsn_end as f32 - ymargin - lsn_offset)), stretch * (lsn_max as f32 - (lsn_end as f32 - margin - lsn_offset)),
key_diff as f32 - stretch * 2.0 * xmargin, key_diff as f32 - stretch * 2.0 * margin,
stretch * (lsn_diff - 2.0 * ymargin) stretch * (lsn_diff - 2.0 * margin)
) )
.fill(fill) .fill(fill)
.stroke(Stroke::Color(rgb(0, 0, 0), 0.1)) .stroke(Stroke::Color(rgb(0, 0, 0), 0.1))

View File

@@ -95,7 +95,7 @@ pub(crate) fn parse_filename(name: &str) -> Option<LayerFile> {
} }
// Finds the max_holes largest holes, ignoring any that are smaller than MIN_HOLE_LENGTH" // Finds the max_holes largest holes, ignoring any that are smaller than MIN_HOLE_LENGTH"
async fn get_holes(path: &Path, max_holes: usize) -> Result<Vec<Hole>> { fn get_holes(path: &Path, max_holes: usize) -> Result<Vec<Hole>> {
let file = FileBlockReader::new(VirtualFile::open(path)?); let file = FileBlockReader::new(VirtualFile::open(path)?);
let summary_blk = file.read_blk(0)?; let summary_blk = file.read_blk(0)?;
let actual_summary = Summary::des_prefix(summary_blk.as_ref())?; let actual_summary = Summary::des_prefix(summary_blk.as_ref())?;
@@ -129,7 +129,7 @@ async fn get_holes(path: &Path, max_holes: usize) -> Result<Vec<Hole>> {
Ok(holes) Ok(holes)
} }
pub(crate) async fn main(cmd: &AnalyzeLayerMapCmd) -> Result<()> { pub(crate) fn main(cmd: &AnalyzeLayerMapCmd) -> Result<()> {
let storage_path = &cmd.path; let storage_path = &cmd.path;
let max_holes = cmd.max_holes.unwrap_or(DEFAULT_MAX_HOLES); let max_holes = cmd.max_holes.unwrap_or(DEFAULT_MAX_HOLES);
@@ -160,7 +160,7 @@ pub(crate) async fn main(cmd: &AnalyzeLayerMapCmd) -> Result<()> {
parse_filename(&layer.file_name().into_string().unwrap()) parse_filename(&layer.file_name().into_string().unwrap())
{ {
if layer_file.is_delta { if layer_file.is_delta {
layer_file.holes = get_holes(&layer.path(), max_holes).await?; layer_file.holes = get_holes(&layer.path(), max_holes)?;
n_deltas += 1; n_deltas += 1;
} }
layers.push(layer_file); layers.push(layer_file);

View File

@@ -43,7 +43,8 @@ pub(crate) enum LayerCmd {
}, },
} }
async fn read_delta_file(path: impl AsRef<Path>) -> Result<()> { fn read_delta_file(path: impl AsRef<Path>) -> Result<()> {
use pageserver::tenant::blob_io::BlobCursor;
use pageserver::tenant::block_io::BlockReader; use pageserver::tenant::block_io::BlockReader;
let path = path.as_ref(); let path = path.as_ref();
@@ -77,7 +78,7 @@ async fn read_delta_file(path: impl AsRef<Path>) -> Result<()> {
Ok(()) Ok(())
} }
pub(crate) async fn main(cmd: &LayerCmd) -> Result<()> { pub(crate) fn main(cmd: &LayerCmd) -> Result<()> {
match cmd { match cmd {
LayerCmd::List { path } => { LayerCmd::List { path } => {
for tenant in fs::read_dir(path.join("tenants"))? { for tenant in fs::read_dir(path.join("tenants"))? {
@@ -152,7 +153,7 @@ pub(crate) async fn main(cmd: &LayerCmd) -> Result<()> {
); );
if layer_file.is_delta { if layer_file.is_delta {
read_delta_file(layer.path()).await?; read_delta_file(layer.path())?;
} else { } else {
anyhow::bail!("not supported yet :("); anyhow::bail!("not supported yet :(");
} }

View File

@@ -72,13 +72,12 @@ struct AnalyzeLayerMapCmd {
max_holes: Option<usize>, max_holes: Option<usize>,
} }
#[tokio::main] fn main() -> anyhow::Result<()> {
async fn main() -> anyhow::Result<()> {
let cli = CliOpts::parse(); let cli = CliOpts::parse();
match cli.command { match cli.command {
Commands::Layer(cmd) => { Commands::Layer(cmd) => {
layers::main(&cmd).await?; layers::main(&cmd)?;
} }
Commands::Metadata(cmd) => { Commands::Metadata(cmd) => {
handle_metadata(&cmd)?; handle_metadata(&cmd)?;
@@ -87,7 +86,7 @@ async fn main() -> anyhow::Result<()> {
draw_timeline_dir::main()?; draw_timeline_dir::main()?;
} }
Commands::AnalyzeLayerMap(cmd) => { Commands::AnalyzeLayerMap(cmd) => {
layer_map_analyzer::main(&cmd).await?; layer_map_analyzer::main(&cmd)?;
} }
Commands::PrintLayerFile(cmd) => { Commands::PrintLayerFile(cmd) => {
if let Err(e) = read_pg_control_file(&cmd.path) { if let Err(e) = read_pg_control_file(&cmd.path) {
@@ -95,7 +94,7 @@ async fn main() -> anyhow::Result<()> {
"Failed to read input file as a pg control one: {e:#}\n\ "Failed to read input file as a pg control one: {e:#}\n\
Attempting to read it as layer file" Attempting to read it as layer file"
); );
print_layerfile(&cmd.path).await?; print_layerfile(&cmd.path)?;
} }
} }
}; };
@@ -114,12 +113,12 @@ fn read_pg_control_file(control_file_path: &Path) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
async fn print_layerfile(path: &Path) -> anyhow::Result<()> { fn print_layerfile(path: &Path) -> anyhow::Result<()> {
// Basic initialization of things that don't change after startup // Basic initialization of things that don't change after startup
virtual_file::init(10); virtual_file::init(10);
page_cache::init(100); page_cache::init(100);
let ctx = RequestContext::new(TaskKind::DebugTool, DownloadBehavior::Error); let ctx = RequestContext::new(TaskKind::DebugTool, DownloadBehavior::Error);
dump_layerfile_from_path(path, true, &ctx).await dump_layerfile_from_path(path, true, &ctx)
} }
fn handle_metadata( fn handle_metadata(

View File

@@ -19,6 +19,12 @@ use tokio::io;
use tokio::io::AsyncWrite; use tokio::io::AsyncWrite;
use tracing::*; use tracing::*;
/// NB: This relies on a modified version of tokio_tar that does *not* write the
/// end-of-archive marker (1024 zero bytes), when the Builder struct is dropped
/// without explicitly calling 'finish' or 'into_inner'!
///
/// See https://github.com/neondatabase/tokio-tar/pull/1
///
use tokio_tar::{Builder, EntryType, Header}; use tokio_tar::{Builder, EntryType, Header};
use crate::context::RequestContext; use crate::context::RequestContext;

View File

@@ -396,8 +396,8 @@ fn start_pageserver(
let guard = scopeguard::guard_on_success((), |_| tracing::info!("Cancelled before initial logical sizes completed")); let guard = scopeguard::guard_on_success((), |_| tracing::info!("Cancelled before initial logical sizes completed"));
let init_sizes_done = match tokio::time::timeout(timeout, &mut init_sizes_done).await { let init_sizes_done = tokio::select! {
Ok(_) => { _ = &mut init_sizes_done => {
let now = std::time::Instant::now(); let now = std::time::Instant::now();
tracing::info!( tracing::info!(
from_init_done_millis = (now - init_done).as_millis(), from_init_done_millis = (now - init_done).as_millis(),
@@ -406,7 +406,7 @@ fn start_pageserver(
); );
None None
} }
Err(_) => { _ = tokio::time::sleep(timeout) => {
tracing::info!( tracing::info!(
timeout_millis = timeout.as_millis(), timeout_millis = timeout.as_millis(),
"Initial logical size timeout elapsed; starting background jobs" "Initial logical size timeout elapsed; starting background jobs"
@@ -495,50 +495,50 @@ fn start_pageserver(
Ok(()) Ok(())
}, },
); );
}
if let Some(metric_collection_endpoint) = &conf.metric_collection_endpoint { if let Some(metric_collection_endpoint) = &conf.metric_collection_endpoint {
let background_jobs_barrier = background_jobs_barrier; let background_jobs_barrier = background_jobs_barrier;
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.
// The actual size calculation does need downloads, and // The actual size calculation does need downloads, and
// creates a child context with the right DownloadBehavior. // creates a child context with the right DownloadBehavior.
DownloadBehavior::Error, DownloadBehavior::Error,
); );
task_mgr::spawn( task_mgr::spawn(
crate::BACKGROUND_RUNTIME.handle(), MGMT_REQUEST_RUNTIME.handle(),
TaskKind::MetricsCollection, TaskKind::MetricsCollection,
None, None,
None, None,
"consumption metrics collection", "consumption metrics collection",
true, true,
async move { async move {
// first wait until background jobs are cleared to launch. // first wait until background jobs are cleared to launch.
// //
// this is because we only process active tenants and timelines, and the // this is because we only process active tenants and timelines, and the
// Timeline::get_current_logical_size will spawn the logical size calculation, // Timeline::get_current_logical_size will spawn the logical size calculation,
// which will not be rate-limited. // which will not be rate-limited.
let cancel = task_mgr::shutdown_token(); let cancel = task_mgr::shutdown_token();
tokio::select! { tokio::select! {
_ = cancel.cancelled() => { return Ok(()); }, _ = cancel.cancelled() => { return Ok(()); },
_ = background_jobs_barrier.wait() => {} _ = background_jobs_barrier.wait() => {}
}; };
pageserver::consumption_metrics::collect_metrics( pageserver::consumption_metrics::collect_metrics(
metric_collection_endpoint, metric_collection_endpoint,
conf.metric_collection_interval, conf.metric_collection_interval,
conf.cached_metric_collection_interval, conf.cached_metric_collection_interval,
conf.synthetic_size_calculation_interval, conf.synthetic_size_calculation_interval,
conf.id, conf.id,
metrics_ctx, metrics_ctx,
) )
.instrument(info_span!("metrics_collection")) .instrument(info_span!("metrics_collection"))
.await?; .await?;
Ok(()) Ok(())
}, },
); );
}
} }
// Spawn a task to listen for libpq connections. It will spawn further tasks // Spawn a task to listen for libpq connections. It will spawn further tasks

View File

@@ -33,8 +33,7 @@ use crate::tenant::config::TenantConf;
use crate::tenant::config::TenantConfOpt; use crate::tenant::config::TenantConfOpt;
use crate::tenant::{TENANT_ATTACHING_MARKER_FILENAME, TIMELINES_SEGMENT_NAME}; use crate::tenant::{TENANT_ATTACHING_MARKER_FILENAME, TIMELINES_SEGMENT_NAME};
use crate::{ use crate::{
IGNORED_TENANT_FILE_NAME, METADATA_FILE_NAME, TENANT_CONFIG_NAME, TIMELINE_DELETE_MARK_SUFFIX, IGNORED_TENANT_FILE_NAME, METADATA_FILE_NAME, TENANT_CONFIG_NAME, TIMELINE_UNINIT_MARK_SUFFIX,
TIMELINE_UNINIT_MARK_SUFFIX,
}; };
pub mod defaults { pub mod defaults {
@@ -97,12 +96,12 @@ pub mod defaults {
#background_task_maximum_delay = '{DEFAULT_BACKGROUND_TASK_MAXIMUM_DELAY}' #background_task_maximum_delay = '{DEFAULT_BACKGROUND_TASK_MAXIMUM_DELAY}'
[tenant_config] # [tenant_config]
#checkpoint_distance = {DEFAULT_CHECKPOINT_DISTANCE} # in bytes #checkpoint_distance = {DEFAULT_CHECKPOINT_DISTANCE} # in bytes
#checkpoint_timeout = {DEFAULT_CHECKPOINT_TIMEOUT} #checkpoint_timeout = {DEFAULT_CHECKPOINT_TIMEOUT}
#compaction_target_size = {DEFAULT_COMPACTION_TARGET_SIZE} # in bytes #compaction_target_size = {DEFAULT_COMPACTION_TARGET_SIZE} # in bytes
#compaction_period = '{DEFAULT_COMPACTION_PERIOD}' #compaction_period = '{DEFAULT_COMPACTION_PERIOD}'
#compaction_threshold = {DEFAULT_COMPACTION_THRESHOLD} #compaction_threshold = '{DEFAULT_COMPACTION_THRESHOLD}'
#gc_period = '{DEFAULT_GC_PERIOD}' #gc_period = '{DEFAULT_GC_PERIOD}'
#gc_horizon = {DEFAULT_GC_HORIZON} #gc_horizon = {DEFAULT_GC_HORIZON}
@@ -112,8 +111,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 #gc_feedback = false
# [remote_storage]
[remote_storage]
"### "###
); );
@@ -172,13 +170,11 @@ pub struct PageServerConf {
pub log_format: LogFormat, pub log_format: LogFormat,
/// Number of concurrent [`Tenant::gather_size_inputs`](crate::tenant::Tenant::gather_size_inputs) allowed. /// Number of concurrent [`Tenant::gather_size_inputs`] allowed.
pub concurrent_tenant_size_logical_size_queries: ConfigurableSemaphore, pub concurrent_tenant_size_logical_size_queries: ConfigurableSemaphore,
/// Limit of concurrent [`Tenant::gather_size_inputs`] issued by module `eviction_task`. /// Limit of concurrent [`Tenant::gather_size_inputs`] issued by module `eviction_task`.
/// The number of permits is the same as `concurrent_tenant_size_logical_size_queries`. /// The number of permits is the same as `concurrent_tenant_size_logical_size_queries`.
/// See the comment in `eviction_task` for details. /// See the comment in `eviction_task` for details.
///
/// [`Tenant::gather_size_inputs`]: crate::tenant::Tenant::gather_size_inputs
pub eviction_task_immitated_concurrent_logical_size_queries: ConfigurableSemaphore, pub eviction_task_immitated_concurrent_logical_size_queries: ConfigurableSemaphore,
// How often to collect metrics and send them to the metrics endpoint. // How often to collect metrics and send them to the metrics endpoint.
@@ -573,21 +569,21 @@ impl PageServerConf {
.join(TENANT_ATTACHING_MARKER_FILENAME) .join(TENANT_ATTACHING_MARKER_FILENAME)
} }
pub fn tenant_ignore_mark_file_path(&self, tenant_id: &TenantId) -> PathBuf { pub fn tenant_ignore_mark_file_path(&self, tenant_id: TenantId) -> PathBuf {
self.tenant_path(tenant_id).join(IGNORED_TENANT_FILE_NAME) self.tenant_path(&tenant_id).join(IGNORED_TENANT_FILE_NAME)
} }
/// Points to a place in pageserver's local directory, /// Points to a place in pageserver's local directory,
/// where certain tenant's tenantconf file should be located. /// where certain tenant's tenantconf file should be located.
pub fn tenant_config_path(&self, tenant_id: &TenantId) -> PathBuf { pub fn tenant_config_path(&self, tenant_id: TenantId) -> PathBuf {
self.tenant_path(tenant_id).join(TENANT_CONFIG_NAME) self.tenant_path(&tenant_id).join(TENANT_CONFIG_NAME)
} }
pub fn timelines_path(&self, tenant_id: &TenantId) -> PathBuf { pub fn timelines_path(&self, tenant_id: &TenantId) -> PathBuf {
self.tenant_path(tenant_id).join(TIMELINES_SEGMENT_NAME) self.tenant_path(tenant_id).join(TIMELINES_SEGMENT_NAME)
} }
pub fn timeline_path(&self, tenant_id: &TenantId, timeline_id: &TimelineId) -> PathBuf { pub fn timeline_path(&self, timeline_id: &TimelineId, tenant_id: &TenantId) -> PathBuf {
self.timelines_path(tenant_id).join(timeline_id.to_string()) self.timelines_path(tenant_id).join(timeline_id.to_string())
} }
@@ -597,22 +593,11 @@ impl PageServerConf {
timeline_id: TimelineId, timeline_id: TimelineId,
) -> PathBuf { ) -> PathBuf {
path_with_suffix_extension( path_with_suffix_extension(
self.timeline_path(&tenant_id, &timeline_id), self.timeline_path(&timeline_id, &tenant_id),
TIMELINE_UNINIT_MARK_SUFFIX, TIMELINE_UNINIT_MARK_SUFFIX,
) )
} }
pub fn timeline_delete_mark_file_path(
&self,
tenant_id: TenantId,
timeline_id: TimelineId,
) -> PathBuf {
path_with_suffix_extension(
self.timeline_path(&tenant_id, &timeline_id),
TIMELINE_DELETE_MARK_SUFFIX,
)
}
pub fn traces_path(&self) -> PathBuf { pub fn traces_path(&self) -> PathBuf {
self.workdir.join("traces") self.workdir.join("traces")
} }
@@ -631,8 +616,8 @@ impl PageServerConf {
/// Points to a place in pageserver's local directory, /// Points to a place in pageserver's local directory,
/// where certain timeline's metadata file should be located. /// where certain timeline's metadata file should be located.
pub fn metadata_path(&self, tenant_id: &TenantId, timeline_id: &TimelineId) -> PathBuf { pub fn metadata_path(&self, timeline_id: TimelineId, tenant_id: TenantId) -> PathBuf {
self.timeline_path(tenant_id, timeline_id) self.timeline_path(&timeline_id, &tenant_id)
.join(METADATA_FILE_NAME) .join(METADATA_FILE_NAME)
} }
@@ -1007,8 +992,6 @@ impl ConfigurableSemaphore {
/// Require a non-zero initial permits, because using permits == 0 is a crude way to disable a /// Require a non-zero initial permits, because using permits == 0 is a crude way to disable a
/// feature such as [`Tenant::gather_size_inputs`]. Otherwise any semaphore using future will /// feature such as [`Tenant::gather_size_inputs`]. Otherwise any semaphore using future will
/// behave like [`futures::future::pending`], just waiting until new permits are added. /// behave like [`futures::future::pending`], just waiting until new permits are added.
///
/// [`Tenant::gather_size_inputs`]: crate::tenant::Tenant::gather_size_inputs
pub fn new(initial_permits: NonZeroUsize) -> Self { pub fn new(initial_permits: NonZeroUsize) -> Self {
ConfigurableSemaphore { ConfigurableSemaphore {
initial_permits, initial_permits,

View File

@@ -7,7 +7,7 @@ use crate::context::{DownloadBehavior, RequestContext};
use crate::task_mgr::{self, TaskKind, BACKGROUND_RUNTIME}; use crate::task_mgr::{self, TaskKind, BACKGROUND_RUNTIME};
use crate::tenant::{mgr, LogicalSizeCalculationCause}; use crate::tenant::{mgr, LogicalSizeCalculationCause};
use anyhow; use anyhow;
use chrono::{DateTime, Utc}; use chrono::Utc;
use consumption_metrics::{idempotency_key, Event, EventChunk, EventType, CHUNK_SIZE}; use consumption_metrics::{idempotency_key, Event, EventChunk, EventType, CHUNK_SIZE};
use pageserver_api::models::TenantState; use pageserver_api::models::TenantState;
use reqwest::Url; use reqwest::Url;
@@ -18,7 +18,11 @@ use std::time::Duration;
use tracing::*; use tracing::*;
use utils::id::{NodeId, TenantId, TimelineId}; use utils::id::{NodeId, TenantId, TimelineId};
const DEFAULT_HTTP_REPORTING_TIMEOUT: Duration = Duration::from_secs(60); const WRITTEN_SIZE: &str = "written_size";
const SYNTHETIC_STORAGE_SIZE: &str = "synthetic_storage_size";
const RESIDENT_SIZE: &str = "resident_size";
const REMOTE_STORAGE_SIZE: &str = "remote_storage_size";
const TIMELINE_LOGICAL_SIZE: &str = "timeline_logical_size";
#[serde_as] #[serde_as]
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
@@ -38,121 +42,6 @@ pub struct PageserverConsumptionMetricsKey {
pub metric: &'static str, pub metric: &'static str,
} }
impl PageserverConsumptionMetricsKey {
const fn absolute_values(self) -> AbsoluteValueFactory {
AbsoluteValueFactory(self)
}
const fn incremental_values(self) -> IncrementalValueFactory {
IncrementalValueFactory(self)
}
}
/// Helper type which each individual metric kind can return to produce only absolute values.
struct AbsoluteValueFactory(PageserverConsumptionMetricsKey);
impl AbsoluteValueFactory {
fn now(self, val: u64) -> (PageserverConsumptionMetricsKey, (EventType, u64)) {
let key = self.0;
let time = Utc::now();
(key, (EventType::Absolute { time }, val))
}
}
/// Helper type which each individual metric kind can return to produce only incremental values.
struct IncrementalValueFactory(PageserverConsumptionMetricsKey);
impl IncrementalValueFactory {
#[allow(clippy::wrong_self_convention)]
fn from_previous_up_to(
self,
prev_end: DateTime<Utc>,
up_to: DateTime<Utc>,
val: u64,
) -> (PageserverConsumptionMetricsKey, (EventType, u64)) {
let key = self.0;
// cannot assert prev_end < up_to because these are realtime clock based
(
key,
(
EventType::Incremental {
start_time: prev_end,
stop_time: up_to,
},
val,
),
)
}
fn key(&self) -> &PageserverConsumptionMetricsKey {
&self.0
}
}
// the static part of a PageserverConsumptionMetricsKey
impl PageserverConsumptionMetricsKey {
const fn written_size(tenant_id: TenantId, timeline_id: TimelineId) -> AbsoluteValueFactory {
PageserverConsumptionMetricsKey {
tenant_id,
timeline_id: Some(timeline_id),
metric: "written_size",
}
.absolute_values()
}
/// Values will be the difference of the latest written_size (last_record_lsn) to what we
/// previously sent.
const fn written_size_delta(
tenant_id: TenantId,
timeline_id: TimelineId,
) -> IncrementalValueFactory {
PageserverConsumptionMetricsKey {
tenant_id,
timeline_id: Some(timeline_id),
metric: "written_size_bytes_delta",
}
.incremental_values()
}
const fn timeline_logical_size(
tenant_id: TenantId,
timeline_id: TimelineId,
) -> AbsoluteValueFactory {
PageserverConsumptionMetricsKey {
tenant_id,
timeline_id: Some(timeline_id),
metric: "timeline_logical_size",
}
.absolute_values()
}
const fn remote_storage_size(tenant_id: TenantId) -> AbsoluteValueFactory {
PageserverConsumptionMetricsKey {
tenant_id,
timeline_id: None,
metric: "remote_storage_size",
}
.absolute_values()
}
const fn resident_size(tenant_id: TenantId) -> AbsoluteValueFactory {
PageserverConsumptionMetricsKey {
tenant_id,
timeline_id: None,
metric: "resident_size",
}
.absolute_values()
}
const fn synthetic_size(tenant_id: TenantId) -> AbsoluteValueFactory {
PageserverConsumptionMetricsKey {
tenant_id,
timeline_id: None,
metric: "synthetic_storage_size",
}
.absolute_values()
}
}
/// Main thread that serves metrics collection /// Main thread that serves metrics collection
pub async fn collect_metrics( pub async fn collect_metrics(
metric_collection_endpoint: &Url, metric_collection_endpoint: &Url,
@@ -184,11 +73,8 @@ pub async fn collect_metrics(
); );
// define client here to reuse it for all requests // define client here to reuse it for all requests
let client = reqwest::ClientBuilder::new() let client = reqwest::Client::new();
.timeout(DEFAULT_HTTP_REPORTING_TIMEOUT) let mut cached_metrics: HashMap<PageserverConsumptionMetricsKey, u64> = HashMap::new();
.build()
.expect("Failed to create http client with timeout");
let mut cached_metrics = HashMap::new();
let mut prev_iteration_time: std::time::Instant = std::time::Instant::now(); let mut prev_iteration_time: std::time::Instant = std::time::Instant::now();
loop { loop {
@@ -197,7 +83,7 @@ pub async fn collect_metrics(
info!("collect_metrics received cancellation request"); info!("collect_metrics received cancellation request");
return Ok(()); return Ok(());
}, },
tick_at = ticker.tick() => { _ = ticker.tick() => {
// send cached metrics every cached_metric_collection_interval // send cached metrics every cached_metric_collection_interval
let send_cached = prev_iteration_time.elapsed() >= cached_metric_collection_interval; let send_cached = prev_iteration_time.elapsed() >= cached_metric_collection_interval;
@@ -207,12 +93,6 @@ pub async fn collect_metrics(
} }
collect_metrics_iteration(&client, &mut cached_metrics, metric_collection_endpoint, node_id, &ctx, send_cached).await; collect_metrics_iteration(&client, &mut cached_metrics, metric_collection_endpoint, node_id, &ctx, send_cached).await;
crate::tenant::tasks::warn_when_period_overrun(
tick_at.elapsed(),
metric_collection_interval,
"consumption_metrics_collect_metrics",
);
} }
} }
} }
@@ -230,13 +110,13 @@ pub async fn collect_metrics(
/// - refactor this function (chunking+sending part) to reuse it in proxy module; /// - refactor this function (chunking+sending part) to reuse it in proxy module;
pub async fn collect_metrics_iteration( pub async fn collect_metrics_iteration(
client: &reqwest::Client, client: &reqwest::Client,
cached_metrics: &mut HashMap<PageserverConsumptionMetricsKey, (EventType, u64)>, cached_metrics: &mut HashMap<PageserverConsumptionMetricsKey, u64>,
metric_collection_endpoint: &reqwest::Url, metric_collection_endpoint: &reqwest::Url,
node_id: NodeId, node_id: NodeId,
ctx: &RequestContext, ctx: &RequestContext,
send_cached: bool, send_cached: bool,
) { ) {
let mut current_metrics: Vec<(PageserverConsumptionMetricsKey, (EventType, u64))> = Vec::new(); let mut current_metrics: Vec<(PageserverConsumptionMetricsKey, u64)> = Vec::new();
trace!( trace!(
"starting collect_metrics_iteration. metric_collection_endpoint: {}", "starting collect_metrics_iteration. metric_collection_endpoint: {}",
metric_collection_endpoint metric_collection_endpoint
@@ -275,80 +155,27 @@ pub async fn collect_metrics_iteration(
if timeline.is_active() { if timeline.is_active() {
let timeline_written_size = u64::from(timeline.get_last_record_lsn()); let timeline_written_size = u64::from(timeline.get_last_record_lsn());
let (key, written_size_now) = current_metrics.push((
PageserverConsumptionMetricsKey::written_size(tenant_id, timeline.timeline_id) PageserverConsumptionMetricsKey {
.now(timeline_written_size); tenant_id,
timeline_id: Some(timeline.timeline_id),
// last_record_lsn can only go up, right now at least, TODO: #2592 or related metric: WRITTEN_SIZE,
// features might change this.
let written_size_delta_key = PageserverConsumptionMetricsKey::written_size_delta(
tenant_id,
timeline.timeline_id,
);
// use this when available, because in a stream of incremental values, it will be
// accurate where as when last_record_lsn stops moving, we will only cache the last
// one of those.
let last_stop_time =
cached_metrics
.get(written_size_delta_key.key())
.map(|(until, _val)| {
until
.incremental_timerange()
.expect("never create EventType::Absolute for written_size_delta")
.end
});
// by default, use the last sent written_size as the basis for
// calculating the delta. if we don't yet have one, use the load time value.
let prev = cached_metrics
.get(&key)
.map(|(prev_at, prev)| {
// use the prev time from our last incremental update, or default to latest
// absolute update on the first round.
let prev_at = prev_at
.absolute_time()
.expect("never create EventType::Incremental for written_size");
let prev_at = last_stop_time.unwrap_or(prev_at);
(*prev_at, *prev)
})
.unwrap_or_else(|| {
// if we don't have a previous point of comparison, compare to the load time
// lsn.
let (disk_consistent_lsn, loaded_at) = &timeline.loaded_at;
(DateTime::from(*loaded_at), disk_consistent_lsn.0)
});
// written_size_delta_bytes
current_metrics.extend(
if let Some(delta) = written_size_now.1.checked_sub(prev.1) {
let up_to = written_size_now
.0
.absolute_time()
.expect("never create EventType::Incremental for written_size");
let key_value =
written_size_delta_key.from_previous_up_to(prev.0, *up_to, delta);
Some(key_value)
} else {
None
}, },
); timeline_written_size,
));
// written_size
current_metrics.push((key, written_size_now));
let span = info_span!("collect_metrics_iteration", tenant_id = %timeline.tenant_id, timeline_id = %timeline.timeline_id); let span = info_span!("collect_metrics_iteration", tenant_id = %timeline.tenant_id, timeline_id = %timeline.timeline_id);
match span.in_scope(|| timeline.get_current_logical_size(ctx)) { match span.in_scope(|| timeline.get_current_logical_size(ctx)) {
// Only send timeline logical size when it is fully calculated. // Only send timeline logical size when it is fully calculated.
Ok((size, is_exact)) if is_exact => { Ok((size, is_exact)) if is_exact => {
current_metrics.push( current_metrics.push((
PageserverConsumptionMetricsKey::timeline_logical_size( PageserverConsumptionMetricsKey {
tenant_id, tenant_id,
timeline.timeline_id, timeline_id: Some(timeline.timeline_id),
) metric: TIMELINE_LOGICAL_SIZE,
.now(size), },
); size,
));
} }
Ok((_, _)) => {} Ok((_, _)) => {}
Err(err) => { Err(err) => {
@@ -367,10 +194,14 @@ pub async fn collect_metrics_iteration(
match tenant.get_remote_size().await { match tenant.get_remote_size().await {
Ok(tenant_remote_size) => { Ok(tenant_remote_size) => {
current_metrics.push( current_metrics.push((
PageserverConsumptionMetricsKey::remote_storage_size(tenant_id) PageserverConsumptionMetricsKey {
.now(tenant_remote_size), tenant_id,
); timeline_id: None,
metric: REMOTE_STORAGE_SIZE,
},
tenant_remote_size,
));
} }
Err(err) => { Err(err) => {
error!( error!(
@@ -380,37 +211,34 @@ pub async fn collect_metrics_iteration(
} }
} }
current_metrics.push( current_metrics.push((
PageserverConsumptionMetricsKey::resident_size(tenant_id).now(tenant_resident_size), PageserverConsumptionMetricsKey {
); tenant_id,
timeline_id: None,
metric: RESIDENT_SIZE,
},
tenant_resident_size,
));
// Note that this metric is calculated in a separate bgworker // Note that this metric is calculated in a separate bgworker
// Here we only use cached value, which may lag behind the real latest one // Here we only use cached value, which may lag behind the real latest one
let tenant_synthetic_size = tenant.get_cached_synthetic_size(); let tenant_synthetic_size = tenant.get_cached_synthetic_size();
current_metrics.push((
if tenant_synthetic_size != 0 { PageserverConsumptionMetricsKey {
// only send non-zeroes because otherwise these show up as errors in logs tenant_id,
current_metrics.push( timeline_id: None,
PageserverConsumptionMetricsKey::synthetic_size(tenant_id) metric: SYNTHETIC_STORAGE_SIZE,
.now(tenant_synthetic_size), },
); tenant_synthetic_size,
} ));
} }
// Filter metrics, unless we want to send all metrics, including cached ones. // Filter metrics, unless we want to send all metrics, including cached ones.
// See: https://github.com/neondatabase/neon/issues/3485 // See: https://github.com/neondatabase/neon/issues/3485
if !send_cached { if !send_cached {
current_metrics.retain(|(curr_key, (kind, curr_val))| { current_metrics.retain(|(curr_key, curr_val)| match cached_metrics.get(curr_key) {
if kind.is_incremental() { Some(val) => val != curr_val,
// incremental values (currently only written_size_delta) should not get any cache None => true,
// deduplication because they will be used by upstream for "is still alive."
true
} else {
match cached_metrics.get(curr_key) {
Some((_, val)) => val != curr_val,
None => true,
}
}
}); });
} }
@@ -429,8 +257,8 @@ pub async fn collect_metrics_iteration(
chunk_to_send.clear(); chunk_to_send.clear();
// enrich metrics with type,timestamp and idempotency key before sending // enrich metrics with type,timestamp and idempotency key before sending
chunk_to_send.extend(chunk.iter().map(|(curr_key, (when, curr_val))| Event { chunk_to_send.extend(chunk.iter().map(|(curr_key, curr_val)| Event {
kind: *when, kind: EventType::Absolute { time: Utc::now() },
metric: curr_key.metric, metric: curr_key.metric,
idempotency_key: idempotency_key(node_id.to_string()), idempotency_key: idempotency_key(node_id.to_string()),
value: *curr_val, value: *curr_val,
@@ -445,43 +273,32 @@ pub async fn collect_metrics_iteration(
}) })
.expect("PageserverConsumptionMetric should not fail serialization"); .expect("PageserverConsumptionMetric should not fail serialization");
const MAX_RETRIES: u32 = 3; let res = client
.post(metric_collection_endpoint.clone())
.json(&chunk_json)
.send()
.await;
for attempt in 0..MAX_RETRIES { match res {
let res = client Ok(res) => {
.post(metric_collection_endpoint.clone()) if res.status().is_success() {
.json(&chunk_json) // update cached metrics after they were sent successfully
.send() for (curr_key, curr_val) in chunk.iter() {
.await; cached_metrics.insert(curr_key.clone(), *curr_val);
}
match res { } else {
Ok(res) => { error!("metrics endpoint refused the sent metrics: {:?}", res);
if res.status().is_success() { for metric in chunk_to_send.iter() {
// update cached metrics after they were sent successfully // Report if the metric value is suspiciously large
for (curr_key, curr_val) in chunk.iter() { if metric.value > (1u64 << 40) {
cached_metrics.insert(curr_key.clone(), *curr_val);
}
} else {
error!("metrics endpoint refused the sent metrics: {:?}", res);
for metric in chunk_to_send
.iter()
.filter(|metric| metric.value > (1u64 << 40))
{
// Report if the metric value is suspiciously large
error!("potentially abnormal metric value: {:?}", metric); error!("potentially abnormal metric value: {:?}", metric);
} }
} }
break;
}
Err(err) if err.is_timeout() => {
error!(attempt, "timeout sending metrics, retrying immediately");
continue;
}
Err(err) => {
error!(attempt, ?err, "failed to send metrics");
break;
} }
} }
Err(err) => {
error!("failed to send metrics: {:?}", err);
}
} }
} }
} }
@@ -500,7 +317,7 @@ pub async fn calculate_synthetic_size_worker(
_ = task_mgr::shutdown_watcher() => { _ = task_mgr::shutdown_watcher() => {
return Ok(()); return Ok(());
}, },
tick_at = ticker.tick() => { _ = ticker.tick() => {
let tenants = match mgr::list_tenants().await { let tenants = match mgr::list_tenants().await {
Ok(tenants) => tenants, Ok(tenants) => tenants,
@@ -526,12 +343,6 @@ pub async fn calculate_synthetic_size_worker(
} }
} }
crate::tenant::tasks::warn_when_period_overrun(
tick_at.elapsed(),
synthetic_size_calculation_interval,
"consumption_metrics_synthetic_size_worker",
);
} }
} }
} }

View File

@@ -179,9 +179,6 @@ impl RequestContext {
/// a context and you are unwilling to change all callers to provide one. /// a context and you are unwilling to change all callers to provide one.
/// ///
/// Before we add cancellation, we should get rid of this method. /// Before we add cancellation, we should get rid of this method.
///
/// [`attached_child`]: Self::attached_child
/// [`detached_child`]: Self::detached_child
pub fn todo_child(task_kind: TaskKind, download_behavior: DownloadBehavior) -> Self { pub fn todo_child(task_kind: TaskKind, download_behavior: DownloadBehavior) -> Self {
Self::new(task_kind, download_behavior) Self::new(task_kind, download_behavior)
} }

View File

@@ -60,7 +60,7 @@ use utils::serde_percent::Percent;
use crate::{ use crate::{
config::PageServerConf, config::PageServerConf,
task_mgr::{self, TaskKind, BACKGROUND_RUNTIME}, task_mgr::{self, TaskKind, BACKGROUND_RUNTIME},
tenant::{self, storage_layer::PersistentLayer, timeline::EvictionError, Timeline}, tenant::{self, storage_layer::PersistentLayer, Timeline},
}; };
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -110,6 +110,7 @@ pub fn launch_disk_usage_global_eviction_task(
disk_usage_eviction_task(&state, task_config, storage, &conf.tenants_path(), cancel) disk_usage_eviction_task(&state, task_config, storage, &conf.tenants_path(), cancel)
.await; .await;
info!("disk usage based eviction task finishing");
Ok(()) Ok(())
}, },
); );
@@ -125,16 +126,13 @@ async fn disk_usage_eviction_task(
tenants_dir: &Path, tenants_dir: &Path,
cancel: CancellationToken, cancel: CancellationToken,
) { ) {
scopeguard::defer! {
info!("disk usage based eviction task finishing");
};
use crate::tenant::tasks::random_init_delay; use crate::tenant::tasks::random_init_delay;
{ {
if random_init_delay(task_config.period, &cancel) if random_init_delay(task_config.period, &cancel)
.await .await
.is_err() .is_err()
{ {
info!("shutting down");
return; return;
} }
} }
@@ -166,11 +164,12 @@ async fn disk_usage_eviction_task(
.await; .await;
let sleep_until = start + task_config.period; let sleep_until = start + task_config.period;
if tokio::time::timeout_at(sleep_until, cancel.cancelled()) tokio::select! {
.await _ = tokio::time::sleep_until(sleep_until) => {},
.is_ok() _ = cancel.cancelled() => {
{ info!("shutting down");
break; break
}
} }
} }
} }
@@ -305,7 +304,7 @@ pub async fn disk_usage_eviction_task_iteration_impl<U: Usage>(
let now = SystemTime::now(); let now = SystemTime::now();
for (i, (partition, candidate)) in candidates.iter().enumerate() { for (i, (partition, candidate)) in candidates.iter().enumerate() {
debug!( debug!(
"cand {}/{}: size={}, no_access_for={}us, partition={:?}, {}/{}/{}", "cand {}/{}: size={}, no_access_for={}us, parition={:?}, tenant={} timeline={} layer={}",
i + 1, i + 1,
candidates.len(), candidates.len(),
candidate.layer.file_size(), candidate.layer.file_size(),
@@ -315,7 +314,7 @@ pub async fn disk_usage_eviction_task_iteration_impl<U: Usage>(
partition, partition,
candidate.layer.get_tenant_id(), candidate.layer.get_tenant_id(),
candidate.layer.get_timeline_id(), candidate.layer.get_timeline_id(),
candidate.layer, candidate.layer.filename().file_name(),
); );
} }
@@ -390,22 +389,13 @@ pub async fn disk_usage_eviction_task_iteration_impl<U: Usage>(
assert_eq!(results.len(), batch.len()); assert_eq!(results.len(), batch.len());
for (result, layer) in results.into_iter().zip(batch.iter()) { for (result, layer) in results.into_iter().zip(batch.iter()) {
match result { match result {
Some(Ok(())) => { Some(Ok(true)) => {
usage_assumed.add_available_bytes(layer.file_size()); usage_assumed.add_available_bytes(layer.file_size());
} }
Some(Err(EvictionError::CannotEvictRemoteLayer)) => { Some(Ok(false)) => {
unreachable!("get_local_layers_for_disk_usage_eviction finds only local layers") // this is:
} // - Replacement::{NotFound, Unexpected}
Some(Err(EvictionError::FileNotFound)) => { // - it cannot be is_remote_layer, filtered already
evictions_failed.file_sizes += layer.file_size();
evictions_failed.count += 1;
}
Some(Err(
e @ EvictionError::LayerNotFound(_)
| e @ EvictionError::StatFailed(_),
)) => {
let e = utils::error::report_compact_sources(&e);
warn!(%layer, "failed to evict layer: {e}");
evictions_failed.file_sizes += layer.file_size(); evictions_failed.file_sizes += layer.file_size();
evictions_failed.count += 1; evictions_failed.count += 1;
} }
@@ -413,6 +403,10 @@ pub async fn disk_usage_eviction_task_iteration_impl<U: Usage>(
assert!(cancel.is_cancelled()); assert!(cancel.is_cancelled());
return; return;
} }
Some(Err(e)) => {
// we really shouldn't be getting this, precondition failure
error!("failed to evict layer: {:#}", e);
}
} }
} }
} }
@@ -522,7 +516,7 @@ async fn collect_eviction_candidates(
if !tl.is_active() { if !tl.is_active() {
continue; continue;
} }
let info = tl.get_local_layers_for_disk_usage_eviction().await; let info = tl.get_local_layers_for_disk_usage_eviction();
debug!(tenant_id=%tl.tenant_id, timeline_id=%tl.timeline_id, "timeline resident layers count: {}", info.resident_layers.len()); debug!(tenant_id=%tl.tenant_id, timeline_id=%tl.timeline_id, "timeline resident layers count: {}", info.resident_layers.len());
tenant_candidates.extend( tenant_candidates.extend(
info.resident_layers info.resident_layers
@@ -545,12 +539,12 @@ async fn collect_eviction_candidates(
// We could be better here, e.g., sum of all L0 layers + most recent L1 layer. // We could be better here, e.g., sum of all L0 layers + most recent L1 layer.
// That's what's typically used by the various background loops. // That's what's typically used by the various background loops.
// //
// The default can be overridden with a fixed value in the tenant conf. // The default can be overriden with a fixed value in the tenant conf.
// A default override can be put in the default tenant conf in the pageserver.toml. // A default override can be put in the default tenant conf in the pageserver.toml.
let min_resident_size = if let Some(s) = tenant.get_min_resident_size_override() { let min_resident_size = if let Some(s) = tenant.get_min_resident_size_override() {
debug!( debug!(
tenant_id=%tenant.tenant_id(), tenant_id=%tenant.tenant_id(),
overridden_size=s, overriden_size=s,
"using overridden min resident size for tenant" "using overridden min resident size for tenant"
); );
s s

View File

@@ -186,8 +186,10 @@ paths:
schema: schema:
$ref: "#/components/schemas/Error" $ref: "#/components/schemas/Error"
delete: delete:
description: "Attempts to delete specified timeline. 500 and 409 errors should be retried" description: "Attempts to delete specified timeline. On 500 errors should be retried"
responses: responses:
"200":
description: Ok
"400": "400":
description: Error when no tenant id found in path or no timeline id description: Error when no tenant id found in path or no timeline id
content: content:
@@ -212,14 +214,8 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/NotFoundError" $ref: "#/components/schemas/NotFoundError"
"409":
description: Deletion is already in progress, continue polling
content:
application/json:
schema:
$ref: "#/components/schemas/ConflictError"
"412": "412":
description: Tenant is missing, or timeline has children description: Tenant is missing
content: content:
application/json: application/json:
schema: schema:
@@ -390,7 +386,6 @@ paths:
"202": "202":
description: Tenant attaching scheduled description: Tenant attaching scheduled
"400": "400":
description: Bad Request
content: content:
application/json: application/json:
schema: schema:
@@ -722,12 +717,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/ForbiddenError" $ref: "#/components/schemas/ForbiddenError"
"406":
description: Permanently unsatisfiable request, don't retry.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409": "409":
description: Timeline already exists, creation skipped description: Timeline already exists, creation skipped
content: content:
@@ -956,7 +945,7 @@ components:
type: string type: string
enum: [ "maybe", "attached", "failed" ] enum: [ "maybe", "attached", "failed" ]
data: data:
type: object - type: object
properties: properties:
reason: reason:
type: string type: string

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