mirror of
https://github.com/neondatabase/neon.git
synced 2026-03-24 18:50:37 +00:00
Compare commits
110 Commits
skip-sync
...
test_multi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e3f42e0ba | ||
|
|
7feb0d1a80 | ||
|
|
457e3a3ebc | ||
|
|
25d2f4b669 | ||
|
|
1685593f38 | ||
|
|
8d0f4a7857 | ||
|
|
3fc3666df7 | ||
|
|
89746a48c6 | ||
|
|
8d27a9c54e | ||
|
|
d98cb39978 | ||
|
|
27c73c8740 | ||
|
|
9e871318a0 | ||
|
|
e1061879aa | ||
|
|
f09e82270e | ||
|
|
d4a5fd5258 | ||
|
|
921bb86909 | ||
|
|
1e7db5458f | ||
|
|
b4d36f572d | ||
|
|
762a8a7bb5 | ||
|
|
2e8a3afab1 | ||
|
|
4580f5085a | ||
|
|
e074ccf170 | ||
|
|
196943c78f | ||
|
|
149dd36b6b | ||
|
|
be271e3edf | ||
|
|
7c85c7ea91 | ||
|
|
1066bca5e3 | ||
|
|
1aad8918e1 | ||
|
|
966213f429 | ||
|
|
35e73759f5 | ||
|
|
48936d44f8 | ||
|
|
2eae0a1fe5 | ||
|
|
53470ad12a | ||
|
|
edccef4514 | ||
|
|
982fce1e72 | ||
|
|
e767ced8d0 | ||
|
|
1309571f5d | ||
|
|
9a69b6cb94 | ||
|
|
cc82cd1b07 | ||
|
|
c76b74c50d | ||
|
|
ed938885ff | ||
|
|
db4d094afa | ||
|
|
0626e0bfd3 | ||
|
|
444d6e337f | ||
|
|
3a1be9b246 | ||
|
|
664d32eb7f | ||
|
|
ed845b644b | ||
|
|
87dd37a2f2 | ||
|
|
1355bd0ac5 | ||
|
|
a1d6b1a4af | ||
|
|
92aee7e07f | ||
|
|
5e2f29491f | ||
|
|
618d36ee6d | ||
|
|
33c2d94ba6 | ||
|
|
08bfe1c826 | ||
|
|
65ff256bb8 | ||
|
|
5177c1e4b1 | ||
|
|
49efcc3773 | ||
|
|
76b1cdc17e | ||
|
|
1f151d03d8 | ||
|
|
ac758e4f51 | ||
|
|
4f280c2953 | ||
|
|
20137d9588 | ||
|
|
634be4f4e0 | ||
|
|
d340cf3721 | ||
|
|
1741edf933 | ||
|
|
269e20aeab | ||
|
|
91435006bd | ||
|
|
b263510866 | ||
|
|
e418fc6dc3 | ||
|
|
434eaadbe3 | ||
|
|
6fb7edf494 | ||
|
|
505aa242ac | ||
|
|
1c516906e7 | ||
|
|
7d7cd8375c | ||
|
|
c92b7543b5 | ||
|
|
dbf88cf2d7 | ||
|
|
f1db87ac36 | ||
|
|
3f9defbfb4 | ||
|
|
c7143dbde6 | ||
|
|
cbf9a40889 | ||
|
|
10aba174c9 | ||
|
|
ab2ea8cfa5 | ||
|
|
9c8c55e819 | ||
|
|
10110bee69 | ||
|
|
cff7ae0b0d | ||
|
|
78a7f68902 | ||
|
|
24eaa3b7ca | ||
|
|
26828560a8 | ||
|
|
86604b3b7d | ||
|
|
4957bb2d48 | ||
|
|
ff1a1aea86 | ||
|
|
c9f05d418d | ||
|
|
9de1a6fb14 | ||
|
|
fbd37740c5 | ||
|
|
3e55d9dec6 | ||
|
|
f558f88a08 | ||
|
|
b990200496 | ||
|
|
7e20b49da4 | ||
|
|
032b603011 | ||
|
|
ca0e0781c8 | ||
|
|
b2a5e91a88 | ||
|
|
44e7d5132f | ||
|
|
c19681bc12 | ||
|
|
ec9b585837 | ||
|
|
02ef246db6 | ||
|
|
195d4932c6 | ||
|
|
7fe0a4bf1a | ||
|
|
ef2b9ffbcb | ||
|
|
250a27fb85 |
@@ -12,6 +12,11 @@ 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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -150,6 +150,14 @@ 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
|
||||||
|
|||||||
55
.github/workflows/approved-for-ci-run.yml
vendored
Normal file
55
.github/workflows/approved-for-ci-run.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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}"
|
||||||
157
.github/workflows/build_and_test.yml
vendored
157
.github/workflows/build_and_test.yml
vendored
@@ -5,6 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- release
|
- release
|
||||||
|
- ci-run/pr-*
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
@@ -127,6 +128,11 @@ 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() }}
|
||||||
@@ -155,7 +161,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.sha }}
|
GIT_VERSION: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Fix git ownership
|
- name: Fix git ownership
|
||||||
@@ -174,6 +180,27 @@ 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
|
||||||
@@ -369,13 +396,11 @@ 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
|
||||||
@@ -384,9 +409,11 @@ 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
|
||||||
|
|
||||||
@@ -614,7 +641,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.sha }}
|
--build-arg GIT_VERSION=${{ github.event.pull_request.head.sha || 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}}
|
||||||
@@ -658,7 +685,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.sha }}
|
--build-arg GIT_VERSION=${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
--build-arg BUILD_TAG=${{needs.tag.outputs.build-tag}}
|
--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
|
||||||
@@ -715,13 +742,42 @@ 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.sha }}
|
--build-arg GIT_VERSION=${{ github.event.pull_request.head.sha || 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 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
|
||||||
@@ -738,7 +794,7 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
shell: sh -eu {0}
|
shell: sh -eu {0}
|
||||||
env:
|
env:
|
||||||
VM_BUILDER_VERSION: v0.11.0
|
VM_BUILDER_VERSION: v0.13.1
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -840,8 +896,10 @@ 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: |
|
||||||
@@ -852,8 +910,10 @@ 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: |
|
||||||
@@ -875,16 +935,95 @@ 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:
|
||||||
|
# While on transition period we extract public extensions from compute-node image and custom extensions from extensions image.
|
||||||
|
# Later all the extensions will be moved to extensions image.
|
||||||
|
EXTENSIONS_IMAGE: ${{ github.ref_name == 'release' && '093970136003' || '369495373322'}}.dkr.ecr.eu-central-1.amazonaws.com/extensions-${{ matrix.version }}:latest
|
||||||
|
COMPUTE_NODE_IMAGE: ${{ github.ref_name == 'release' && '093970136003' || '369495373322'}}.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:latest
|
||||||
|
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' &&
|
||||||
|
'neon-prod-extensions-ap-southeast-1 neon-prod-extensions-eu-central-1 neon-prod-extensions-us-east-1 neon-prod-extensions-us-east-2 neon-prod-extensions-us-west-2' ||
|
||||||
|
'neon-dev-extensions-eu-central-1 neon-dev-extensions-eu-west-1 neon-dev-extensions-us-east-2' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Pull postgres-extensions image
|
||||||
|
run: |
|
||||||
|
docker pull ${EXTENSIONS_IMAGE}
|
||||||
|
docker pull ${COMPUTE_NODE_IMAGE}
|
||||||
|
|
||||||
|
- name: Create postgres-extensions container
|
||||||
|
id: create-container
|
||||||
|
run: |
|
||||||
|
EID=$(docker create ${EXTENSIONS_IMAGE} true)
|
||||||
|
echo "EID=${EID}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
CID=$(docker create ${COMPUTE_NODE_IMAGE} true)
|
||||||
|
echo "CID=${CID}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Extract postgres-extensions from container
|
||||||
|
run: |
|
||||||
|
rm -rf ./extensions-to-upload ./custom-extensions # Just in case
|
||||||
|
|
||||||
|
# In compute image we have a bit different directory layout
|
||||||
|
mkdir -p extensions-to-upload/share
|
||||||
|
docker cp ${{ steps.create-container.outputs.CID }}:/usr/local/share/extension ./extensions-to-upload/share/extension
|
||||||
|
docker cp ${{ steps.create-container.outputs.CID }}:/usr/local/lib ./extensions-to-upload/lib
|
||||||
|
|
||||||
|
# Delete Neon extensitons (they always present on compute-node image)
|
||||||
|
rm -rf ./extensions-to-upload/share/extension/neon*
|
||||||
|
rm -rf ./extensions-to-upload/lib/neon*
|
||||||
|
|
||||||
|
# Delete leftovers from the extension build step
|
||||||
|
rm -rf ./extensions-to-upload/lib/pgxs
|
||||||
|
rm -rf ./extensions-to-upload/lib/pkgconfig
|
||||||
|
|
||||||
|
docker cp ${{ steps.create-container.outputs.EID }}:/extensions ./custom-extensions
|
||||||
|
for EXT_NAME in $(ls ./custom-extensions); do
|
||||||
|
mkdir -p ./extensions-to-upload/${EXT_NAME}/share
|
||||||
|
|
||||||
|
mv ./custom-extensions/${EXT_NAME}/share/extension ./extensions-to-upload/${EXT_NAME}/share/extension
|
||||||
|
mv ./custom-extensions/${EXT_NAME}/lib ./extensions-to-upload/${EXT_NAME}/lib
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Upload postgres-extensions to S3
|
||||||
|
# TODO: Reenable step after switching to the new extensions format (tar-gzipped + index.json)
|
||||||
|
if: false
|
||||||
|
run: |
|
||||||
|
for BUCKET in $(echo ${S3_BUCKETS}); 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.CID || steps.create-container.outputs.EID) }}
|
||||||
|
run: |
|
||||||
|
docker rm ${{ steps.create-container.outputs.CID }} || true
|
||||||
|
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: [ promote-images, tag, regress-tests ]
|
needs: [ upload-postgres-extensions-to-s3, 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
|
||||||
|
|||||||
3
.github/workflows/neon_extra_builds.yml
vendored
3
.github/workflows/neon_extra_builds.yml
vendored
@@ -3,7 +3,8 @@ name: Check neon with extra platform builds
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- ci-run/pr-*
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
|
|||||||
332
Cargo.lock
generated
332
Cargo.lock
generated
@@ -158,6 +158,19 @@ 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"
|
||||||
@@ -200,17 +213,6 @@ 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"
|
||||||
@@ -604,7 +606,7 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"miniz_oxide",
|
"miniz_oxide 0.6.2",
|
||||||
"object",
|
"object",
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
]
|
]
|
||||||
@@ -805,18 +807,6 @@ 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"
|
||||||
@@ -837,7 +827,7 @@ dependencies = [
|
|||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"clap_lex 0.5.0",
|
"clap_lex",
|
||||||
"strsim",
|
"strsim",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -853,15 +843,6 @@ 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"
|
||||||
@@ -914,9 +895,11 @@ name = "compute_tools"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-compression",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"compute_api",
|
"compute_api",
|
||||||
|
"flate2",
|
||||||
"futures",
|
"futures",
|
||||||
"hyper",
|
"hyper",
|
||||||
"notify",
|
"notify",
|
||||||
@@ -977,7 +960,7 @@ name = "control_plane"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"comfy-table",
|
"comfy-table",
|
||||||
"compute_api",
|
"compute_api",
|
||||||
"git-version",
|
"git-version",
|
||||||
@@ -1047,19 +1030,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "criterion"
|
name = "criterion"
|
||||||
version = "0.4.0"
|
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 = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb"
|
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anes",
|
"anes",
|
||||||
"atty",
|
|
||||||
"cast",
|
"cast",
|
||||||
"ciborium",
|
"ciborium",
|
||||||
"clap 3.2.25",
|
"clap",
|
||||||
"criterion-plot",
|
"criterion-plot",
|
||||||
|
"is-terminal",
|
||||||
"itertools",
|
"itertools",
|
||||||
"lazy_static",
|
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"once_cell",
|
||||||
"oorandom",
|
"oorandom",
|
||||||
"plotters",
|
"plotters",
|
||||||
"rayon",
|
"rayon",
|
||||||
@@ -1140,7 +1123,7 @@ dependencies = [
|
|||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot 0.12.1",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
"signal-hook-mio",
|
"signal-hook-mio",
|
||||||
"winapi",
|
"winapi",
|
||||||
@@ -1210,7 +1193,7 @@ dependencies = [
|
|||||||
"hashbrown 0.12.3",
|
"hashbrown 0.12.3",
|
||||||
"lock_api",
|
"lock_api",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot_core",
|
"parking_lot_core 0.9.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1399,6 +1382,16 @@ 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"
|
||||||
@@ -1676,15 +1669,6 @@ 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"
|
||||||
@@ -1939,6 +1923,9 @@ 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]]
|
||||||
@@ -2189,6 +2176,15 @@ 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"
|
||||||
@@ -2267,16 +2263,6 @@ 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"
|
||||||
@@ -2393,9 +2379,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry"
|
name = "opentelemetry"
|
||||||
version = "0.18.0"
|
version = "0.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69d6c3d7288a106c0a363e4b0e8d308058d56902adefb16f4936f417ffef086e"
|
checksum = "5f4b8347cc26099d3aeee044065ecc3ae11469796b4d65d065a23a584ed92a6f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"opentelemetry_api",
|
"opentelemetry_api",
|
||||||
"opentelemetry_sdk",
|
"opentelemetry_sdk",
|
||||||
@@ -2403,9 +2389,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry-http"
|
name = "opentelemetry-http"
|
||||||
version = "0.7.0"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1edc79add46364183ece1a4542592ca593e6421c60807232f5b8f7a31703825d"
|
checksum = "a819b71d6530c4297b49b3cae2939ab3a8cc1b9f382826a1bc29dd0ca3864906"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2416,9 +2402,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry-otlp"
|
name = "opentelemetry-otlp"
|
||||||
version = "0.11.0"
|
version = "0.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d1c928609d087790fc936a1067bdc310ae702bdf3b090c3f281b713622c8bbde"
|
checksum = "8af72d59a4484654ea8eb183fea5ae4eb6a41d7ac3e3bae5f4d2a282a3a7d3ca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -2434,48 +2420,47 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry-proto"
|
name = "opentelemetry-proto"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d61a2f56df5574508dd86aaca016c917489e589ece4141df1b5e349af8d66c28"
|
checksum = "045f8eea8c0fa19f7d48e7bc3128a39c2e5c533d5c61298c548dfefc1064474c"
|
||||||
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.10.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 = "9b02e0230abb0ab6636d18e2ba8fa02903ea63772281340ccac18e0af3ec9eeb"
|
checksum = "24e33428e6bf08c6f7fcea4ddb8e358fab0fe48ab877a87c70c6ebe20f673ce5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry_api"
|
name = "opentelemetry_api"
|
||||||
version = "0.18.0"
|
version = "0.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c24f96e21e7acc813c7a8394ee94978929db2bcc46cf6b5014fc612bf7760c22"
|
checksum = "ed41783a5bf567688eb38372f2b7a8530f5a607a4b49d38dd7573236c23ca7e2"
|
||||||
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.18.0"
|
version = "0.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ca41c4933371b61c2a2f214bf16931499af4ec90543604ec828f7a625c09113"
|
checksum = "8b3a2a91fdbfdd4d212c0dcc2ab540de2c2bcbbd90be17de7a7daf8822d010c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
@@ -2504,31 +2489,19 @@ 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 4.3.0",
|
"clap",
|
||||||
"git-version",
|
"git-version",
|
||||||
"pageserver",
|
"pageserver",
|
||||||
"postgres_ffi",
|
"postgres_ffi",
|
||||||
@@ -2542,12 +2515,13 @@ 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 4.3.0",
|
"clap",
|
||||||
"close_fds",
|
"close_fds",
|
||||||
"const_format",
|
"const_format",
|
||||||
"consumption_metrics",
|
"consumption_metrics",
|
||||||
@@ -2558,6 +2532,7 @@ dependencies = [
|
|||||||
"enum-map",
|
"enum-map",
|
||||||
"enumset",
|
"enumset",
|
||||||
"fail",
|
"fail",
|
||||||
|
"flate2",
|
||||||
"futures",
|
"futures",
|
||||||
"git-version",
|
"git-version",
|
||||||
"hex",
|
"hex",
|
||||||
@@ -2629,6 +2604,17 @@ 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"
|
||||||
@@ -2636,7 +2622,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lock_api",
|
"lock_api",
|
||||||
"parking_lot_core",
|
"parking_lot_core 0.9.7",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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]]
|
||||||
@@ -2652,6 +2652,16 @@ dependencies = [
|
|||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pbkdf2"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0ca0b5a68607598bf3bad68f32227a8164f6254833f84eafaac409cd6746c31"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
"hmac",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "peeking_take_while"
|
name = "peeking_take_while"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -2926,9 +2936,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.58"
|
version = "1.0.64"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8"
|
checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -2957,7 +2967,7 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"memchr",
|
"memchr",
|
||||||
"parking_lot",
|
"parking_lot 0.12.1",
|
||||||
"procfs",
|
"procfs",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
@@ -3022,12 +3032,11 @@ 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 4.3.0",
|
"clap",
|
||||||
"consumption_metrics",
|
"consumption_metrics",
|
||||||
"futures",
|
"futures",
|
||||||
"git-version",
|
"git-version",
|
||||||
@@ -3045,7 +3054,8 @@ dependencies = [
|
|||||||
"native-tls",
|
"native-tls",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
"parking_lot",
|
"parking_lot 0.12.1",
|
||||||
|
"pbkdf2",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"postgres-native-tls",
|
"postgres-native-tls",
|
||||||
"postgres_backend",
|
"postgres_backend",
|
||||||
@@ -3056,6 +3066,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
|
"reqwest-retry",
|
||||||
"reqwest-tracing",
|
"reqwest-tracing",
|
||||||
"routerify",
|
"routerify",
|
||||||
"rstest",
|
"rstest",
|
||||||
@@ -3292,10 +3303,33 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest-tracing"
|
name = "reqwest-retry"
|
||||||
version = "0.4.4"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "783e8130d2427ddd7897dd3f814d4a3aea31b05deb42a4fdf8c18258fe5aefd1"
|
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]]
|
||||||
|
name = "reqwest-tracing"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b97ad83c2fc18113346b7158d79732242002427c30f620fa817c1f32901e0a8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3309,6 +3343,17 @@ 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"
|
||||||
@@ -3507,7 +3552,7 @@ dependencies = [
|
|||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"const_format",
|
"const_format",
|
||||||
"crc32c",
|
"crc32c",
|
||||||
"fs2",
|
"fs2",
|
||||||
@@ -3518,7 +3563,7 @@ dependencies = [
|
|||||||
"hyper",
|
"hyper",
|
||||||
"metrics",
|
"metrics",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot 0.12.1",
|
||||||
"postgres",
|
"postgres",
|
||||||
"postgres-protocol",
|
"postgres-protocol",
|
||||||
"postgres_backend",
|
"postgres_backend",
|
||||||
@@ -3809,7 +3854,8 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
source = "git+https://github.com/neondatabase/sharded-slab.git?rev=98d16753ab01c61f0a028de44167307a00efea00#98d16753ab01c61f0a028de44167307a00efea00"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
@@ -3937,7 +3983,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"bytes",
|
"bytes",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"const_format",
|
"const_format",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -3947,12 +3993,12 @@ dependencies = [
|
|||||||
"hyper",
|
"hyper",
|
||||||
"metrics",
|
"metrics",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot 0.12.1",
|
||||||
"prost",
|
"prost",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tonic 0.9.2",
|
"tonic 0.9.2",
|
||||||
"tonic-build 0.9.2",
|
"tonic-build",
|
||||||
"tracing",
|
"tracing",
|
||||||
"utils",
|
"utils",
|
||||||
"workspace_hack",
|
"workspace_hack",
|
||||||
@@ -4053,7 +4099,7 @@ checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"filetime",
|
"filetime",
|
||||||
"libc",
|
"libc",
|
||||||
"xattr",
|
"xattr 0.2.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4118,12 +4164,6 @@ 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"
|
||||||
@@ -4281,7 +4321,7 @@ dependencies = [
|
|||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
"parking_lot",
|
"parking_lot 0.12.1",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"phf",
|
"phf",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -4340,16 +4380,17 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-tar"
|
name = "tokio-tar"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
source = "git+https://github.com/neondatabase/tokio-tar.git?rev=404df61437de0feef49ba2ccdbdd94eb8ad6e142#404df61437de0feef49ba2ccdbdd94eb8ad6e142"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"filetime",
|
"filetime",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall 0.2.16",
|
"redox_syscall 0.3.5",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"xattr",
|
"xattr 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4476,19 +4517,6 @@ 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"
|
||||||
@@ -4539,7 +4567,7 @@ name = "trace"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"pageserver_api",
|
"pageserver_api",
|
||||||
"utils",
|
"utils",
|
||||||
"workspace_hack",
|
"workspace_hack",
|
||||||
@@ -4612,9 +4640,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-opentelemetry"
|
name = "tracing-opentelemetry"
|
||||||
version = "0.18.0"
|
version = "0.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21ebb87a95ea13271332df069020513ab70bdb5637ca42d6e492dc3bbbad48de"
|
checksum = "00a39dcf9bfc1742fa4d6215253b33a6e474be78275884c216fc2a06267b3600"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
@@ -4641,7 +4669,6 @@ 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",
|
||||||
@@ -4810,11 +4837,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",
|
||||||
@@ -4840,6 +4867,7 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-error",
|
"tracing-error",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -4887,7 +4915,7 @@ name = "wal_craft"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -4991,6 +5019,21 @@ 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"
|
||||||
@@ -5252,7 +5295,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
"either",
|
"either",
|
||||||
@@ -5322,6 +5365,15 @@ 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"
|
||||||
|
|||||||
26
Cargo.toml
26
Cargo.toml
@@ -32,9 +32,10 @@ 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"
|
||||||
@@ -83,18 +84,20 @@ 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.18.0"
|
opentelemetry = "0.19.0"
|
||||||
opentelemetry-otlp = { version = "0.11.0", default_features=false, features = ["http-proto", "trace", "http", "reqwest-client"] }
|
opentelemetry-otlp = { version = "0.12.0", default_features=false, features = ["http-proto", "trace", "http", "reqwest-client"] }
|
||||||
opentelemetry-semantic-conventions = "0.10.0"
|
opentelemetry-semantic-conventions = "0.11.0"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
|
pbkdf2 = "0.12.1"
|
||||||
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
|
||||||
prost = "0.11"
|
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_18"] }
|
reqwest-tracing = { version = "0.4.0", features = ["opentelemetry_0_19"] }
|
||||||
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"
|
||||||
@@ -121,14 +124,15 @@ 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.18.0"
|
tracing-opentelemetry = "0.19.0"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", default_features = false, features = ["smallvec", "fmt", "tracing-log", "std", "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"
|
||||||
@@ -145,7 +149,6 @@ postgres-native-tls = { git = "https://github.com/neondatabase/rust-postgres.git
|
|||||||
postgres-protocol = { git = "https://github.com/neondatabase/rust-postgres.git", rev="1aaedab101b23f7612042850d8f2036810fa7c7f" }
|
postgres-protocol = { git = "https://github.com/neondatabase/rust-postgres.git", rev="1aaedab101b23f7612042850d8f2036810fa7c7f" }
|
||||||
postgres-types = { git = "https://github.com/neondatabase/rust-postgres.git", rev="1aaedab101b23f7612042850d8f2036810fa7c7f" }
|
postgres-types = { git = "https://github.com/neondatabase/rust-postgres.git", rev="1aaedab101b23f7612042850d8f2036810fa7c7f" }
|
||||||
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="1aaedab101b23f7612042850d8f2036810fa7c7f" }
|
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="1aaedab101b23f7612042850d8f2036810fa7c7f" }
|
||||||
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
|
||||||
@@ -170,7 +173,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.4"
|
criterion = "0.5.1"
|
||||||
rcgen = "0.10"
|
rcgen = "0.10"
|
||||||
rstest = "0.17"
|
rstest = "0.17"
|
||||||
tempfile = "3.4"
|
tempfile = "3.4"
|
||||||
@@ -182,11 +185,6 @@ tonic-build = "0.9"
|
|||||||
# 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="1aaedab101b23f7612042850d8f2036810fa7c7f" }
|
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="1aaedab101b23f7612042850d8f2036810fa7c7f" }
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -132,10 +132,20 @@ 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/
|
||||||
|
|
||||||
# packaged cmake is too old
|
RUN case "$(uname -m)" in \
|
||||||
RUN wget https://github.com/Kitware/CMake/releases/download/v3.24.2/cmake-3.24.2-linux-x86_64.sh \
|
"x86_64") \
|
||||||
|
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 "739d372726cb23129d57a539ce1432453448816e345e1545f6127296926b6754 /tmp/cmake-install.sh" | sha256sum --check \
|
&& echo "${CMAKE_CHECKSUM} /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
|
||||||
@@ -189,8 +199,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.0.tar.gz -O pgvector.tar.gz && \
|
RUN wget https://github.com/pgvector/pgvector/archive/refs/tags/v0.4.4.tar.gz -O pgvector.tar.gz && \
|
||||||
echo "b76cf84ddad452cc880a6c8c661d137ddd8679c000a16332f4f03ecf6e10bcc8 pgvector.tar.gz" | sha256sum --check && \
|
echo "1cb70a63f8928e396474796c22a20be9f7285a8a013009deb8152445b61b72e6 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 && \
|
||||||
@@ -515,6 +525,45 @@ RUN wget https://github.com/ChenHuajun/pg_roaringbitmap/archive/refs/tags/v0.5.4
|
|||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/roaringbitmap.control
|
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"
|
||||||
|
# eeb3ba7c3a60c95b2604dd543c64b2f1bb4a3703 made on 15/07/2023
|
||||||
|
# There is no release tag yet
|
||||||
|
RUN wget https://github.com/neondatabase/pg_embedding/archive/eeb3ba7c3a60c95b2604dd543c64b2f1bb4a3703.tar.gz -O pg_embedding.tar.gz && \
|
||||||
|
echo "030846df723652f99a8689ce63b66fa0c23477a7fd723533ab8a6b28ab70730f 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/
|
||||||
|
|
||||||
|
# Kaniko doesn't allow to do `${from#/usr/local/pgsql/}`, so we use `${from:17}` instead
|
||||||
|
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 | sort > /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 | sort > /after.txt && \
|
||||||
|
/bin/bash -c 'for from in $(comm -13 /before.txt /after.txt); do to=/extensions/anon/${from:17} && mkdir -p $(dirname ${to}) && cp -a ${from} ${to}; done'
|
||||||
|
|
||||||
#########################################################################################
|
#########################################################################################
|
||||||
#
|
#
|
||||||
# Layer "rust extensions"
|
# Layer "rust extensions"
|
||||||
@@ -623,6 +672,7 @@ 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/
|
||||||
@@ -650,6 +700,7 @@ 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=rdkit-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
COPY --from=pg-uuidv7-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-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) \
|
||||||
@@ -698,6 +749,22 @@ 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 scratch AS postgres-extensions
|
||||||
|
# After the transition this layer will include all extensitons.
|
||||||
|
# As for now, it's only for new custom ones
|
||||||
|
#
|
||||||
|
# # Default extensions
|
||||||
|
# COPY --from=postgres-cleanup-layer /usr/local/pgsql/share/extension /usr/local/pgsql/share/extension
|
||||||
|
# COPY --from=postgres-cleanup-layer /usr/local/pgsql/lib /usr/local/pgsql/lib
|
||||||
|
# Custom extensions
|
||||||
|
COPY --from=pg-anon-pg-build /extensions/anon/lib/ /extensions/anon/lib
|
||||||
|
COPY --from=pg-anon-pg-build /extensions/anon/share/extension /extensions/anon/share/extension
|
||||||
|
|
||||||
#########################################################################################
|
#########################################################################################
|
||||||
#
|
#
|
||||||
# Final layer
|
# Final layer
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -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
|
||||||
Starting pageserver at '127.0.0.1:64000' in '.neon'.
|
Initializing pageserver node 1 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 at '127.0.0.1:64000' in '.neon'.
|
Starting pageserver node 1 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,8 +152,7 @@ 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 ...
|
||||||
Extracting base backup to create postgres instance: path=.neon/pgdatadirs/tenants/9ef87a5bf0d92544f6fafeeb3239695c/main port=55432
|
Starting postgres at 'postgresql://cloud_admin@127.0.0.1:55432/postgres'
|
||||||
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
|
||||||
@@ -189,18 +188,17 @@ 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 ...
|
||||||
Extracting base backup to create postgres instance: path=.neon/pgdatadirs/tenants/9ef87a5bf0d92544f6fafeeb3239695c/migration_check port=55433
|
Starting postgres at 'postgresql://cloud_admin@127.0.0.1:55434/postgres'
|
||||||
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:55433 b3b863fa45fa9e57e615f9f2d944e601 migration_check 0/16F9A70 running
|
migration_check 127.0.0.1:55434 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 -p55433 -h 127.0.0.1 -U cloud_admin postgres
|
> psql -p55434 -h 127.0.0.1 -U cloud_admin postgres
|
||||||
postgres=# select * from t;
|
postgres=# select * from t;
|
||||||
key | value
|
key | value
|
||||||
-----+-------
|
-----+-------
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ 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
|
||||||
|
|||||||
@@ -223,9 +223,8 @@ fn main() -> Result<()> {
|
|||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
// Launch remaining service threads
|
// Launch remaining service threads
|
||||||
let _monitor_handle = launch_monitor(&compute).expect("cannot launch compute monitor thread");
|
let _monitor_handle = launch_monitor(&compute);
|
||||||
let _configurator_handle =
|
let _configurator_handle = launch_configurator(&compute);
|
||||||
launch_configurator(&compute).expect("cannot launch configurator thread");
|
|
||||||
|
|
||||||
// Start Postgres
|
// Start Postgres
|
||||||
let mut delay_exit = false;
|
let mut delay_exit = false;
|
||||||
@@ -256,6 +255,16 @@ 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:?}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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};
|
||||||
@@ -15,6 +16,7 @@ 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::*;
|
||||||
@@ -140,14 +142,14 @@ fn create_neon_superuser(spec: &ComputeSpec, client: &mut Client) -> Result<()>
|
|||||||
.cluster
|
.cluster
|
||||||
.roles
|
.roles
|
||||||
.iter()
|
.iter()
|
||||||
.map(|r| format!("'{}'", escape_literal(&r.name)))
|
.map(|r| escape_literal(&r.name))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let dbs = spec
|
let dbs = spec
|
||||||
.cluster
|
.cluster
|
||||||
.databases
|
.databases
|
||||||
.iter()
|
.iter()
|
||||||
.map(|db| format!("'{}'", escape_literal(&db.name)))
|
.map(|db| escape_literal(&db.name))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let roles_decl = if roles.is_empty() {
|
let roles_decl = if roles.is_empty() {
|
||||||
@@ -235,7 +237,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(self, compute_state))]
|
#[instrument(skip_all, fields(%lsn))]
|
||||||
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();
|
||||||
@@ -253,20 +255,52 @@ impl ComputeNode {
|
|||||||
|
|
||||||
let mut client = config.connect(NoTls)?;
|
let mut client = config.connect(NoTls)?;
|
||||||
let basebackup_cmd = match lsn {
|
let basebackup_cmd = match lsn {
|
||||||
Lsn(0) => format!("basebackup {} {}", spec.tenant_id, spec.timeline_id), // First start of the compute
|
// HACK We don't use compression on first start (Lsn(0)) because there's no API for it
|
||||||
_ => format!("basebackup {} {} {}", spec.tenant_id, spec.timeline_id, lsn),
|
Lsn(0) => format!("basebackup {} {}", spec.tenant_id, spec.timeline_id),
|
||||||
|
_ => 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.
|
||||||
let mut ar = tar::Archive::new(copyreader);
|
if gzip {
|
||||||
ar.set_ignore_zeros(true);
|
let mut ar = tar::Archive::new(flate2::read::GzDecoder::new(&mut bufreader));
|
||||||
ar.unpack(&self.pgdata)?;
|
ar.set_ignore_zeros(true);
|
||||||
|
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()
|
||||||
@@ -277,8 +311,8 @@ impl ComputeNode {
|
|||||||
|
|
||||||
// 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(self, storage_auth_token))]
|
#[instrument(skip_all)]
|
||||||
fn sync_safekeepers(&self, storage_auth_token: Option<String>) -> Result<Lsn> {
|
pub 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)
|
||||||
@@ -322,7 +356,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(self, compute_state))]
|
#[instrument(skip_all)]
|
||||||
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;
|
||||||
@@ -380,7 +414,7 @@ impl ComputeNode {
|
|||||||
|
|
||||||
/// 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(self))]
|
#[instrument(skip_all)]
|
||||||
pub fn start_postgres(
|
pub fn start_postgres(
|
||||||
&self,
|
&self,
|
||||||
storage_auth_token: Option<String>,
|
storage_auth_token: Option<String>,
|
||||||
@@ -404,7 +438,7 @@ impl ComputeNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Do initial configuration of the already started Postgres.
|
/// Do initial configuration of the already started Postgres.
|
||||||
#[instrument(skip(self, compute_state))]
|
#[instrument(skip_all)]
|
||||||
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.
|
||||||
@@ -458,7 +492,7 @@ impl ComputeNode {
|
|||||||
// 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(self, client))]
|
#[instrument(skip_all)]
|
||||||
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(())
|
||||||
@@ -466,7 +500,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(self))]
|
#[instrument(skip_all)]
|
||||||
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;
|
||||||
|
|
||||||
@@ -501,7 +535,7 @@ impl ComputeNode {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip_all)]
|
||||||
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 pspec = compute_state.pspec.as_ref().expect("spec must be set");
|
||||||
@@ -516,9 +550,9 @@ impl ComputeNode {
|
|||||||
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 pg = self.start_postgres(pspec.storage_auth_token.clone())?;
|
||||||
|
|
||||||
|
let config_time = Utc::now();
|
||||||
if pspec.spec.mode == ComputeMode::Primary && !pspec.spec.skip_pg_catalog_updates {
|
if pspec.spec.mode == ComputeMode::Primary && !pspec.spec.skip_pg_catalog_updates {
|
||||||
self.apply_config(&compute_state)?;
|
self.apply_config(&compute_state)?;
|
||||||
}
|
}
|
||||||
@@ -526,11 +560,16 @@ impl ComputeNode {
|
|||||||
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.config_ms = startup_end_time
|
state.metrics.start_postgres_ms = config_time
|
||||||
.signed_duration_since(start_time)
|
.signed_duration_since(start_time)
|
||||||
.to_std()
|
.to_std()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_millis() as u64;
|
.as_millis() as u64;
|
||||||
|
state.metrics.config_ms = startup_end_time
|
||||||
|
.signed_duration_since(config_time)
|
||||||
|
.to_std()
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as u64;
|
||||||
state.metrics.total_startup_ms = startup_end_time
|
state.metrics.total_startup_ms = startup_end_time
|
||||||
.signed_duration_since(compute_state.start_time)
|
.signed_duration_since(compute_state.start_time)
|
||||||
.to_std()
|
.to_std()
|
||||||
@@ -544,6 +583,13 @@ impl ComputeNode {
|
|||||||
pspec.spec.cluster.cluster_id.as_deref().unwrap_or("None")
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,30 +47,22 @@ 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!(
|
writeln!(file, "neon.pageserver_connstring={}", escape_conf_value(s))?;
|
||||||
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!(
|
writeln!(file, "neon.tenant_id={}", escape_conf_value(&s.to_string()))?;
|
||||||
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())
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
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(compute))]
|
#[instrument(skip_all)]
|
||||||
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 {
|
||||||
@@ -42,13 +41,14 @@ fn configurator_main_loop(compute: &Arc<ComputeNode>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn launch_configurator(compute: &Arc<ComputeNode>) -> Result<thread::JoinHandle<()>> {
|
pub fn launch_configurator(compute: &Arc<ComputeNode>) -> thread::JoinHandle<()> {
|
||||||
let compute = Arc::clone(compute);
|
let compute = Arc::clone(compute);
|
||||||
|
|
||||||
Ok(thread::Builder::new()
|
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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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};
|
||||||
@@ -105,10 +104,11 @@ 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>) -> Result<thread::JoinHandle<()>> {
|
pub fn launch_monitor(state: &Arc<ComputeNode>) -> thread::JoinHandle<()> {
|
||||||
let state = Arc::clone(state);
|
let state = Arc::clone(state);
|
||||||
|
|
||||||
Ok(thread::Builder::new()
|
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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,26 @@ 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
|
/// Escape a string for including it in a SQL literal. Wrapping the result
|
||||||
|
/// with `E'{}'` or `'{}'` is not required, as it returns a ready-to-use
|
||||||
|
/// SQL string literal, e.g. `'db'''` or `E'db\\'`.
|
||||||
|
/// 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 {
|
pub fn escape_literal(s: &str) -> String {
|
||||||
s.replace('\'', "''").replace('\\', "\\\\")
|
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.
|
/// Escape a string so that it can be used in postgresql.conf. Wrapping the result
|
||||||
/// Same as escape_literal, currently.
|
/// with `'{}'` is not required, as it returns a ready-to-use config string.
|
||||||
pub fn escape_conf_value(s: &str) -> String {
|
pub fn escape_conf_value(s: &str) -> String {
|
||||||
s.replace('\'', "''").replace('\\', "\\\\")
|
let res = s.replace('\'', "''").replace('\\', "\\\\");
|
||||||
|
format!("'{}'", res)
|
||||||
}
|
}
|
||||||
|
|
||||||
trait GenericOptionExt {
|
trait GenericOptionExt {
|
||||||
@@ -37,7 +48,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 {
|
||||||
@@ -49,7 +60,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 {
|
||||||
@@ -215,7 +226,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(pg))]
|
#[instrument(skip_all, fields(pgdata = %pgdata.display()))]
|
||||||
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");
|
||||||
|
|
||||||
|
|||||||
@@ -397,10 +397,44 @@ 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" => {
|
||||||
let query: String = format!("DROP DATABASE IF EXISTS {}", &op.name.pg_quote());
|
// In Postgres we can't drop a database if it is a template.
|
||||||
|
// 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(query.as_str(), &[])?;
|
client.execute(unset_template_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();
|
||||||
|
|||||||
@@ -89,4 +89,12 @@ 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\\\\''\\\\'''");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 [`lock_file`] module for more info.
|
//! See the [`lock_file`](utils::lock_file) module for more info.
|
||||||
|
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@@ -180,6 +180,11 @@ 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) => {
|
||||||
|
|||||||
@@ -308,7 +308,8 @@ 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")?;
|
||||||
env.init(pg_version)
|
let force = init_match.get_flag("force");
|
||||||
|
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.
|
||||||
@@ -1013,6 +1014,13 @@ 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)
|
||||||
@@ -1028,6 +1036,7 @@ 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")
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
//!
|
//!
|
||||||
//! 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;
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
//!
|
//!
|
||||||
//! 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
|
||||||
@@ -22,7 +24,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Directory contents:
|
//! Directory contents:
|
||||||
//!
|
//!
|
||||||
//! ```ignore
|
//! ```text
|
||||||
//! .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
|
||||||
@@ -287,7 +289,7 @@ impl Endpoint {
|
|||||||
.env
|
.env
|
||||||
.safekeepers
|
.safekeepers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|sk| format!("localhost:{}", sk.pg_port))
|
.map(|sk| format!("localhost:{}", sk.get_compute_port()))
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(",");
|
.join(",");
|
||||||
conf.append("neon.safekeepers", &safekeepers);
|
conf.append("neon.safekeepers", &safekeepers);
|
||||||
@@ -316,7 +318,7 @@ impl Endpoint {
|
|||||||
.env
|
.env
|
||||||
.safekeepers
|
.safekeepers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| x.pg_port.to_string())
|
.map(|x| x.get_compute_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(",");
|
||||||
@@ -405,6 +407,16 @@ 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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,7 +463,7 @@ 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.pg_port));
|
safekeeper_connstrings.push(format!("127.0.0.1:{}", sk.get_compute_port()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,7 +519,13 @@ 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;
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ 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>,
|
||||||
@@ -149,6 +150,7 @@ 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,
|
||||||
@@ -158,6 +160,14 @@ 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()
|
||||||
@@ -364,7 +374,7 @@ impl LocalEnv {
|
|||||||
//
|
//
|
||||||
// Initialize a new Neon repository
|
// Initialize a new Neon repository
|
||||||
//
|
//
|
||||||
pub fn init(&mut self, pg_version: u32) -> anyhow::Result<()> {
|
pub fn init(&mut self, pg_version: u32, force: bool) -> 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!(
|
||||||
@@ -372,11 +382,29 @@ impl LocalEnv {
|
|||||||
"repository base path is missing"
|
"repository base path is missing"
|
||||||
);
|
);
|
||||||
|
|
||||||
ensure!(
|
if base_path.exists() {
|
||||||
!base_path.exists(),
|
if force {
|
||||||
"directory '{}' already exists. Perhaps already initialized?",
|
println!("removing all contents of '{}'", base_path.display());
|
||||||
base_path.display()
|
// instead of directly calling `remove_dir_all`, we keep the original dir but removing
|
||||||
);
|
// 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 {}",
|
||||||
@@ -392,7 +420,9 @@ impl LocalEnv {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::create_dir(base_path)?;
|
if !base_path.exists() {
|
||||||
|
fs::create_dir(base_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Generate keypair for JWT.
|
// Generate keypair for JWT.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
//!
|
//!
|
||||||
//! 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;
|
||||||
@@ -119,45 +120,55 @@ 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",
|
"-D".to_owned(),
|
||||||
datadir.to_str().with_context(|| {
|
datadir
|
||||||
format!("Datadir path {datadir:?} cannot be represented as a unicode string")
|
.to_str()
|
||||||
})?,
|
.with_context(|| {
|
||||||
"--id",
|
format!("Datadir path {datadir:?} cannot be represented as a unicode string")
|
||||||
&id_string,
|
})?
|
||||||
"--listen-pg",
|
.to_owned(),
|
||||||
&listen_pg,
|
"--id".to_owned(),
|
||||||
"--listen-http",
|
id_string,
|
||||||
&listen_http,
|
"--listen-pg".to_owned(),
|
||||||
"--availability-zone",
|
listen_pg,
|
||||||
&availability_zone,
|
"--listen-http".to_owned(),
|
||||||
|
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");
|
args.push("--no-sync".to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
let broker_endpoint = format!("{}", self.env.broker.client_url());
|
let broker_endpoint = format!("{}", self.env.broker.client_url());
|
||||||
args.extend(["--broker-endpoint", &broker_endpoint]);
|
args.extend(["--broker-endpoint".to_owned(), 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", &backup_threads]);
|
args.extend(["--backup-threads".to_owned(), 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", remote_storage]);
|
args.extend(["--remote-storage".to_owned(), remote_storage.clone()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
"--auth-validation-public-key-path".to_owned(),
|
||||||
key_path.to_str().with_context(|| {
|
key_path
|
||||||
format!("Key path {key_path:?} cannot be represented as a unicode string")
|
.to_str()
|
||||||
})?,
|
.with_context(|| {
|
||||||
|
format!("Key path {key_path:?} cannot be represented as a unicode string")
|
||||||
|
})?
|
||||||
|
.to_owned(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ services:
|
|||||||
- "/bin/bash"
|
- "/bin/bash"
|
||||||
- "-c"
|
- "-c"
|
||||||
command:
|
command:
|
||||||
- "until pg_isready -h compute -p 55433 ; do
|
- "until pg_isready -h compute -p 55433 -U cloud_admin ; do
|
||||||
echo 'Waiting to start compute...' && sleep 1;
|
echo 'Waiting to start compute...' && sleep 1;
|
||||||
done"
|
done"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ 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
|
||||||
|
|||||||
@@ -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::timeout!` is
|
one of the Futures returns, and drops the others. `tokio::time::timeout`
|
||||||
another example. In the Rust ecosystem, some functions are
|
is 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::timeout!`, and other such
|
careful when using `tokio::select!`, `tokio::time::timeout`, and other
|
||||||
functions that can cause a Future to be dropped. They can only be used
|
such functions that can cause a Future to be dropped. They can only be
|
||||||
with functions that are explicitly documented to be cancellation-safe,
|
used 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
|
||||||
|
|||||||
84
docs/rfcs/024-user-mgmt.md
Normal file
84
docs/rfcs/024-user-mgmt.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# 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).
|
||||||
22
docs/tools.md
Normal file
22
docs/tools.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 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)`
|
||||||
@@ -71,6 +71,8 @@ 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 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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Helpers for observing duration on HistogramVec / CounterVec / GaugeVec / MetricVec<T>.
|
//! Helpers for observing duration on `HistogramVec` / `CounterVec` / `GaugeVec` / `MetricVec<T>`.
|
||||||
|
|
||||||
use std::{future::Future, time::Instant};
|
use std::{future::Future, time::Instant};
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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,
|
||||||
@@ -76,7 +77,12 @@ 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.
|
||||||
///
|
///
|
||||||
@@ -118,7 +124,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,12 +417,16 @@ pub struct LayerResidenceEvent {
|
|||||||
pub reason: LayerResidenceEventReason,
|
pub reason: LayerResidenceEventReason,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The reason for recording a given [`ResidenceEvent`].
|
/// The reason for recording a given [`LayerResidenceEvent`].
|
||||||
#[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`].
|
||||||
@@ -924,7 +934,13 @@ 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 {
|
||||||
|
|||||||
@@ -60,8 +60,9 @@ 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) {
|
||||||
|
|||||||
@@ -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) as u16
|
((xid / pg_constants::MULTIXACT_MEMBERS_PER_MEMBERGROUP as u32)
|
||||||
% pg_constants::MULTIXACT_MEMBERGROUPS_PER_PAGE
|
% pg_constants::MULTIXACT_MEMBERGROUPS_PER_PAGE as u32
|
||||||
* pg_constants::MULTIXACT_MEMBERGROUP_SIZE) as usize
|
* pg_constants::MULTIXACT_MEMBERGROUP_SIZE as u32) as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mx_offset_to_flags_bitshift(xid: MultiXactId) -> u16 {
|
pub fn mx_offset_to_flags_bitshift(xid: MultiXactId) -> u16 {
|
||||||
@@ -81,3 +81,41 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,14 +49,16 @@ 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.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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 box[1] in polling internally). tokio::io::split is used for splitting
|
//! allocates a [Box] 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.
|
||||||
//!
|
//!
|
||||||
//! [1] https://docs.rs/futures-util/0.3.26/src/futures_util/lock/bilock.rs.html#107
|
//! [Box]: 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 {
|
||||||
|
|||||||
@@ -934,6 +934,15 @@ 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::*;
|
||||||
@@ -965,12 +974,3 @@ mod tests {
|
|||||||
assert_eq!(split_options(¶ms), ["foo bar", " \\", "baz ", "lol"]);
|
assert_eq!(split_options(¶ms), ["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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,6 +50,12 @@ 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!(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
future::Future,
|
future::Future,
|
||||||
|
io::ErrorKind,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
};
|
};
|
||||||
@@ -150,10 +151,7 @@ impl RemoteStorage for LocalFs {
|
|||||||
let mut files = vec![];
|
let mut files = vec![];
|
||||||
let mut directory_queue = vec![full_path.clone()];
|
let mut directory_queue = vec![full_path.clone()];
|
||||||
|
|
||||||
while !directory_queue.is_empty() {
|
while let Some(cur_folder) = directory_queue.pop() {
|
||||||
let cur_folder = directory_queue
|
|
||||||
.pop()
|
|
||||||
.expect("queue cannot be empty: we just checked");
|
|
||||||
let mut entries = fs::read_dir(cur_folder.clone()).await?;
|
let mut entries = fs::read_dir(cur_folder.clone()).await?;
|
||||||
while let Some(entry) = entries.next_entry().await? {
|
while let Some(entry) = entries.next_entry().await? {
|
||||||
let file_name: PathBuf = entry.file_name().into();
|
let file_name: PathBuf = entry.file_name().into();
|
||||||
@@ -343,18 +341,14 @@ 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);
|
||||||
if !file_path.exists() {
|
match fs::remove_file(&file_path).await {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
// The file doesn't exist. This shouldn't yield an error to mirror S3's behaviour.
|
||||||
// See https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html
|
// See https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html
|
||||||
// > If there isn't a null version, Amazon S3 does not remove any objects but will still respond that the command was successful.
|
// > If there isn't a null version, Amazon S3 does not remove any objects but will still respond that the command was successful.
|
||||||
return Ok(());
|
Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
|
||||||
|
Err(e) => Err(anyhow::anyhow!(e)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if !file_path.is_file() {
|
|
||||||
anyhow::bail!("{file_path:?} is not a file");
|
|
||||||
}
|
|
||||||
Ok(fs::remove_file(file_path)
|
|
||||||
.await
|
|
||||||
.map_err(|e| anyhow::anyhow!(e))?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_objects<'a>(&self, paths: &'a [RemotePath]) -> anyhow::Result<()> {
|
async fn delete_objects<'a>(&self, paths: &'a [RemotePath]) -> anyhow::Result<()> {
|
||||||
|
|||||||
@@ -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 it's size calculated.
|
/// `Segment` which has had its size calculated.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct SegmentSize {
|
struct SegmentSize {
|
||||||
method: SegmentMethod,
|
method: SegmentMethod,
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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
|
||||||
@@ -41,6 +40,12 @@ 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
|
||||||
|
|||||||
@@ -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, PartialEq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy, 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)
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ 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;
|
||||||
@@ -24,6 +31,15 @@ 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);
|
||||||
|
|||||||
111
libs/utils/src/error.rs
Normal file
111
libs/utils/src/error.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/// 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ 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};
|
||||||
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;
|
||||||
@@ -148,26 +147,140 @@ 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();
|
||||||
|
|
||||||
let mut buffer = vec![];
|
/// An [`std::io::Write`] implementation on top of a channel sending [`bytes::Bytes`] chunks.
|
||||||
let encoder = TextEncoder::new();
|
struct ChannelWriter {
|
||||||
|
buffer: BytesMut,
|
||||||
|
tx: mpsc::Sender<std::io::Result<Bytes>>,
|
||||||
|
written: usize,
|
||||||
|
}
|
||||||
|
|
||||||
let metrics = tokio::task::spawn_blocking(move || {
|
impl ChannelWriter {
|
||||||
// Currently we take a lot of mutexes while collecting metrics, so it's
|
fn new(buf_len: usize, tx: mpsc::Sender<std::io::Result<Bytes>>) -> Self {
|
||||||
// better to spawn a blocking task to avoid blocking the event loop.
|
assert_ne!(buf_len, 0);
|
||||||
metrics::gather()
|
ChannelWriter {
|
||||||
})
|
// split about half off the buffer from the start, because we flush depending on
|
||||||
.await
|
// capacity. first flush will come sooner than without this, but now resizes will
|
||||||
.map_err(|e: JoinError| ApiError::InternalServerError(e.into()))?;
|
// have better chance of picking up the "other" half. not guaranteed of course.
|
||||||
encoder.encode(&metrics, &mut buffer).unwrap();
|
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 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::from(buffer))
|
.body(body)
|
||||||
.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ 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)")
|
||||||
@@ -109,10 +112,16 @@ 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).
|
||||||
/// So everything becides docker build is covered by git_version crate, and docker uses a `GIT_VERSION` argument to get the value required.
|
/// When building locally, the `git_version` is used to query .git. When building on CI and docker,
|
||||||
/// 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.
|
/// we don't build the actual PR branch commits, but always a "phantom" would be merge commit to
|
||||||
/// Git version received from environment variable used as a fallback in git_version invocation.
|
/// the target branch -- the actual PR commit from which we build from is supplied as GIT_VERSION
|
||||||
/// And to avoid running buildscript every recompilation, we use rerun-if-env-changed option.
|
/// environment variable.
|
||||||
|
///
|
||||||
|
/// 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?
|
||||||
@@ -124,25 +133,36 @@ 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) => {
|
||||||
const $const_identifier: &str = git_version::git_version!(
|
// this should try GIT_VERSION first only then git_version::git_version!
|
||||||
prefix = "git:",
|
const $const_identifier: &::core::primitive::str = {
|
||||||
fallback = concat!(
|
const __COMMIT_FROM_GIT: &::core::primitive::str = git_version::git_version! {
|
||||||
"git-env:",
|
prefix = "",
|
||||||
env!("GIT_VERSION", "Missing GIT_VERSION envvar")
|
fallback = "unknown",
|
||||||
),
|
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 {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
//! 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 [`pid_file`].
|
//! The only consumer of this module is currently
|
||||||
//! See the module-level comment there for potential pitfalls
|
//! [`pid_file`](crate::pid_file). See the module-level comment
|
||||||
//! with lock files that are used to store PIDs (pidfiles).
|
//! there for potential pitfalls with lock files that are used
|
||||||
|
//! to store PIDs (pidfiles).
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
@@ -81,7 +82,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`] module for what the variants mean
|
/// Check out the [`pid_file`](crate::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.
|
||||||
|
|||||||
@@ -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(atty::is(atty::Stream::Stdout))
|
.with_ansi(false)
|
||||||
.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::disarm`] but keep in mind, if tracing is stopped, then panics will be
|
/// [`TracingPanicHookGuard::forget`] 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 {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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};
|
||||||
@@ -75,3 +76,34 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
//! 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());
|
||||||
@@ -20,23 +27,18 @@
|
|||||||
//!
|
//!
|
||||||
//! 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"]);
|
||||||
//! match check_fields_present([&extractor]) {
|
//! if let Err(missing) = check_fields_present!([&extractor]) {
|
||||||
//! Ok(()) => {},
|
//! // if you copypaste this to a custom assert method, remember to add #[track_caller]
|
||||||
//! Err(missing) => {
|
//! // to get the "user" code location for the panic.
|
||||||
//! panic!("Missing fields: {:?}", missing.into_iter().map(|f| f.name() ).collect::<Vec<_>>());
|
//! panic!("Missing fields: {missing:?}");
|
||||||
//! }
|
|
||||||
//! }
|
//! }
|
||||||
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! 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>
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use std::{
|
#[derive(Debug)]
|
||||||
collections::HashSet,
|
|
||||||
fmt::{self},
|
|
||||||
hash::{Hash, Hasher},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub enum ExtractionResult {
|
pub enum ExtractionResult {
|
||||||
Present,
|
Present,
|
||||||
Absent,
|
Absent,
|
||||||
@@ -71,51 +73,103 @@ impl<const L: usize> Extractor for MultiNameExtractor<L> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MemoryIdentity<'a>(&'a dyn Extractor);
|
/// Checks that the given extractors are satisfied with the current span hierarchy.
|
||||||
|
///
|
||||||
impl<'a> MemoryIdentity<'a> {
|
/// This should not be called directly, but used through [`check_fields_present`] which allows
|
||||||
fn as_ptr(&self) -> *const () {
|
/// `Summary::Unconfigured` only when the calling crate is being `#[cfg(test)]` as a conservative default.
|
||||||
self.0 as *const _ as *const ()
|
#[doc(hidden)]
|
||||||
}
|
pub fn check_fields_present0<const L: usize>(
|
||||||
}
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The extractor names passed as keys to [`new`].
|
|
||||||
pub fn check_fields_present<const L: usize>(
|
|
||||||
must_be_present: [&dyn Extractor; L],
|
must_be_present: [&dyn Extractor; L],
|
||||||
) -> Result<(), Vec<&dyn Extractor>> {
|
) -> Result<Summary, Vec<&dyn Extractor>> {
|
||||||
let mut missing: HashSet<MemoryIdentity> =
|
let mut missing = must_be_present.into_iter().collect::<Vec<_>>();
|
||||||
HashSet::from_iter(must_be_present.into_iter().map(|r| MemoryIdentity(r)));
|
|
||||||
let trace = tracing_error::SpanTrace::capture();
|
let trace = tracing_error::SpanTrace::capture();
|
||||||
trace.with_spans(|md, _formatted_fields| {
|
trace.with_spans(|md, _formatted_fields| {
|
||||||
missing.retain(|extractor| match extractor.0.extract(md.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::Present => false,
|
||||||
ExtractionResult::Absent => true,
|
ExtractionResult::Absent => true,
|
||||||
});
|
});
|
||||||
!missing.is_empty() // continue walking up until we've found all missing
|
|
||||||
|
// continue walking up until we've found all missing
|
||||||
|
!missing.is_empty()
|
||||||
});
|
});
|
||||||
if missing.is_empty() {
|
if missing.is_empty() {
|
||||||
Ok(())
|
Ok(Summary::FoundEverything)
|
||||||
|
} else if !tracing_subscriber_configured() {
|
||||||
|
Ok(Summary::Unconfigured)
|
||||||
} else {
|
} else {
|
||||||
Err(missing.into_iter().map(|mi| mi.0).collect())
|
// 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().
|
||||||
|
//
|
||||||
|
// another way to end up here is with RUST_LOG=pageserver=off while configuring the
|
||||||
|
// logging, though I guess in that case the SpanTrace::status() == EMPTY would be valid.
|
||||||
|
// this case is covered by test `not_found_if_tracing_error_subscriber_has_wrong_filter`.
|
||||||
|
Err(missing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks that the given extractors are satisfied with the current span hierarchy.
|
||||||
|
///
|
||||||
|
/// The macro is the preferred way of checking if fields exist while passing checks if a test does
|
||||||
|
/// not have tracing configured.
|
||||||
|
///
|
||||||
|
/// Why mangled name? Because #[macro_export] will expose it at utils::__check_fields_present.
|
||||||
|
/// However we can game a module namespaced macro for `use` purposes by re-exporting the
|
||||||
|
/// #[macro_export] exported name with an alias (below).
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! __check_fields_present {
|
||||||
|
($extractors:expr) => {{
|
||||||
|
{
|
||||||
|
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>();
|
||||||
|
});
|
||||||
|
|
||||||
|
!noop_configured
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
@@ -123,6 +177,36 @@ 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>,
|
||||||
@@ -159,7 +243,8 @@ 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();
|
||||||
check_fields_present([&setup.tenant_extractor, &setup.timeline_extractor]).unwrap();
|
let res = check_fields_present0([&setup.tenant_extractor, &setup.timeline_extractor]);
|
||||||
|
assert!(matches!(res, Ok(Summary::FoundEverything)), "{res:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -167,8 +252,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 =
|
let missing = check_fields_present0([&setup.tenant_extractor, &setup.timeline_extractor])
|
||||||
check_fields_present([&setup.tenant_extractor, &setup.timeline_extractor]).unwrap_err();
|
.unwrap_err();
|
||||||
assert_missing(missing, vec![&setup.tenant_extractor]);
|
assert_missing(missing, vec![&setup.tenant_extractor]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +270,8 @@ 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();
|
||||||
|
|
||||||
check_fields_present([&setup.tenant_extractor, &setup.timeline_extractor]).unwrap();
|
let res = check_fields_present0([&setup.tenant_extractor, &setup.timeline_extractor]);
|
||||||
|
assert!(matches!(res, Ok(Summary::FoundEverything)), "{res:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -198,7 +284,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_present([&setup.tenant_extractor]).unwrap_err();
|
let missing = check_fields_present0([&setup.tenant_extractor]).unwrap_err();
|
||||||
assert_missing(missing, vec![&setup.tenant_extractor]);
|
assert_missing(missing, vec![&setup.tenant_extractor]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +293,8 @@ 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();
|
||||||
check_fields_present([&setup.tenant_extractor]).unwrap();
|
let res = check_fields_present0([&setup.tenant_extractor]);
|
||||||
|
assert!(matches!(res, Ok(Summary::FoundEverything)), "{res:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -223,7 +310,8 @@ 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();
|
||||||
|
|
||||||
check_fields_present([&setup.tenant_extractor]).unwrap();
|
let res = check_fields_present0([&setup.tenant_extractor]);
|
||||||
|
assert!(matches!(res, Ok(Summary::FoundEverything)), "{res:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -231,7 +319,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_present([&setup.tenant_extractor]).unwrap_err();
|
let missing = check_fields_present0([&setup.tenant_extractor]).unwrap_err();
|
||||||
assert_missing(missing, vec![&setup.tenant_extractor]);
|
assert_missing(missing, vec![&setup.tenant_extractor]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,43 +333,107 @@ 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_present([&setup.tenant_extractor]).unwrap_err();
|
let missing = check_fields_present0([&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() {
|
fn tracing_error_subscriber_not_set_up_straight_line() {
|
||||||
// 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 missing = check_fields_present([&extractor]).unwrap_err();
|
let res = check_fields_present0([&extractor]);
|
||||||
assert_missing(missing, vec![&extractor]);
|
assert!(matches!(res, Ok(Summary::Unconfigured)), "{res:?}");
|
||||||
|
|
||||||
|
// 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]
|
||||||
#[should_panic]
|
fn tracing_error_subscriber_not_set_up_with_instrument() {
|
||||||
fn panics_if_tracing_error_subscriber_has_wrong_filter() {
|
// no setup
|
||||||
|
|
||||||
|
// 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_error::ErrorLayer::default().with_filter(tracing_subscriber::filter::filter_fn(
|
||||||
tracing_subscriber::filter::dynamic_filter_fn(|md, _| {
|
|md| !(md.is_span() && *md.level() == tracing::Level::INFO),
|
||||||
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 extractor = MultiNameExtractor::new("E", ["e"]);
|
let extractors: [&dyn Extractor; 1] = [&MultiNameExtractor::new("E", ["e"])];
|
||||||
let missing = check_fields_present([&extractor]).unwrap_err();
|
|
||||||
assert_missing(missing, vec![&extractor]);
|
if span.is_disabled() {
|
||||||
|
// 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:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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
|
||||||
@@ -24,6 +25,7 @@ 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
|
||||||
@@ -80,6 +82,7 @@ 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"
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
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::{Layer, LayerDescriptor, LayerFileName};
|
use pageserver::tenant::storage_layer::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<LayerDescriptor> {
|
fn build_layer_map(filename_dump: PathBuf) -> LayerMap {
|
||||||
let mut layer_map = LayerMap::<LayerDescriptor>::default();
|
let mut layer_map = LayerMap::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);
|
||||||
@@ -27,13 +28,13 @@ fn build_layer_map(filename_dump: PathBuf) -> LayerMap<LayerDescriptor> {
|
|||||||
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 = LayerDescriptor::from(fname);
|
let layer = PersistentLayerDesc::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.get_persistent_layer_desc(), Arc::new(layer));
|
updates.insert_historic(layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("min: {min_lsn}, max: {max_lsn}");
|
println!("min: {min_lsn}, max: {max_lsn}");
|
||||||
@@ -43,7 +44,7 @@ fn build_layer_map(filename_dump: PathBuf) -> LayerMap<LayerDescriptor> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a layer map query pattern for benchmarks
|
/// Construct a layer map query pattern for benchmarks
|
||||||
fn uniform_query_pattern(layer_map: &LayerMap<LayerDescriptor>) -> Vec<(Key, Lsn)> {
|
fn uniform_query_pattern(layer_map: &LayerMap) -> 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
|
||||||
@@ -69,7 +70,7 @@ fn uniform_query_pattern(layer_map: &LayerMap<LayerDescriptor>) -> 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<LayerDescriptor>, _lsn: Lsn) -> KeyPartitioning {
|
fn uniform_key_partitioning(layer_map: &LayerMap, _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,
|
||||||
@@ -209,13 +210,15 @@ 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 = LayerDescriptor {
|
let layer = PersistentLayerDesc::new_img(
|
||||||
key: zero.add(10 * i32)..zero.add(10 * i32 + 1),
|
TenantId::generate(),
|
||||||
lsn: Lsn(i)..Lsn(i + 1),
|
TimelineId::generate(),
|
||||||
is_incremental: false,
|
zero.add(10 * i32)..zero.add(10 * i32 + 1),
|
||||||
short_id: format!("Layer {}", i),
|
Lsn(i),
|
||||||
};
|
false,
|
||||||
updates.insert_historic(layer.get_persistent_layer_desc(), Arc::new(layer));
|
0,
|
||||||
|
);
|
||||||
|
updates.insert_historic(layer);
|
||||||
}
|
}
|
||||||
updates.flush();
|
updates.flush();
|
||||||
println!("Finished layer map init in {:?}", now.elapsed());
|
println!("Finished layer map init in {:?}", now.elapsed());
|
||||||
|
|||||||
@@ -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,7 +117,8 @@ 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 margin = 0.05 * lsn_diff; // Height-dependent margin to disambiguate overlapping deltas
|
let mut ymargin = 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
|
||||||
@@ -128,7 +129,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;
|
||||||
margin = 0.05;
|
ymargin = 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),
|
||||||
@@ -137,10 +138,10 @@ pub fn main() -> Result<()> {
|
|||||||
println!(
|
println!(
|
||||||
" {}",
|
" {}",
|
||||||
rectangle(
|
rectangle(
|
||||||
key_start as f32 + stretch * margin,
|
key_start as f32 + stretch * xmargin,
|
||||||
stretch * (lsn_max as f32 - (lsn_end as f32 - margin - lsn_offset)),
|
stretch * (lsn_max as f32 - (lsn_end as f32 - ymargin - lsn_offset)),
|
||||||
key_diff as f32 - stretch * 2.0 * margin,
|
key_diff as f32 - stretch * 2.0 * xmargin,
|
||||||
stretch * (lsn_diff - 2.0 * margin)
|
stretch * (lsn_diff - 2.0 * ymargin)
|
||||||
)
|
)
|
||||||
.fill(fill)
|
.fill(fill)
|
||||||
.stroke(Stroke::Color(rgb(0, 0, 0), 0.1))
|
.stroke(Stroke::Color(rgb(0, 0, 0), 0.1))
|
||||||
|
|||||||
@@ -19,12 +19,6 @@ 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;
|
||||||
|
|||||||
@@ -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 = tokio::select! {
|
let init_sizes_done = match tokio::time::timeout(timeout, &mut init_sizes_done).await {
|
||||||
_ = &mut init_sizes_done => {
|
Ok(_) => {
|
||||||
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
|
||||||
}
|
}
|
||||||
_ = tokio::time::sleep(timeout) => {
|
Err(_) => {
|
||||||
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"
|
||||||
|
|||||||
@@ -171,11 +171,13 @@ pub struct PageServerConf {
|
|||||||
|
|
||||||
pub log_format: LogFormat,
|
pub log_format: LogFormat,
|
||||||
|
|
||||||
/// Number of concurrent [`Tenant::gather_size_inputs`] allowed.
|
/// Number of concurrent [`Tenant::gather_size_inputs`](crate::tenant::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.
|
||||||
@@ -570,21 +572,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, timeline_id: &TimelineId, tenant_id: &TenantId) -> PathBuf {
|
pub fn timeline_path(&self, tenant_id: &TenantId, timeline_id: &TimelineId) -> PathBuf {
|
||||||
self.timelines_path(tenant_id).join(timeline_id.to_string())
|
self.timelines_path(tenant_id).join(timeline_id.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +596,7 @@ impl PageServerConf {
|
|||||||
timeline_id: TimelineId,
|
timeline_id: TimelineId,
|
||||||
) -> PathBuf {
|
) -> PathBuf {
|
||||||
path_with_suffix_extension(
|
path_with_suffix_extension(
|
||||||
self.timeline_path(&timeline_id, &tenant_id),
|
self.timeline_path(&tenant_id, &timeline_id),
|
||||||
TIMELINE_UNINIT_MARK_SUFFIX,
|
TIMELINE_UNINIT_MARK_SUFFIX,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -617,8 +619,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, timeline_id: TimelineId, tenant_id: TenantId) -> PathBuf {
|
pub fn metadata_path(&self, tenant_id: &TenantId, timeline_id: &TimelineId) -> PathBuf {
|
||||||
self.timeline_path(&timeline_id, &tenant_id)
|
self.timeline_path(tenant_id, timeline_id)
|
||||||
.join(METADATA_FILE_NAME)
|
.join(METADATA_FILE_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -993,6 +995,8 @@ 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,
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ const RESIDENT_SIZE: &str = "resident_size";
|
|||||||
const REMOTE_STORAGE_SIZE: &str = "remote_storage_size";
|
const REMOTE_STORAGE_SIZE: &str = "remote_storage_size";
|
||||||
const TIMELINE_LOGICAL_SIZE: &str = "timeline_logical_size";
|
const TIMELINE_LOGICAL_SIZE: &str = "timeline_logical_size";
|
||||||
|
|
||||||
|
const DEFAULT_HTTP_REPORTING_TIMEOUT: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
struct Ids {
|
struct Ids {
|
||||||
@@ -73,7 +75,10 @@ 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::Client::new();
|
let client = reqwest::ClientBuilder::new()
|
||||||
|
.timeout(DEFAULT_HTTP_REPORTING_TIMEOUT)
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create http client with timeout");
|
||||||
let mut cached_metrics: HashMap<PageserverConsumptionMetricsKey, u64> = HashMap::new();
|
let mut cached_metrics: HashMap<PageserverConsumptionMetricsKey, u64> = 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();
|
||||||
|
|
||||||
@@ -83,7 +88,7 @@ pub async fn collect_metrics(
|
|||||||
info!("collect_metrics received cancellation request");
|
info!("collect_metrics received cancellation request");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
},
|
},
|
||||||
_ = ticker.tick() => {
|
tick_at = 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;
|
||||||
@@ -93,6 +98,12 @@ 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",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,14 +234,18 @@ pub async fn collect_metrics_iteration(
|
|||||||
// 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((
|
|
||||||
PageserverConsumptionMetricsKey {
|
if tenant_synthetic_size != 0 {
|
||||||
tenant_id,
|
// only send non-zeroes because otherwise these show up as errors in logs
|
||||||
timeline_id: None,
|
current_metrics.push((
|
||||||
metric: SYNTHETIC_STORAGE_SIZE,
|
PageserverConsumptionMetricsKey {
|
||||||
},
|
tenant_id,
|
||||||
tenant_synthetic_size,
|
timeline_id: None,
|
||||||
));
|
metric: SYNTHETIC_STORAGE_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.
|
||||||
@@ -273,31 +288,42 @@ pub async fn collect_metrics_iteration(
|
|||||||
})
|
})
|
||||||
.expect("PageserverConsumptionMetric should not fail serialization");
|
.expect("PageserverConsumptionMetric should not fail serialization");
|
||||||
|
|
||||||
let res = client
|
const MAX_RETRIES: u32 = 3;
|
||||||
.post(metric_collection_endpoint.clone())
|
|
||||||
.json(&chunk_json)
|
|
||||||
.send()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match res {
|
for attempt in 0..MAX_RETRIES {
|
||||||
Ok(res) => {
|
let res = client
|
||||||
if res.status().is_success() {
|
.post(metric_collection_endpoint.clone())
|
||||||
// update cached metrics after they were sent successfully
|
.json(&chunk_json)
|
||||||
for (curr_key, curr_val) in chunk.iter() {
|
.send()
|
||||||
cached_metrics.insert(curr_key.clone(), *curr_val);
|
.await;
|
||||||
}
|
|
||||||
} else {
|
match res {
|
||||||
error!("metrics endpoint refused the sent metrics: {:?}", res);
|
Ok(res) => {
|
||||||
for metric in chunk_to_send.iter() {
|
if res.status().is_success() {
|
||||||
// Report if the metric value is suspiciously large
|
// update cached metrics after they were sent successfully
|
||||||
if metric.value > (1u64 << 40) {
|
for (curr_key, curr_val) in chunk.iter() {
|
||||||
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,7 +343,7 @@ pub async fn calculate_synthetic_size_worker(
|
|||||||
_ = task_mgr::shutdown_watcher() => {
|
_ = task_mgr::shutdown_watcher() => {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
},
|
},
|
||||||
_ = ticker.tick() => {
|
tick_at = ticker.tick() => {
|
||||||
|
|
||||||
let tenants = match mgr::list_tenants().await {
|
let tenants = match mgr::list_tenants().await {
|
||||||
Ok(tenants) => tenants,
|
Ok(tenants) => tenants,
|
||||||
@@ -343,6 +369,12 @@ 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",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,6 +179,9 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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},
|
tenant::{self, storage_layer::PersistentLayer, timeline::EvictionError, Timeline},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -110,7 +110,6 @@ 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(())
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -126,13 +125,16 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,12 +166,11 @@ async fn disk_usage_eviction_task(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let sleep_until = start + task_config.period;
|
let sleep_until = start + task_config.period;
|
||||||
tokio::select! {
|
if tokio::time::timeout_at(sleep_until, cancel.cancelled())
|
||||||
_ = tokio::time::sleep_until(sleep_until) => {},
|
.await
|
||||||
_ = cancel.cancelled() => {
|
.is_ok()
|
||||||
info!("shutting down");
|
{
|
||||||
break
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,7 +305,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, parition={:?}, tenant={} timeline={} layer={}",
|
"cand {}/{}: size={}, no_access_for={}us, partition={:?}, {}/{}/{}",
|
||||||
i + 1,
|
i + 1,
|
||||||
candidates.len(),
|
candidates.len(),
|
||||||
candidate.layer.file_size(),
|
candidate.layer.file_size(),
|
||||||
@@ -314,7 +315,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.filename().file_name(),
|
candidate.layer,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,13 +390,22 @@ 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(true)) => {
|
Some(Ok(())) => {
|
||||||
usage_assumed.add_available_bytes(layer.file_size());
|
usage_assumed.add_available_bytes(layer.file_size());
|
||||||
}
|
}
|
||||||
Some(Ok(false)) => {
|
Some(Err(EvictionError::CannotEvictRemoteLayer)) => {
|
||||||
// this is:
|
unreachable!("get_local_layers_for_disk_usage_eviction finds only local layers")
|
||||||
// - Replacement::{NotFound, Unexpected}
|
}
|
||||||
// - it cannot be is_remote_layer, filtered already
|
Some(Err(EvictionError::FileNotFound)) => {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -403,10 +413,6 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -722,6 +722,12 @@ 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:
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ use super::models::{
|
|||||||
TimelineCreateRequest, TimelineGcRequest, TimelineInfo,
|
TimelineCreateRequest, TimelineGcRequest, TimelineInfo,
|
||||||
};
|
};
|
||||||
use crate::context::{DownloadBehavior, RequestContext};
|
use crate::context::{DownloadBehavior, RequestContext};
|
||||||
use crate::disk_usage_eviction_task;
|
|
||||||
use crate::metrics::{StorageTimeOperation, STORAGE_TIME_GLOBAL};
|
use crate::metrics::{StorageTimeOperation, STORAGE_TIME_GLOBAL};
|
||||||
use crate::pgdatadir_mapping::LsnForTimestamp;
|
use crate::pgdatadir_mapping::LsnForTimestamp;
|
||||||
use crate::task_mgr::TaskKind;
|
use crate::task_mgr::TaskKind;
|
||||||
@@ -35,6 +34,7 @@ use crate::tenant::size::ModelInputs;
|
|||||||
use crate::tenant::storage_layer::LayerAccessStatsReset;
|
use crate::tenant::storage_layer::LayerAccessStatsReset;
|
||||||
use crate::tenant::{LogicalSizeCalculationCause, PageReconstructError, Timeline};
|
use crate::tenant::{LogicalSizeCalculationCause, PageReconstructError, Timeline};
|
||||||
use crate::{config::PageServerConf, tenant::mgr};
|
use crate::{config::PageServerConf, tenant::mgr};
|
||||||
|
use crate::{disk_usage_eviction_task, tenant};
|
||||||
use utils::{
|
use utils::{
|
||||||
auth::JwtAuth,
|
auth::JwtAuth,
|
||||||
http::{
|
http::{
|
||||||
@@ -328,18 +328,25 @@ async fn timeline_create_handler(
|
|||||||
&ctx,
|
&ctx,
|
||||||
)
|
)
|
||||||
.await {
|
.await {
|
||||||
Ok(Some(new_timeline)) => {
|
Ok(new_timeline) => {
|
||||||
// Created. Construct a TimelineInfo for it.
|
// Created. Construct a TimelineInfo for it.
|
||||||
let timeline_info = build_timeline_info_common(&new_timeline, &ctx)
|
let timeline_info = build_timeline_info_common(&new_timeline, &ctx)
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::InternalServerError)?;
|
.map_err(ApiError::InternalServerError)?;
|
||||||
json_response(StatusCode::CREATED, timeline_info)
|
json_response(StatusCode::CREATED, timeline_info)
|
||||||
}
|
}
|
||||||
Ok(None) => json_response(StatusCode::CONFLICT, ()), // timeline already exists
|
Err(tenant::CreateTimelineError::AlreadyExists) => {
|
||||||
Err(err) => Err(ApiError::InternalServerError(err)),
|
json_response(StatusCode::CONFLICT, ())
|
||||||
|
}
|
||||||
|
Err(tenant::CreateTimelineError::AncestorLsn(err)) => {
|
||||||
|
json_response(StatusCode::NOT_ACCEPTABLE, HttpErrorBody::from_msg(
|
||||||
|
format!("{err:#}")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(tenant::CreateTimelineError::Other(err)) => Err(ApiError::InternalServerError(err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.instrument(info_span!("timeline_create", tenant = %tenant_id, timeline_id = %new_timeline_id, lsn=?request_data.ancestor_start_lsn, pg_version=?request_data.pg_version))
|
.instrument(info_span!("timeline_create", %tenant_id, timeline_id = %new_timeline_id, lsn=?request_data.ancestor_start_lsn, pg_version=?request_data.pg_version))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +381,7 @@ async fn timeline_list_handler(
|
|||||||
}
|
}
|
||||||
Ok::<Vec<TimelineInfo>, ApiError>(response_data)
|
Ok::<Vec<TimelineInfo>, ApiError>(response_data)
|
||||||
}
|
}
|
||||||
.instrument(info_span!("timeline_list", tenant = %tenant_id))
|
.instrument(info_span!("timeline_list", %tenant_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
json_response(StatusCode::OK, response_data)
|
json_response(StatusCode::OK, response_data)
|
||||||
@@ -411,7 +418,7 @@ async fn timeline_detail_handler(
|
|||||||
|
|
||||||
Ok::<_, ApiError>(timeline_info)
|
Ok::<_, ApiError>(timeline_info)
|
||||||
}
|
}
|
||||||
.instrument(info_span!("timeline_detail", tenant = %tenant_id, timeline = %timeline_id))
|
.instrument(info_span!("timeline_detail", %tenant_id, %timeline_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
json_response(StatusCode::OK, timeline_info)
|
json_response(StatusCode::OK, timeline_info)
|
||||||
@@ -472,7 +479,7 @@ async fn tenant_attach_handler(
|
|||||||
remote_storage.clone(),
|
remote_storage.clone(),
|
||||||
&ctx,
|
&ctx,
|
||||||
)
|
)
|
||||||
.instrument(info_span!("tenant_attach", tenant = %tenant_id))
|
.instrument(info_span!("tenant_attach", %tenant_id))
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
return Err(ApiError::BadRequest(anyhow!(
|
return Err(ApiError::BadRequest(anyhow!(
|
||||||
@@ -494,7 +501,7 @@ async fn timeline_delete_handler(
|
|||||||
let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Warn);
|
let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Warn);
|
||||||
|
|
||||||
mgr::delete_timeline(tenant_id, timeline_id, &ctx)
|
mgr::delete_timeline(tenant_id, timeline_id, &ctx)
|
||||||
.instrument(info_span!("timeline_delete", tenant = %tenant_id, timeline = %timeline_id))
|
.instrument(info_span!("timeline_delete", %tenant_id, %timeline_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// FIXME: needs to be an error for console to retry it. Ideally Accepted should be used and retried until 404.
|
// FIXME: needs to be an error for console to retry it. Ideally Accepted should be used and retried until 404.
|
||||||
@@ -512,7 +519,7 @@ async fn tenant_detach_handler(
|
|||||||
let state = get_state(&request);
|
let state = get_state(&request);
|
||||||
let conf = state.conf;
|
let conf = state.conf;
|
||||||
mgr::detach_tenant(conf, tenant_id, detach_ignored.unwrap_or(false))
|
mgr::detach_tenant(conf, tenant_id, detach_ignored.unwrap_or(false))
|
||||||
.instrument(info_span!("tenant_detach", tenant = %tenant_id))
|
.instrument(info_span!("tenant_detach", %tenant_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
json_response(StatusCode::OK, ())
|
json_response(StatusCode::OK, ())
|
||||||
@@ -535,7 +542,7 @@ async fn tenant_load_handler(
|
|||||||
state.remote_storage.clone(),
|
state.remote_storage.clone(),
|
||||||
&ctx,
|
&ctx,
|
||||||
)
|
)
|
||||||
.instrument(info_span!("load", tenant = %tenant_id))
|
.instrument(info_span!("load", %tenant_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
json_response(StatusCode::ACCEPTED, ())
|
json_response(StatusCode::ACCEPTED, ())
|
||||||
@@ -551,7 +558,7 @@ async fn tenant_ignore_handler(
|
|||||||
let state = get_state(&request);
|
let state = get_state(&request);
|
||||||
let conf = state.conf;
|
let conf = state.conf;
|
||||||
mgr::ignore_tenant(conf, tenant_id)
|
mgr::ignore_tenant(conf, tenant_id)
|
||||||
.instrument(info_span!("ignore_tenant", tenant = %tenant_id))
|
.instrument(info_span!("ignore_tenant", %tenant_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
json_response(StatusCode::OK, ())
|
json_response(StatusCode::OK, ())
|
||||||
@@ -604,7 +611,7 @@ async fn tenant_status(
|
|||||||
attachment_status: state.attachment_status(),
|
attachment_status: state.attachment_status(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.instrument(info_span!("tenant_status_handler", tenant = %tenant_id))
|
.instrument(info_span!("tenant_status_handler", %tenant_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
json_response(StatusCode::OK, tenant_info)
|
json_response(StatusCode::OK, tenant_info)
|
||||||
@@ -843,7 +850,7 @@ async fn tenant_create_handler(
|
|||||||
state.remote_storage.clone(),
|
state.remote_storage.clone(),
|
||||||
&ctx,
|
&ctx,
|
||||||
)
|
)
|
||||||
.instrument(info_span!("tenant_create", tenant = ?target_tenant_id))
|
.instrument(info_span!("tenant_create", tenant_id = %target_tenant_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// We created the tenant. Existing API semantics are that the tenant
|
// We created the tenant. Existing API semantics are that the tenant
|
||||||
@@ -905,7 +912,7 @@ async fn update_tenant_config_handler(
|
|||||||
|
|
||||||
let state = get_state(&request);
|
let state = get_state(&request);
|
||||||
mgr::set_new_tenant_config(state.conf, tenant_conf, tenant_id)
|
mgr::set_new_tenant_config(state.conf, tenant_conf, tenant_id)
|
||||||
.instrument(info_span!("tenant_config", tenant = ?tenant_id))
|
.instrument(info_span!("tenant_config", %tenant_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
json_response(StatusCode::OK, ())
|
json_response(StatusCode::OK, ())
|
||||||
@@ -1136,7 +1143,7 @@ async fn disk_usage_eviction_run(
|
|||||||
let Some(storage) = state.remote_storage.clone() else {
|
let Some(storage) = state.remote_storage.clone() else {
|
||||||
return Err(ApiError::InternalServerError(anyhow::anyhow!(
|
return Err(ApiError::InternalServerError(anyhow::anyhow!(
|
||||||
"remote storage not configured, cannot run eviction iteration"
|
"remote storage not configured, cannot run eviction iteration"
|
||||||
)))
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let state = state.disk_usage_eviction_state.clone();
|
let state = state.disk_usage_eviction_state.clone();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use metrics::metric_vec_duration::DurationResultObserver;
|
use metrics::metric_vec_duration::DurationResultObserver;
|
||||||
use metrics::{
|
use metrics::{
|
||||||
register_counter_vec, register_histogram, register_histogram_vec, register_int_counter,
|
register_counter_vec, register_histogram, register_histogram_vec, register_int_counter,
|
||||||
register_int_counter_vec, register_int_gauge, register_int_gauge_vec, register_uint_gauge_vec,
|
register_int_counter_vec, register_int_gauge, register_int_gauge_vec, register_uint_gauge,
|
||||||
Counter, CounterVec, Histogram, HistogramVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec,
|
register_uint_gauge_vec, Counter, CounterVec, Histogram, HistogramVec, IntCounter,
|
||||||
UIntGauge, UIntGaugeVec,
|
IntCounterVec, IntGauge, IntGaugeVec, UIntGauge, UIntGaugeVec,
|
||||||
};
|
};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use pageserver_api::models::TenantState;
|
use pageserver_api::models::TenantState;
|
||||||
@@ -130,6 +130,122 @@ pub static MATERIALIZED_PAGE_CACHE_HIT: Lazy<IntCounter> = Lazy::new(|| {
|
|||||||
.expect("failed to define a metric")
|
.expect("failed to define a metric")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub struct PageCacheMetrics {
|
||||||
|
pub read_accesses_materialized_page: IntCounter,
|
||||||
|
pub read_accesses_ephemeral: IntCounter,
|
||||||
|
pub read_accesses_immutable: IntCounter,
|
||||||
|
|
||||||
|
pub read_hits_ephemeral: IntCounter,
|
||||||
|
pub read_hits_immutable: IntCounter,
|
||||||
|
pub read_hits_materialized_page_exact: IntCounter,
|
||||||
|
pub read_hits_materialized_page_older_lsn: IntCounter,
|
||||||
|
}
|
||||||
|
|
||||||
|
static PAGE_CACHE_READ_HITS: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||||
|
register_int_counter_vec!(
|
||||||
|
"pageserver_page_cache_read_hits_total",
|
||||||
|
"Number of read accesses to the page cache that hit",
|
||||||
|
&["key_kind", "hit_kind"]
|
||||||
|
)
|
||||||
|
.expect("failed to define a metric")
|
||||||
|
});
|
||||||
|
|
||||||
|
static PAGE_CACHE_READ_ACCESSES: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||||
|
register_int_counter_vec!(
|
||||||
|
"pageserver_page_cache_read_accesses_total",
|
||||||
|
"Number of read accesses to the page cache",
|
||||||
|
&["key_kind"]
|
||||||
|
)
|
||||||
|
.expect("failed to define a metric")
|
||||||
|
});
|
||||||
|
|
||||||
|
pub static PAGE_CACHE: Lazy<PageCacheMetrics> = Lazy::new(|| PageCacheMetrics {
|
||||||
|
read_accesses_materialized_page: {
|
||||||
|
PAGE_CACHE_READ_ACCESSES
|
||||||
|
.get_metric_with_label_values(&["materialized_page"])
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
|
||||||
|
read_accesses_ephemeral: {
|
||||||
|
PAGE_CACHE_READ_ACCESSES
|
||||||
|
.get_metric_with_label_values(&["ephemeral"])
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
|
||||||
|
read_accesses_immutable: {
|
||||||
|
PAGE_CACHE_READ_ACCESSES
|
||||||
|
.get_metric_with_label_values(&["immutable"])
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
|
||||||
|
read_hits_ephemeral: {
|
||||||
|
PAGE_CACHE_READ_HITS
|
||||||
|
.get_metric_with_label_values(&["ephemeral", "-"])
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
|
||||||
|
read_hits_immutable: {
|
||||||
|
PAGE_CACHE_READ_HITS
|
||||||
|
.get_metric_with_label_values(&["immutable", "-"])
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
|
||||||
|
read_hits_materialized_page_exact: {
|
||||||
|
PAGE_CACHE_READ_HITS
|
||||||
|
.get_metric_with_label_values(&["materialized_page", "exact"])
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
|
||||||
|
read_hits_materialized_page_older_lsn: {
|
||||||
|
PAGE_CACHE_READ_HITS
|
||||||
|
.get_metric_with_label_values(&["materialized_page", "older_lsn"])
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pub struct PageCacheSizeMetrics {
|
||||||
|
pub max_bytes: UIntGauge,
|
||||||
|
|
||||||
|
pub current_bytes_ephemeral: UIntGauge,
|
||||||
|
pub current_bytes_immutable: UIntGauge,
|
||||||
|
pub current_bytes_materialized_page: UIntGauge,
|
||||||
|
}
|
||||||
|
|
||||||
|
static PAGE_CACHE_SIZE_CURRENT_BYTES: Lazy<UIntGaugeVec> = Lazy::new(|| {
|
||||||
|
register_uint_gauge_vec!(
|
||||||
|
"pageserver_page_cache_size_current_bytes",
|
||||||
|
"Current size of the page cache in bytes, by key kind",
|
||||||
|
&["key_kind"]
|
||||||
|
)
|
||||||
|
.expect("failed to define a metric")
|
||||||
|
});
|
||||||
|
|
||||||
|
pub static PAGE_CACHE_SIZE: Lazy<PageCacheSizeMetrics> = Lazy::new(|| PageCacheSizeMetrics {
|
||||||
|
max_bytes: {
|
||||||
|
register_uint_gauge!(
|
||||||
|
"pageserver_page_cache_size_max_bytes",
|
||||||
|
"Maximum size of the page cache in bytes"
|
||||||
|
)
|
||||||
|
.expect("failed to define a metric")
|
||||||
|
},
|
||||||
|
|
||||||
|
current_bytes_ephemeral: {
|
||||||
|
PAGE_CACHE_SIZE_CURRENT_BYTES
|
||||||
|
.get_metric_with_label_values(&["ephemeral"])
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
current_bytes_immutable: {
|
||||||
|
PAGE_CACHE_SIZE_CURRENT_BYTES
|
||||||
|
.get_metric_with_label_values(&["immutable"])
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
current_bytes_materialized_page: {
|
||||||
|
PAGE_CACHE_SIZE_CURRENT_BYTES
|
||||||
|
.get_metric_with_label_values(&["materialized_page"])
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
static WAIT_LSN_TIME: Lazy<HistogramVec> = Lazy::new(|| {
|
static WAIT_LSN_TIME: Lazy<HistogramVec> = Lazy::new(|| {
|
||||||
register_histogram_vec!(
|
register_histogram_vec!(
|
||||||
"pageserver_wait_lsn_seconds",
|
"pageserver_wait_lsn_seconds",
|
||||||
@@ -204,11 +320,11 @@ pub static TENANT_STATE_METRIC: Lazy<UIntGaugeVec> = Lazy::new(|| {
|
|||||||
|
|
||||||
pub static TENANT_SYNTHETIC_SIZE_METRIC: Lazy<UIntGaugeVec> = Lazy::new(|| {
|
pub static TENANT_SYNTHETIC_SIZE_METRIC: Lazy<UIntGaugeVec> = Lazy::new(|| {
|
||||||
register_uint_gauge_vec!(
|
register_uint_gauge_vec!(
|
||||||
"pageserver_tenant_synthetic_size",
|
"pageserver_tenant_synthetic_cached_size_bytes",
|
||||||
"Synthetic size of each tenant",
|
"Synthetic size of each tenant in bytes",
|
||||||
&["tenant_id"]
|
&["tenant_id"]
|
||||||
)
|
)
|
||||||
.expect("Failed to register pageserver_tenant_synthetic_size metric")
|
.expect("Failed to register pageserver_tenant_synthetic_cached_size_bytes metric")
|
||||||
});
|
});
|
||||||
|
|
||||||
// Metrics for cloud upload. These metrics reflect data uploaded to cloud storage,
|
// Metrics for cloud upload. These metrics reflect data uploaded to cloud storage,
|
||||||
@@ -269,7 +385,7 @@ pub static UNEXPECTED_ONDEMAND_DOWNLOADS: Lazy<IntCounter> = Lazy::new(|| {
|
|||||||
.expect("failed to define a metric")
|
.expect("failed to define a metric")
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Each [`Timeline`]'s [`EVICTIONS_WITH_LOW_RESIDENCE_DURATION`] metric.
|
/// Each `Timeline`'s [`EVICTIONS_WITH_LOW_RESIDENCE_DURATION`] metric.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EvictionsWithLowResidenceDuration {
|
pub struct EvictionsWithLowResidenceDuration {
|
||||||
data_source: &'static str,
|
data_source: &'static str,
|
||||||
@@ -425,6 +541,17 @@ pub static SMGR_QUERY_TIME: Lazy<HistogramVec> = Lazy::new(|| {
|
|||||||
.expect("failed to define a metric")
|
.expect("failed to define a metric")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// keep in sync with control plane Go code so that we can validate
|
||||||
|
// compute's basebackup_ms metric with our perspective in the context of SLI/SLO.
|
||||||
|
static COMPUTE_STARTUP_BUCKETS: Lazy<[f64; 28]> = Lazy::new(|| {
|
||||||
|
// Go code uses milliseconds. Variable is called `computeStartupBuckets`
|
||||||
|
[
|
||||||
|
5, 10, 20, 30, 50, 70, 100, 120, 150, 200, 250, 300, 350, 400, 450, 500, 600, 800, 1000,
|
||||||
|
1500, 2000, 2500, 3000, 5000, 10000, 20000, 40000, 60000,
|
||||||
|
]
|
||||||
|
.map(|ms| (ms as f64) / 1000.0)
|
||||||
|
});
|
||||||
|
|
||||||
pub struct BasebackupQueryTime(HistogramVec);
|
pub struct BasebackupQueryTime(HistogramVec);
|
||||||
pub static BASEBACKUP_QUERY_TIME: Lazy<BasebackupQueryTime> = Lazy::new(|| {
|
pub static BASEBACKUP_QUERY_TIME: Lazy<BasebackupQueryTime> = Lazy::new(|| {
|
||||||
BasebackupQueryTime({
|
BasebackupQueryTime({
|
||||||
@@ -432,7 +559,7 @@ pub static BASEBACKUP_QUERY_TIME: Lazy<BasebackupQueryTime> = Lazy::new(|| {
|
|||||||
"pageserver_basebackup_query_seconds",
|
"pageserver_basebackup_query_seconds",
|
||||||
"Histogram of basebackup queries durations, by result type",
|
"Histogram of basebackup queries durations, by result type",
|
||||||
&["result"],
|
&["result"],
|
||||||
CRITICAL_OP_BUCKETS.into(),
|
COMPUTE_STARTUP_BUCKETS.to_vec(),
|
||||||
)
|
)
|
||||||
.expect("failed to define a metric")
|
.expect("failed to define a metric")
|
||||||
})
|
})
|
||||||
@@ -702,7 +829,7 @@ pub static WAL_REDO_RECORD_COUNTER: Lazy<IntCounter> = Lazy::new(|| {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Similar to [`prometheus::HistogramTimer`] but does not record on drop.
|
/// Similar to `prometheus::HistogramTimer` but does not record on drop.
|
||||||
pub struct StorageTimeMetricsTimer {
|
pub struct StorageTimeMetricsTimer {
|
||||||
metrics: StorageTimeMetrics,
|
metrics: StorageTimeMetrics,
|
||||||
start: Instant,
|
start: Instant,
|
||||||
@@ -760,7 +887,7 @@ impl StorageTimeMetrics {
|
|||||||
|
|
||||||
/// Starts timing a new operation.
|
/// Starts timing a new operation.
|
||||||
///
|
///
|
||||||
/// Note: unlike [`prometheus::HistogramTimer`] the returned timer does not record on drop.
|
/// Note: unlike `prometheus::HistogramTimer` the returned timer does not record on drop.
|
||||||
pub fn start_timer(&self) -> StorageTimeMetricsTimer {
|
pub fn start_timer(&self) -> StorageTimeMetricsTimer {
|
||||||
StorageTimeMetricsTimer::new(self.clone())
|
StorageTimeMetricsTimer::new(self.clone())
|
||||||
}
|
}
|
||||||
@@ -968,7 +1095,6 @@ impl RemoteTimelineClientMetrics {
|
|||||||
op_kind: &RemoteOpKind,
|
op_kind: &RemoteOpKind,
|
||||||
status: &'static str,
|
status: &'static str,
|
||||||
) -> Histogram {
|
) -> Histogram {
|
||||||
// XXX would be nice to have an upgradable RwLock
|
|
||||||
let mut guard = self.remote_operation_time.lock().unwrap();
|
let mut guard = self.remote_operation_time.lock().unwrap();
|
||||||
let key = (file_kind.as_str(), op_kind.as_str(), status);
|
let key = (file_kind.as_str(), op_kind.as_str(), status);
|
||||||
let metric = guard.entry(key).or_insert_with(move || {
|
let metric = guard.entry(key).or_insert_with(move || {
|
||||||
@@ -990,7 +1116,6 @@ impl RemoteTimelineClientMetrics {
|
|||||||
file_kind: &RemoteOpFileKind,
|
file_kind: &RemoteOpFileKind,
|
||||||
op_kind: &RemoteOpKind,
|
op_kind: &RemoteOpKind,
|
||||||
) -> IntGauge {
|
) -> IntGauge {
|
||||||
// XXX would be nice to have an upgradable RwLock
|
|
||||||
let mut guard = self.calls_unfinished_gauge.lock().unwrap();
|
let mut guard = self.calls_unfinished_gauge.lock().unwrap();
|
||||||
let key = (file_kind.as_str(), op_kind.as_str());
|
let key = (file_kind.as_str(), op_kind.as_str());
|
||||||
let metric = guard.entry(key).or_insert_with(move || {
|
let metric = guard.entry(key).or_insert_with(move || {
|
||||||
@@ -1011,7 +1136,6 @@ impl RemoteTimelineClientMetrics {
|
|||||||
file_kind: &RemoteOpFileKind,
|
file_kind: &RemoteOpFileKind,
|
||||||
op_kind: &RemoteOpKind,
|
op_kind: &RemoteOpKind,
|
||||||
) -> Histogram {
|
) -> Histogram {
|
||||||
// XXX would be nice to have an upgradable RwLock
|
|
||||||
let mut guard = self.calls_started_hist.lock().unwrap();
|
let mut guard = self.calls_started_hist.lock().unwrap();
|
||||||
let key = (file_kind.as_str(), op_kind.as_str());
|
let key = (file_kind.as_str(), op_kind.as_str());
|
||||||
let metric = guard.entry(key).or_insert_with(move || {
|
let metric = guard.entry(key).or_insert_with(move || {
|
||||||
@@ -1032,7 +1156,6 @@ impl RemoteTimelineClientMetrics {
|
|||||||
file_kind: &RemoteOpFileKind,
|
file_kind: &RemoteOpFileKind,
|
||||||
op_kind: &RemoteOpKind,
|
op_kind: &RemoteOpKind,
|
||||||
) -> IntCounter {
|
) -> IntCounter {
|
||||||
// XXX would be nice to have an upgradable RwLock
|
|
||||||
let mut guard = self.bytes_started_counter.lock().unwrap();
|
let mut guard = self.bytes_started_counter.lock().unwrap();
|
||||||
let key = (file_kind.as_str(), op_kind.as_str());
|
let key = (file_kind.as_str(), op_kind.as_str());
|
||||||
let metric = guard.entry(key).or_insert_with(move || {
|
let metric = guard.entry(key).or_insert_with(move || {
|
||||||
@@ -1053,7 +1176,6 @@ impl RemoteTimelineClientMetrics {
|
|||||||
file_kind: &RemoteOpFileKind,
|
file_kind: &RemoteOpFileKind,
|
||||||
op_kind: &RemoteOpKind,
|
op_kind: &RemoteOpKind,
|
||||||
) -> IntCounter {
|
) -> IntCounter {
|
||||||
// XXX would be nice to have an upgradable RwLock
|
|
||||||
let mut guard = self.bytes_finished_counter.lock().unwrap();
|
let mut guard = self.bytes_finished_counter.lock().unwrap();
|
||||||
let key = (file_kind.as_str(), op_kind.as_str());
|
let key = (file_kind.as_str(), op_kind.as_str());
|
||||||
let metric = guard.entry(key).or_insert_with(move || {
|
let metric = guard.entry(key).or_insert_with(move || {
|
||||||
@@ -1145,7 +1267,7 @@ impl RemoteTimelineClientMetrics {
|
|||||||
/// Update the metrics that change when a call to the remote timeline client instance starts.
|
/// Update the metrics that change when a call to the remote timeline client instance starts.
|
||||||
///
|
///
|
||||||
/// Drop the returned guard object once the operation is finished to updates corresponding metrics that track completions.
|
/// Drop the returned guard object once the operation is finished to updates corresponding metrics that track completions.
|
||||||
/// Or, use [`RemoteTimelineClientCallMetricGuard::will_decrement_manually`] and [`call_end`] if that
|
/// Or, use [`RemoteTimelineClientCallMetricGuard::will_decrement_manually`] and [`call_end`](Self::call_end) if that
|
||||||
/// is more suitable.
|
/// is more suitable.
|
||||||
/// Never do both.
|
/// Never do both.
|
||||||
pub(crate) fn call_begin(
|
pub(crate) fn call_begin(
|
||||||
@@ -1178,7 +1300,7 @@ impl RemoteTimelineClientMetrics {
|
|||||||
|
|
||||||
/// Manually udpate the metrics that track completions, instead of using the guard object.
|
/// Manually udpate the metrics that track completions, instead of using the guard object.
|
||||||
/// Using the guard object is generally preferable.
|
/// Using the guard object is generally preferable.
|
||||||
/// See [`call_begin`] for more context.
|
/// See [`call_begin`](Self::call_begin) for more context.
|
||||||
pub(crate) fn call_end(
|
pub(crate) fn call_end(
|
||||||
&self,
|
&self,
|
||||||
file_kind: &RemoteOpFileKind,
|
file_kind: &RemoteOpFileKind,
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ use utils::{
|
|||||||
lsn::Lsn,
|
lsn::Lsn,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::repository::Key;
|
|
||||||
use crate::tenant::writeback_ephemeral_file;
|
use crate::tenant::writeback_ephemeral_file;
|
||||||
|
use crate::{metrics::PageCacheSizeMetrics, repository::Key};
|
||||||
|
|
||||||
static PAGE_CACHE: OnceCell<PageCache> = OnceCell::new();
|
static PAGE_CACHE: OnceCell<PageCache> = OnceCell::new();
|
||||||
const TEST_PAGE_CACHE_SIZE: usize = 50;
|
const TEST_PAGE_CACHE_SIZE: usize = 50;
|
||||||
@@ -187,6 +187,8 @@ pub struct PageCache {
|
|||||||
/// Index of the next candidate to evict, for the Clock replacement algorithm.
|
/// Index of the next candidate to evict, for the Clock replacement algorithm.
|
||||||
/// This is interpreted modulo the page cache size.
|
/// This is interpreted modulo the page cache size.
|
||||||
next_evict_slot: AtomicUsize,
|
next_evict_slot: AtomicUsize,
|
||||||
|
|
||||||
|
size_metrics: &'static PageCacheSizeMetrics,
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -313,6 +315,10 @@ impl PageCache {
|
|||||||
key: &Key,
|
key: &Key,
|
||||||
lsn: Lsn,
|
lsn: Lsn,
|
||||||
) -> Option<(Lsn, PageReadGuard)> {
|
) -> Option<(Lsn, PageReadGuard)> {
|
||||||
|
crate::metrics::PAGE_CACHE
|
||||||
|
.read_accesses_materialized_page
|
||||||
|
.inc();
|
||||||
|
|
||||||
let mut cache_key = CacheKey::MaterializedPage {
|
let mut cache_key = CacheKey::MaterializedPage {
|
||||||
hash_key: MaterializedPageHashKey {
|
hash_key: MaterializedPageHashKey {
|
||||||
tenant_id,
|
tenant_id,
|
||||||
@@ -323,8 +329,21 @@ impl PageCache {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(guard) = self.try_lock_for_read(&mut cache_key) {
|
if let Some(guard) = self.try_lock_for_read(&mut cache_key) {
|
||||||
if let CacheKey::MaterializedPage { hash_key: _, lsn } = cache_key {
|
if let CacheKey::MaterializedPage {
|
||||||
Some((lsn, guard))
|
hash_key: _,
|
||||||
|
lsn: available_lsn,
|
||||||
|
} = cache_key
|
||||||
|
{
|
||||||
|
if available_lsn == lsn {
|
||||||
|
crate::metrics::PAGE_CACHE
|
||||||
|
.read_hits_materialized_page_exact
|
||||||
|
.inc();
|
||||||
|
} else {
|
||||||
|
crate::metrics::PAGE_CACHE
|
||||||
|
.read_hits_materialized_page_older_lsn
|
||||||
|
.inc();
|
||||||
|
}
|
||||||
|
Some((available_lsn, guard))
|
||||||
} else {
|
} else {
|
||||||
panic!("unexpected key type in slot");
|
panic!("unexpected key type in slot");
|
||||||
}
|
}
|
||||||
@@ -499,11 +518,31 @@ impl PageCache {
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
fn lock_for_read(&self, cache_key: &mut CacheKey) -> anyhow::Result<ReadBufResult> {
|
fn lock_for_read(&self, cache_key: &mut CacheKey) -> anyhow::Result<ReadBufResult> {
|
||||||
|
let (read_access, hit) = match cache_key {
|
||||||
|
CacheKey::MaterializedPage { .. } => {
|
||||||
|
unreachable!("Materialized pages use lookup_materialized_page")
|
||||||
|
}
|
||||||
|
CacheKey::EphemeralPage { .. } => (
|
||||||
|
&crate::metrics::PAGE_CACHE.read_accesses_ephemeral,
|
||||||
|
&crate::metrics::PAGE_CACHE.read_hits_ephemeral,
|
||||||
|
),
|
||||||
|
CacheKey::ImmutableFilePage { .. } => (
|
||||||
|
&crate::metrics::PAGE_CACHE.read_accesses_immutable,
|
||||||
|
&crate::metrics::PAGE_CACHE.read_hits_immutable,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
read_access.inc();
|
||||||
|
|
||||||
|
let mut is_first_iteration = true;
|
||||||
loop {
|
loop {
|
||||||
// First check if the key already exists in the cache.
|
// First check if the key already exists in the cache.
|
||||||
if let Some(read_guard) = self.try_lock_for_read(cache_key) {
|
if let Some(read_guard) = self.try_lock_for_read(cache_key) {
|
||||||
|
if is_first_iteration {
|
||||||
|
hit.inc();
|
||||||
|
}
|
||||||
return Ok(ReadBufResult::Found(read_guard));
|
return Ok(ReadBufResult::Found(read_guard));
|
||||||
}
|
}
|
||||||
|
is_first_iteration = false;
|
||||||
|
|
||||||
// Not found. Find a victim buffer
|
// Not found. Find a victim buffer
|
||||||
let (slot_idx, mut inner) =
|
let (slot_idx, mut inner) =
|
||||||
@@ -681,6 +720,9 @@ impl PageCache {
|
|||||||
|
|
||||||
if let Ok(version_idx) = versions.binary_search_by_key(old_lsn, |v| v.lsn) {
|
if let Ok(version_idx) = versions.binary_search_by_key(old_lsn, |v| v.lsn) {
|
||||||
versions.remove(version_idx);
|
versions.remove(version_idx);
|
||||||
|
self.size_metrics
|
||||||
|
.current_bytes_materialized_page
|
||||||
|
.sub_page_sz(1);
|
||||||
if versions.is_empty() {
|
if versions.is_empty() {
|
||||||
old_entry.remove_entry();
|
old_entry.remove_entry();
|
||||||
}
|
}
|
||||||
@@ -693,11 +735,13 @@ impl PageCache {
|
|||||||
let mut map = self.ephemeral_page_map.write().unwrap();
|
let mut map = self.ephemeral_page_map.write().unwrap();
|
||||||
map.remove(&(*file_id, *blkno))
|
map.remove(&(*file_id, *blkno))
|
||||||
.expect("could not find old key in mapping");
|
.expect("could not find old key in mapping");
|
||||||
|
self.size_metrics.current_bytes_ephemeral.sub_page_sz(1);
|
||||||
}
|
}
|
||||||
CacheKey::ImmutableFilePage { file_id, blkno } => {
|
CacheKey::ImmutableFilePage { file_id, blkno } => {
|
||||||
let mut map = self.immutable_page_map.write().unwrap();
|
let mut map = self.immutable_page_map.write().unwrap();
|
||||||
map.remove(&(*file_id, *blkno))
|
map.remove(&(*file_id, *blkno))
|
||||||
.expect("could not find old key in mapping");
|
.expect("could not find old key in mapping");
|
||||||
|
self.size_metrics.current_bytes_immutable.sub_page_sz(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -725,6 +769,9 @@ impl PageCache {
|
|||||||
slot_idx,
|
slot_idx,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
self.size_metrics
|
||||||
|
.current_bytes_materialized_page
|
||||||
|
.add_page_sz(1);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -735,6 +782,7 @@ impl PageCache {
|
|||||||
Entry::Occupied(entry) => Some(*entry.get()),
|
Entry::Occupied(entry) => Some(*entry.get()),
|
||||||
Entry::Vacant(entry) => {
|
Entry::Vacant(entry) => {
|
||||||
entry.insert(slot_idx);
|
entry.insert(slot_idx);
|
||||||
|
self.size_metrics.current_bytes_ephemeral.add_page_sz(1);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -745,6 +793,7 @@ impl PageCache {
|
|||||||
Entry::Occupied(entry) => Some(*entry.get()),
|
Entry::Occupied(entry) => Some(*entry.get()),
|
||||||
Entry::Vacant(entry) => {
|
Entry::Vacant(entry) => {
|
||||||
entry.insert(slot_idx);
|
entry.insert(slot_idx);
|
||||||
|
self.size_metrics.current_bytes_immutable.add_page_sz(1);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -844,6 +893,12 @@ impl PageCache {
|
|||||||
|
|
||||||
let page_buffer = Box::leak(vec![0u8; num_pages * PAGE_SZ].into_boxed_slice());
|
let page_buffer = Box::leak(vec![0u8; num_pages * PAGE_SZ].into_boxed_slice());
|
||||||
|
|
||||||
|
let size_metrics = &crate::metrics::PAGE_CACHE_SIZE;
|
||||||
|
size_metrics.max_bytes.set_page_sz(num_pages);
|
||||||
|
size_metrics.current_bytes_ephemeral.set_page_sz(0);
|
||||||
|
size_metrics.current_bytes_immutable.set_page_sz(0);
|
||||||
|
size_metrics.current_bytes_materialized_page.set_page_sz(0);
|
||||||
|
|
||||||
let slots = page_buffer
|
let slots = page_buffer
|
||||||
.chunks_exact_mut(PAGE_SZ)
|
.chunks_exact_mut(PAGE_SZ)
|
||||||
.map(|chunk| {
|
.map(|chunk| {
|
||||||
@@ -866,6 +921,30 @@ impl PageCache {
|
|||||||
immutable_page_map: Default::default(),
|
immutable_page_map: Default::default(),
|
||||||
slots,
|
slots,
|
||||||
next_evict_slot: AtomicUsize::new(0),
|
next_evict_slot: AtomicUsize::new(0),
|
||||||
|
size_metrics,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait PageSzBytesMetric {
|
||||||
|
fn set_page_sz(&self, count: usize);
|
||||||
|
fn add_page_sz(&self, count: usize);
|
||||||
|
fn sub_page_sz(&self, count: usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn count_times_page_sz(count: usize) -> u64 {
|
||||||
|
u64::try_from(count).unwrap() * u64::try_from(PAGE_SZ).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PageSzBytesMetric for metrics::UIntGauge {
|
||||||
|
fn set_page_sz(&self, count: usize) {
|
||||||
|
self.set(count_times_page_sz(count));
|
||||||
|
}
|
||||||
|
fn add_page_sz(&self, count: usize) {
|
||||||
|
self.add(count_times_page_sz(count));
|
||||||
|
}
|
||||||
|
fn sub_page_sz(&self, count: usize) {
|
||||||
|
self.sub(count_times_page_sz(count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use async_compression::tokio::write::GzipEncoder;
|
||||||
use bytes::Buf;
|
use bytes::Buf;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
@@ -31,8 +32,10 @@ use std::str;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
use tokio_util::io::StreamReader;
|
use tokio_util::io::StreamReader;
|
||||||
|
use tracing::field;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
use utils::id::ConnectionId;
|
use utils::id::ConnectionId;
|
||||||
use utils::{
|
use utils::{
|
||||||
@@ -51,6 +54,7 @@ use crate::metrics::{LIVE_CONNECTIONS_COUNT, SMGR_QUERY_TIME};
|
|||||||
use crate::task_mgr;
|
use crate::task_mgr;
|
||||||
use crate::task_mgr::TaskKind;
|
use crate::task_mgr::TaskKind;
|
||||||
use crate::tenant;
|
use crate::tenant;
|
||||||
|
use crate::tenant::debug_assert_current_span_has_tenant_and_timeline_id;
|
||||||
use crate::tenant::mgr;
|
use crate::tenant::mgr;
|
||||||
use crate::tenant::mgr::GetTenantError;
|
use crate::tenant::mgr::GetTenantError;
|
||||||
use crate::tenant::{Tenant, Timeline};
|
use crate::tenant::{Tenant, Timeline};
|
||||||
@@ -238,6 +242,7 @@ pub async fn libpq_listener_main(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, fields(peer_addr))]
|
||||||
async fn page_service_conn_main(
|
async fn page_service_conn_main(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
broker_client: storage_broker::BrokerClientChannel,
|
broker_client: storage_broker::BrokerClientChannel,
|
||||||
@@ -260,6 +265,7 @@ async fn page_service_conn_main(
|
|||||||
.context("could not set TCP_NODELAY")?;
|
.context("could not set TCP_NODELAY")?;
|
||||||
|
|
||||||
let peer_addr = socket.peer_addr().context("get peer address")?;
|
let peer_addr = socket.peer_addr().context("get peer address")?;
|
||||||
|
tracing::Span::current().record("peer_addr", field::display(peer_addr));
|
||||||
|
|
||||||
// setup read timeout of 10 minutes. the timeout is rather arbitrary for requirements:
|
// setup read timeout of 10 minutes. the timeout is rather arbitrary for requirements:
|
||||||
// - long enough for most valid compute connections
|
// - long enough for most valid compute connections
|
||||||
@@ -362,7 +368,7 @@ impl PageServerHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, pgb, ctx))]
|
#[instrument(skip_all)]
|
||||||
async fn handle_pagerequests<IO>(
|
async fn handle_pagerequests<IO>(
|
||||||
&self,
|
&self,
|
||||||
pgb: &mut PostgresBackend<IO>,
|
pgb: &mut PostgresBackend<IO>,
|
||||||
@@ -373,6 +379,8 @@ impl PageServerHandler {
|
|||||||
where
|
where
|
||||||
IO: AsyncRead + AsyncWrite + Send + Sync + Unpin,
|
IO: AsyncRead + AsyncWrite + Send + Sync + Unpin,
|
||||||
{
|
{
|
||||||
|
debug_assert_current_span_has_tenant_and_timeline_id();
|
||||||
|
|
||||||
// NOTE: pagerequests handler exits when connection is closed,
|
// NOTE: pagerequests handler exits when connection is closed,
|
||||||
// so there is no need to reset the association
|
// so there is no need to reset the association
|
||||||
task_mgr::associate_with(Some(tenant_id), Some(timeline_id));
|
task_mgr::associate_with(Some(tenant_id), Some(timeline_id));
|
||||||
@@ -473,7 +481,7 @@ impl PageServerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
#[instrument(skip(self, pgb, ctx))]
|
#[instrument(skip_all, fields(%base_lsn, end_lsn=%_end_lsn, %pg_version))]
|
||||||
async fn handle_import_basebackup<IO>(
|
async fn handle_import_basebackup<IO>(
|
||||||
&self,
|
&self,
|
||||||
pgb: &mut PostgresBackend<IO>,
|
pgb: &mut PostgresBackend<IO>,
|
||||||
@@ -487,6 +495,8 @@ impl PageServerHandler {
|
|||||||
where
|
where
|
||||||
IO: AsyncRead + AsyncWrite + Send + Sync + Unpin,
|
IO: AsyncRead + AsyncWrite + Send + Sync + Unpin,
|
||||||
{
|
{
|
||||||
|
debug_assert_current_span_has_tenant_and_timeline_id();
|
||||||
|
|
||||||
task_mgr::associate_with(Some(tenant_id), Some(timeline_id));
|
task_mgr::associate_with(Some(tenant_id), Some(timeline_id));
|
||||||
// Create empty timeline
|
// Create empty timeline
|
||||||
info!("creating new timeline");
|
info!("creating new timeline");
|
||||||
@@ -531,7 +541,7 @@ impl PageServerHandler {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, pgb, ctx))]
|
#[instrument(skip_all, fields(%start_lsn, %end_lsn))]
|
||||||
async fn handle_import_wal<IO>(
|
async fn handle_import_wal<IO>(
|
||||||
&self,
|
&self,
|
||||||
pgb: &mut PostgresBackend<IO>,
|
pgb: &mut PostgresBackend<IO>,
|
||||||
@@ -544,6 +554,7 @@ impl PageServerHandler {
|
|||||||
where
|
where
|
||||||
IO: AsyncRead + AsyncWrite + Send + Sync + Unpin,
|
IO: AsyncRead + AsyncWrite + Send + Sync + Unpin,
|
||||||
{
|
{
|
||||||
|
debug_assert_current_span_has_tenant_and_timeline_id();
|
||||||
task_mgr::associate_with(Some(tenant_id), Some(timeline_id));
|
task_mgr::associate_with(Some(tenant_id), Some(timeline_id));
|
||||||
|
|
||||||
let timeline = get_active_tenant_timeline(tenant_id, timeline_id, &ctx).await?;
|
let timeline = get_active_tenant_timeline(tenant_id, timeline_id, &ctx).await?;
|
||||||
@@ -738,7 +749,7 @@ impl PageServerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
#[instrument(skip(self, pgb, ctx))]
|
#[instrument(skip_all, fields(?lsn, ?prev_lsn, %full_backup))]
|
||||||
async fn handle_basebackup_request<IO>(
|
async fn handle_basebackup_request<IO>(
|
||||||
&mut self,
|
&mut self,
|
||||||
pgb: &mut PostgresBackend<IO>,
|
pgb: &mut PostgresBackend<IO>,
|
||||||
@@ -747,11 +758,14 @@ impl PageServerHandler {
|
|||||||
lsn: Option<Lsn>,
|
lsn: Option<Lsn>,
|
||||||
prev_lsn: Option<Lsn>,
|
prev_lsn: Option<Lsn>,
|
||||||
full_backup: bool,
|
full_backup: bool,
|
||||||
|
gzip: bool,
|
||||||
ctx: RequestContext,
|
ctx: RequestContext,
|
||||||
) -> anyhow::Result<()>
|
) -> anyhow::Result<()>
|
||||||
where
|
where
|
||||||
IO: AsyncRead + AsyncWrite + Send + Sync + Unpin,
|
IO: AsyncRead + AsyncWrite + Send + Sync + Unpin,
|
||||||
{
|
{
|
||||||
|
debug_assert_current_span_has_tenant_and_timeline_id();
|
||||||
|
|
||||||
let started = std::time::Instant::now();
|
let started = std::time::Instant::now();
|
||||||
|
|
||||||
// check that the timeline exists
|
// check that the timeline exists
|
||||||
@@ -772,8 +786,9 @@ impl PageServerHandler {
|
|||||||
pgb.write_message_noflush(&BeMessage::CopyOutResponse)?;
|
pgb.write_message_noflush(&BeMessage::CopyOutResponse)?;
|
||||||
pgb.flush().await?;
|
pgb.flush().await?;
|
||||||
|
|
||||||
// Send a tarball of the latest layer on the timeline
|
// Send a tarball of the latest layer on the timeline. Compress if not
|
||||||
{
|
// fullbackup. TODO Compress in that case too (tests need to be updated)
|
||||||
|
if full_backup {
|
||||||
let mut writer = pgb.copyout_writer();
|
let mut writer = pgb.copyout_writer();
|
||||||
basebackup::send_basebackup_tarball(
|
basebackup::send_basebackup_tarball(
|
||||||
&mut writer,
|
&mut writer,
|
||||||
@@ -784,6 +799,40 @@ impl PageServerHandler {
|
|||||||
&ctx,
|
&ctx,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
} else {
|
||||||
|
let mut writer = pgb.copyout_writer();
|
||||||
|
if gzip {
|
||||||
|
let mut encoder = GzipEncoder::with_quality(
|
||||||
|
writer,
|
||||||
|
// NOTE using fast compression because it's on the critical path
|
||||||
|
// for compute startup. For an empty database, we get
|
||||||
|
// <100KB with this method. The Level::Best compression method
|
||||||
|
// gives us <20KB, but maybe we should add basebackup caching
|
||||||
|
// on compute shutdown first.
|
||||||
|
async_compression::Level::Fastest,
|
||||||
|
);
|
||||||
|
basebackup::send_basebackup_tarball(
|
||||||
|
&mut encoder,
|
||||||
|
&timeline,
|
||||||
|
lsn,
|
||||||
|
prev_lsn,
|
||||||
|
full_backup,
|
||||||
|
&ctx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
// shutdown the encoder to ensure the gzip footer is written
|
||||||
|
encoder.shutdown().await?;
|
||||||
|
} else {
|
||||||
|
basebackup::send_basebackup_tarball(
|
||||||
|
&mut writer,
|
||||||
|
&timeline,
|
||||||
|
lsn,
|
||||||
|
prev_lsn,
|
||||||
|
full_backup,
|
||||||
|
&ctx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pgb.write_message_noflush(&BeMessage::CopyDone)?;
|
pgb.write_message_noflush(&BeMessage::CopyDone)?;
|
||||||
@@ -862,6 +911,7 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, fields(tenant_id, timeline_id))]
|
||||||
async fn process_query(
|
async fn process_query(
|
||||||
&mut self,
|
&mut self,
|
||||||
pgb: &mut PostgresBackend<IO>,
|
pgb: &mut PostgresBackend<IO>,
|
||||||
@@ -883,6 +933,10 @@ where
|
|||||||
let timeline_id = TimelineId::from_str(params[1])
|
let timeline_id = TimelineId::from_str(params[1])
|
||||||
.with_context(|| format!("Failed to parse timeline id from {}", params[1]))?;
|
.with_context(|| format!("Failed to parse timeline id from {}", params[1]))?;
|
||||||
|
|
||||||
|
tracing::Span::current()
|
||||||
|
.record("tenant_id", field::display(tenant_id))
|
||||||
|
.record("timeline_id", field::display(timeline_id));
|
||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
self.handle_pagerequests(pgb, tenant_id, timeline_id, ctx)
|
self.handle_pagerequests(pgb, tenant_id, timeline_id, ctx)
|
||||||
@@ -902,9 +956,13 @@ where
|
|||||||
let timeline_id = TimelineId::from_str(params[1])
|
let timeline_id = TimelineId::from_str(params[1])
|
||||||
.with_context(|| format!("Failed to parse timeline id from {}", params[1]))?;
|
.with_context(|| format!("Failed to parse timeline id from {}", params[1]))?;
|
||||||
|
|
||||||
|
tracing::Span::current()
|
||||||
|
.record("tenant_id", field::display(tenant_id))
|
||||||
|
.record("timeline_id", field::display(timeline_id));
|
||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
let lsn = if params.len() == 3 {
|
let lsn = if params.len() >= 3 {
|
||||||
Some(
|
Some(
|
||||||
Lsn::from_str(params[2])
|
Lsn::from_str(params[2])
|
||||||
.with_context(|| format!("Failed to parse Lsn from {}", params[2]))?,
|
.with_context(|| format!("Failed to parse Lsn from {}", params[2]))?,
|
||||||
@@ -913,6 +971,19 @@ where
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let gzip = if params.len() >= 4 {
|
||||||
|
if params[3] == "--gzip" {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
return Err(QueryError::Other(anyhow::anyhow!(
|
||||||
|
"Parameter in position 3 unknown {}",
|
||||||
|
params[3],
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
metrics::metric_vec_duration::observe_async_block_duration_by_result(
|
metrics::metric_vec_duration::observe_async_block_duration_by_result(
|
||||||
&*crate::metrics::BASEBACKUP_QUERY_TIME,
|
&*crate::metrics::BASEBACKUP_QUERY_TIME,
|
||||||
async move {
|
async move {
|
||||||
@@ -923,6 +994,7 @@ where
|
|||||||
lsn,
|
lsn,
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
|
gzip,
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -948,6 +1020,10 @@ where
|
|||||||
let timeline_id = TimelineId::from_str(params[1])
|
let timeline_id = TimelineId::from_str(params[1])
|
||||||
.with_context(|| format!("Failed to parse timeline id from {}", params[1]))?;
|
.with_context(|| format!("Failed to parse timeline id from {}", params[1]))?;
|
||||||
|
|
||||||
|
tracing::Span::current()
|
||||||
|
.record("tenant_id", field::display(tenant_id))
|
||||||
|
.record("timeline_id", field::display(timeline_id));
|
||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
let timeline = get_active_tenant_timeline(tenant_id, timeline_id, &ctx).await?;
|
let timeline = get_active_tenant_timeline(tenant_id, timeline_id, &ctx).await?;
|
||||||
|
|
||||||
@@ -979,6 +1055,10 @@ where
|
|||||||
let timeline_id = TimelineId::from_str(params[1])
|
let timeline_id = TimelineId::from_str(params[1])
|
||||||
.with_context(|| format!("Failed to parse timeline id from {}", params[1]))?;
|
.with_context(|| format!("Failed to parse timeline id from {}", params[1]))?;
|
||||||
|
|
||||||
|
tracing::Span::current()
|
||||||
|
.record("tenant_id", field::display(tenant_id))
|
||||||
|
.record("timeline_id", field::display(timeline_id));
|
||||||
|
|
||||||
// The caller is responsible for providing correct lsn and prev_lsn.
|
// The caller is responsible for providing correct lsn and prev_lsn.
|
||||||
let lsn = if params.len() > 2 {
|
let lsn = if params.len() > 2 {
|
||||||
Some(
|
Some(
|
||||||
@@ -1000,8 +1080,17 @@ where
|
|||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
// Check that the timeline exists
|
// Check that the timeline exists
|
||||||
self.handle_basebackup_request(pgb, tenant_id, timeline_id, lsn, prev_lsn, true, ctx)
|
self.handle_basebackup_request(
|
||||||
.await?;
|
pgb,
|
||||||
|
tenant_id,
|
||||||
|
timeline_id,
|
||||||
|
lsn,
|
||||||
|
prev_lsn,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
pgb.write_message_noflush(&BeMessage::CommandComplete(b"SELECT 1"))?;
|
pgb.write_message_noflush(&BeMessage::CommandComplete(b"SELECT 1"))?;
|
||||||
} else if query_string.starts_with("import basebackup ") {
|
} else if query_string.starts_with("import basebackup ") {
|
||||||
// Import the `base` section (everything but the wal) of a basebackup.
|
// Import the `base` section (everything but the wal) of a basebackup.
|
||||||
@@ -1033,6 +1122,10 @@ where
|
|||||||
let pg_version = u32::from_str(params[4])
|
let pg_version = u32::from_str(params[4])
|
||||||
.with_context(|| format!("Failed to parse pg_version from {}", params[4]))?;
|
.with_context(|| format!("Failed to parse pg_version from {}", params[4]))?;
|
||||||
|
|
||||||
|
tracing::Span::current()
|
||||||
|
.record("tenant_id", field::display(tenant_id))
|
||||||
|
.record("timeline_id", field::display(timeline_id));
|
||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
match self
|
match self
|
||||||
@@ -1077,6 +1170,10 @@ where
|
|||||||
let end_lsn = Lsn::from_str(params[3])
|
let end_lsn = Lsn::from_str(params[3])
|
||||||
.with_context(|| format!("Failed to parse Lsn from {}", params[3]))?;
|
.with_context(|| format!("Failed to parse Lsn from {}", params[3]))?;
|
||||||
|
|
||||||
|
tracing::Span::current()
|
||||||
|
.record("tenant_id", field::display(tenant_id))
|
||||||
|
.record("timeline_id", field::display(timeline_id));
|
||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
match self
|
match self
|
||||||
@@ -1108,6 +1205,8 @@ where
|
|||||||
let tenant_id = TenantId::from_str(params[0])
|
let tenant_id = TenantId::from_str(params[0])
|
||||||
.with_context(|| format!("Failed to parse tenant id from {}", params[0]))?;
|
.with_context(|| format!("Failed to parse tenant id from {}", params[0]))?;
|
||||||
|
|
||||||
|
tracing::Span::current().record("tenant_id", field::display(tenant_id));
|
||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
let tenant = get_active_tenant_with_timeout(tenant_id, &ctx).await?;
|
let tenant = get_active_tenant_with_timeout(tenant_id, &ctx).await?;
|
||||||
|
|||||||
@@ -887,7 +887,7 @@ impl<'a> DatadirModification<'a> {
|
|||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<(), RelationError> {
|
) -> Result<(), RelationError> {
|
||||||
if rel.relnode == 0 {
|
if rel.relnode == 0 {
|
||||||
return Err(RelationError::AlreadyExists);
|
return Err(RelationError::InvalidRelnode);
|
||||||
}
|
}
|
||||||
// It's possible that this is the first rel for this db in this
|
// It's possible that this is the first rel for this db in this
|
||||||
// tablespace. Create the reldir entry for it if so.
|
// tablespace. Create the reldir entry for it if so.
|
||||||
@@ -1131,7 +1131,7 @@ impl<'a> DatadirModification<'a> {
|
|||||||
/// context, breaking the atomicity is OK. If the import is interrupted, the
|
/// context, breaking the atomicity is OK. If the import is interrupted, the
|
||||||
/// whole import fails and the timeline will be deleted anyway.
|
/// whole import fails and the timeline will be deleted anyway.
|
||||||
/// (Or to be precise, it will be left behind for debugging purposes and
|
/// (Or to be precise, it will be left behind for debugging purposes and
|
||||||
/// ignored, see https://github.com/neondatabase/neon/pull/1809)
|
/// ignored, see <https://github.com/neondatabase/neon/pull/1809>)
|
||||||
///
|
///
|
||||||
/// Note: A consequence of flushing the pending operations is that they
|
/// Note: A consequence of flushing the pending operations is that they
|
||||||
/// won't be visible to subsequent operations until `commit`. The function
|
/// won't be visible to subsequent operations until `commit`. The function
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ pub enum TaskKind {
|
|||||||
///
|
///
|
||||||
/// Walreceiver uses its own abstraction called `TaskHandle` to represent the activity of establishing and handling a connection.
|
/// Walreceiver uses its own abstraction called `TaskHandle` to represent the activity of establishing and handling a connection.
|
||||||
/// That abstraction doesn't use `task_mgr`.
|
/// That abstraction doesn't use `task_mgr`.
|
||||||
/// The [`WalReceiverManager`] task ensures that this `TaskHandle` task does not outlive the [`WalReceiverManager`] task.
|
/// The `WalReceiverManager` task ensures that this `TaskHandle` task does not outlive the `WalReceiverManager` task.
|
||||||
/// For the `RequestContext` that we hand to the TaskHandle, we use the [`WalReceiverConnectionHandler`] task kind.
|
/// For the `RequestContext` that we hand to the TaskHandle, we use the [`WalReceiverConnectionHandler`] task kind.
|
||||||
///
|
///
|
||||||
/// Once the connection is established, the `TaskHandle` task creates a
|
/// Once the connection is established, the `TaskHandle` task creates a
|
||||||
@@ -213,16 +213,21 @@ pub enum TaskKind {
|
|||||||
/// the `Connection` object.
|
/// the `Connection` object.
|
||||||
/// A `CancellationToken` created by the `TaskHandle` task ensures
|
/// A `CancellationToken` created by the `TaskHandle` task ensures
|
||||||
/// that the [`WalReceiverConnectionPoller`] task will cancel soon after as the `TaskHandle` is dropped.
|
/// that the [`WalReceiverConnectionPoller`] task will cancel soon after as the `TaskHandle` is dropped.
|
||||||
|
///
|
||||||
|
/// [`WalReceiverConnectionHandler`]: Self::WalReceiverConnectionHandler
|
||||||
|
/// [`WalReceiverConnectionPoller`]: Self::WalReceiverConnectionPoller
|
||||||
WalReceiverManager,
|
WalReceiverManager,
|
||||||
|
|
||||||
/// The `TaskHandle` task that executes [`walreceiver_connection::handle_walreceiver_connection`].
|
/// The `TaskHandle` task that executes `handle_walreceiver_connection`.
|
||||||
/// Not a `task_mgr` task, but we use this `TaskKind` for its `RequestContext`.
|
/// Not a `task_mgr` task, but we use this `TaskKind` for its `RequestContext`.
|
||||||
/// See the comment on [`WalReceiverManager`].
|
/// See the comment on [`WalReceiverManager`].
|
||||||
|
///
|
||||||
|
/// [`WalReceiverManager`]: Self::WalReceiverManager
|
||||||
WalReceiverConnectionHandler,
|
WalReceiverConnectionHandler,
|
||||||
|
|
||||||
/// The task that polls the `tokio-postgres::Connection` object.
|
/// The task that polls the `tokio-postgres::Connection` object.
|
||||||
/// Spawned by task [`WalReceiverConnectionHandler`].
|
/// Spawned by task [`WalReceiverConnectionHandler`](Self::WalReceiverConnectionHandler).
|
||||||
/// See the comment on [`WalReceiverManager`].
|
/// See the comment on [`WalReceiverManager`](Self::WalReceiverManager).
|
||||||
WalReceiverConnectionPoller,
|
WalReceiverConnectionPoller,
|
||||||
|
|
||||||
// Garbage collection worker. One per tenant
|
// Garbage collection worker. One per tenant
|
||||||
@@ -506,17 +511,13 @@ pub async fn shutdown_tasks(
|
|||||||
warn!(name = task.name, tenant_id = ?tenant_id, timeline_id = ?timeline_id, kind = ?task_kind, "stopping left-over");
|
warn!(name = task.name, tenant_id = ?tenant_id, timeline_id = ?timeline_id, kind = ?task_kind, "stopping left-over");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let completed = tokio::select! {
|
if tokio::time::timeout(std::time::Duration::from_secs(1), &mut join_handle)
|
||||||
biased;
|
.await
|
||||||
_ = &mut join_handle => { true },
|
.is_err()
|
||||||
_ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {
|
{
|
||||||
// allow some time to elapse before logging to cut down the number of log
|
// allow some time to elapse before logging to cut down the number of log
|
||||||
// lines.
|
// lines.
|
||||||
info!("waiting for {} to shut down", task.name);
|
info!("waiting for {} to shut down", task.name);
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if !completed {
|
|
||||||
// we never handled this return value, but:
|
// we never handled this return value, but:
|
||||||
// - we don't deschedule which would lead to is_cancelled
|
// - we don't deschedule which would lead to is_cancelled
|
||||||
// - panics are already logged (is_panicked)
|
// - panics are already logged (is_panicked)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
//! parent timeline, and the last LSN that has been written to disk.
|
//! parent timeline, and the last LSN that has been written to disk.
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use anyhow::{bail, ensure, Context};
|
use anyhow::{bail, Context};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use pageserver_api::models::TimelineState;
|
use pageserver_api::models::TimelineState;
|
||||||
use remote_storage::DownloadError;
|
use remote_storage::DownloadError;
|
||||||
@@ -49,6 +49,8 @@ use std::time::{Duration, Instant};
|
|||||||
use self::config::TenantConf;
|
use self::config::TenantConf;
|
||||||
use self::metadata::TimelineMetadata;
|
use self::metadata::TimelineMetadata;
|
||||||
use self::remote_timeline_client::RemoteTimelineClient;
|
use self::remote_timeline_client::RemoteTimelineClient;
|
||||||
|
use self::timeline::uninit::TimelineUninitMark;
|
||||||
|
use self::timeline::uninit::UninitializedTimeline;
|
||||||
use self::timeline::EvictionTaskTenantState;
|
use self::timeline::EvictionTaskTenantState;
|
||||||
use crate::config::PageServerConf;
|
use crate::config::PageServerConf;
|
||||||
use crate::context::{DownloadBehavior, RequestContext};
|
use crate::context::{DownloadBehavior, RequestContext};
|
||||||
@@ -68,6 +70,7 @@ use crate::tenant::storage_layer::ImageLayer;
|
|||||||
use crate::tenant::storage_layer::Layer;
|
use crate::tenant::storage_layer::Layer;
|
||||||
use crate::InitializationOrder;
|
use crate::InitializationOrder;
|
||||||
|
|
||||||
|
use crate::tenant::timeline::uninit::cleanup_timeline_directory;
|
||||||
use crate::virtual_file::VirtualFile;
|
use crate::virtual_file::VirtualFile;
|
||||||
use crate::walredo::PostgresRedoManager;
|
use crate::walredo::PostgresRedoManager;
|
||||||
use crate::walredo::WalRedoManager;
|
use crate::walredo::WalRedoManager;
|
||||||
@@ -81,12 +84,32 @@ use utils::{
|
|||||||
lsn::{Lsn, RecordLsn},
|
lsn::{Lsn, RecordLsn},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Declare a failpoint that can use the `pause` failpoint action.
|
||||||
|
/// We don't want to block the executor thread, hence, spawn_blocking + await.
|
||||||
|
macro_rules! pausable_failpoint {
|
||||||
|
($name:literal) => {
|
||||||
|
if cfg!(feature = "testing") {
|
||||||
|
tokio::task::spawn_blocking({
|
||||||
|
let current = tracing::Span::current();
|
||||||
|
move || {
|
||||||
|
let _entered = current.entered();
|
||||||
|
tracing::info!("at failpoint {}", $name);
|
||||||
|
fail::fail_point!($name);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("spawn_blocking");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub mod blob_io;
|
pub mod blob_io;
|
||||||
pub mod block_io;
|
pub mod block_io;
|
||||||
pub mod disk_btree;
|
pub mod disk_btree;
|
||||||
pub(crate) mod ephemeral_file;
|
pub(crate) mod ephemeral_file;
|
||||||
pub mod layer_map;
|
pub mod layer_map;
|
||||||
pub mod manifest;
|
pub mod manifest;
|
||||||
|
mod span;
|
||||||
|
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
mod par_fsync;
|
mod par_fsync;
|
||||||
@@ -98,11 +121,11 @@ pub mod mgr;
|
|||||||
pub mod tasks;
|
pub mod tasks;
|
||||||
pub mod upload_queue;
|
pub mod upload_queue;
|
||||||
|
|
||||||
mod timeline;
|
pub(crate) mod timeline;
|
||||||
|
|
||||||
pub mod size;
|
pub mod size;
|
||||||
|
|
||||||
pub(crate) use timeline::debug_assert_current_span_has_tenant_and_timeline_id;
|
pub(crate) use timeline::span::debug_assert_current_span_has_tenant_and_timeline_id;
|
||||||
pub use timeline::{
|
pub use timeline::{
|
||||||
LocalLayerInfoForDiskUsageEviction, LogicalSizeCalculationCause, PageReconstructError, Timeline,
|
LocalLayerInfoForDiskUsageEviction, LogicalSizeCalculationCause, PageReconstructError, Timeline,
|
||||||
};
|
};
|
||||||
@@ -110,7 +133,7 @@ pub use timeline::{
|
|||||||
// re-export this function so that page_cache.rs can use it.
|
// re-export this function so that page_cache.rs can use it.
|
||||||
pub use crate::tenant::ephemeral_file::writeback as writeback_ephemeral_file;
|
pub use crate::tenant::ephemeral_file::writeback as writeback_ephemeral_file;
|
||||||
|
|
||||||
// re-export for use in storage_sync.rs
|
// re-export for use in remote_timeline_client.rs
|
||||||
pub use crate::tenant::metadata::save_metadata;
|
pub use crate::tenant::metadata::save_metadata;
|
||||||
|
|
||||||
// re-export for use in walreceiver
|
// re-export for use in walreceiver
|
||||||
@@ -161,200 +184,6 @@ pub struct Tenant {
|
|||||||
eviction_task_tenant_state: tokio::sync::Mutex<EvictionTaskTenantState>,
|
eviction_task_tenant_state: tokio::sync::Mutex<EvictionTaskTenantState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A timeline with some of its files on disk, being initialized.
|
|
||||||
/// This struct ensures the atomicity of the timeline init: it's either properly created and inserted into pageserver's memory, or
|
|
||||||
/// its local files are removed. In the worst case of a crash, an uninit mark file is left behind, which causes the directory
|
|
||||||
/// to be removed on next restart.
|
|
||||||
///
|
|
||||||
/// The caller is responsible for proper timeline data filling before the final init.
|
|
||||||
#[must_use]
|
|
||||||
pub struct UninitializedTimeline<'t> {
|
|
||||||
owning_tenant: &'t Tenant,
|
|
||||||
timeline_id: TimelineId,
|
|
||||||
raw_timeline: Option<(Arc<Timeline>, TimelineUninitMark)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An uninit mark file, created along the timeline dir to ensure the timeline either gets fully initialized and loaded into pageserver's memory,
|
|
||||||
/// or gets removed eventually.
|
|
||||||
///
|
|
||||||
/// XXX: it's important to create it near the timeline dir, not inside it to ensure timeline dir gets removed first.
|
|
||||||
#[must_use]
|
|
||||||
struct TimelineUninitMark {
|
|
||||||
uninit_mark_deleted: bool,
|
|
||||||
uninit_mark_path: PathBuf,
|
|
||||||
timeline_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UninitializedTimeline<'_> {
|
|
||||||
/// Finish timeline creation: insert it into the Tenant's timelines map and remove the
|
|
||||||
/// uninit mark file.
|
|
||||||
///
|
|
||||||
/// This function launches the flush loop if not already done.
|
|
||||||
///
|
|
||||||
/// The caller is responsible for activating the timeline (function `.activate()`).
|
|
||||||
fn finish_creation(mut self) -> anyhow::Result<Arc<Timeline>> {
|
|
||||||
let timeline_id = self.timeline_id;
|
|
||||||
let tenant_id = self.owning_tenant.tenant_id;
|
|
||||||
|
|
||||||
let (new_timeline, uninit_mark) = self.raw_timeline.take().with_context(|| {
|
|
||||||
format!("No timeline for initalization found for {tenant_id}/{timeline_id}")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Check that the caller initialized disk_consistent_lsn
|
|
||||||
let new_disk_consistent_lsn = new_timeline.get_disk_consistent_lsn();
|
|
||||||
ensure!(
|
|
||||||
new_disk_consistent_lsn.is_valid(),
|
|
||||||
"new timeline {tenant_id}/{timeline_id} has invalid disk_consistent_lsn"
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut timelines = self.owning_tenant.timelines.lock().unwrap();
|
|
||||||
match timelines.entry(timeline_id) {
|
|
||||||
Entry::Occupied(_) => anyhow::bail!(
|
|
||||||
"Found freshly initialized timeline {tenant_id}/{timeline_id} in the tenant map"
|
|
||||||
),
|
|
||||||
Entry::Vacant(v) => {
|
|
||||||
uninit_mark.remove_uninit_mark().with_context(|| {
|
|
||||||
format!(
|
|
||||||
"Failed to remove uninit mark file for timeline {tenant_id}/{timeline_id}"
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
v.insert(Arc::clone(&new_timeline));
|
|
||||||
|
|
||||||
new_timeline.maybe_spawn_flush_loop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(new_timeline)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prepares timeline data by loading it from the basebackup archive.
|
|
||||||
pub async fn import_basebackup_from_tar(
|
|
||||||
self,
|
|
||||||
copyin_read: &mut (impl tokio::io::AsyncRead + Send + Sync + Unpin),
|
|
||||||
base_lsn: Lsn,
|
|
||||||
broker_client: storage_broker::BrokerClientChannel,
|
|
||||||
ctx: &RequestContext,
|
|
||||||
) -> anyhow::Result<Arc<Timeline>> {
|
|
||||||
let raw_timeline = self.raw_timeline()?;
|
|
||||||
|
|
||||||
import_datadir::import_basebackup_from_tar(raw_timeline, copyin_read, base_lsn, ctx)
|
|
||||||
.await
|
|
||||||
.context("Failed to import basebackup")?;
|
|
||||||
|
|
||||||
// Flush the new layer files to disk, before we make the timeline as available to
|
|
||||||
// the outside world.
|
|
||||||
//
|
|
||||||
// Flush loop needs to be spawned in order to be able to flush.
|
|
||||||
raw_timeline.maybe_spawn_flush_loop();
|
|
||||||
|
|
||||||
fail::fail_point!("before-checkpoint-new-timeline", |_| {
|
|
||||||
bail!("failpoint before-checkpoint-new-timeline");
|
|
||||||
});
|
|
||||||
|
|
||||||
raw_timeline
|
|
||||||
.freeze_and_flush()
|
|
||||||
.await
|
|
||||||
.context("Failed to flush after basebackup import")?;
|
|
||||||
|
|
||||||
// All the data has been imported. Insert the Timeline into the tenant's timelines
|
|
||||||
// map and remove the uninit mark file.
|
|
||||||
let tl = self.finish_creation()?;
|
|
||||||
tl.activate(broker_client, None, ctx);
|
|
||||||
Ok(tl)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn raw_timeline(&self) -> anyhow::Result<&Arc<Timeline>> {
|
|
||||||
Ok(&self
|
|
||||||
.raw_timeline
|
|
||||||
.as_ref()
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"No raw timeline {}/{} found",
|
|
||||||
self.owning_tenant.tenant_id, self.timeline_id
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for UninitializedTimeline<'_> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if let Some((_, uninit_mark)) = self.raw_timeline.take() {
|
|
||||||
let _entered = info_span!("drop_uninitialized_timeline", tenant = %self.owning_tenant.tenant_id, timeline = %self.timeline_id).entered();
|
|
||||||
error!("Timeline got dropped without initializing, cleaning its files");
|
|
||||||
cleanup_timeline_directory(uninit_mark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cleanup_timeline_directory(uninit_mark: TimelineUninitMark) {
|
|
||||||
let timeline_path = &uninit_mark.timeline_path;
|
|
||||||
match ignore_absent_files(|| fs::remove_dir_all(timeline_path)) {
|
|
||||||
Ok(()) => {
|
|
||||||
info!("Timeline dir {timeline_path:?} removed successfully, removing the uninit mark")
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to clean up uninitialized timeline directory {timeline_path:?}: {e:?}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
drop(uninit_mark); // mark handles its deletion on drop, gets retained if timeline dir exists
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TimelineUninitMark {
|
|
||||||
fn new(uninit_mark_path: PathBuf, timeline_path: PathBuf) -> Self {
|
|
||||||
Self {
|
|
||||||
uninit_mark_deleted: false,
|
|
||||||
uninit_mark_path,
|
|
||||||
timeline_path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_uninit_mark(mut self) -> anyhow::Result<()> {
|
|
||||||
if !self.uninit_mark_deleted {
|
|
||||||
self.delete_mark_file_if_present()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_mark_file_if_present(&mut self) -> anyhow::Result<()> {
|
|
||||||
let uninit_mark_file = &self.uninit_mark_path;
|
|
||||||
let uninit_mark_parent = uninit_mark_file
|
|
||||||
.parent()
|
|
||||||
.with_context(|| format!("Uninit mark file {uninit_mark_file:?} has no parent"))?;
|
|
||||||
ignore_absent_files(|| fs::remove_file(uninit_mark_file)).with_context(|| {
|
|
||||||
format!("Failed to remove uninit mark file at path {uninit_mark_file:?}")
|
|
||||||
})?;
|
|
||||||
crashsafe::fsync(uninit_mark_parent).context("Failed to fsync uninit mark parent")?;
|
|
||||||
self.uninit_mark_deleted = true;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for TimelineUninitMark {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if !self.uninit_mark_deleted {
|
|
||||||
if self.timeline_path.exists() {
|
|
||||||
error!(
|
|
||||||
"Uninit mark {} is not removed, timeline {} stays uninitialized",
|
|
||||||
self.uninit_mark_path.display(),
|
|
||||||
self.timeline_path.display()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// unblock later timeline creation attempts
|
|
||||||
warn!(
|
|
||||||
"Removing intermediate uninit mark file {}",
|
|
||||||
self.uninit_mark_path.display()
|
|
||||||
);
|
|
||||||
if let Err(e) = self.delete_mark_file_if_present() {
|
|
||||||
error!("Failed to remove the uninit mark file: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should not blindly overwrite local metadata with remote one.
|
// We should not blindly overwrite local metadata with remote one.
|
||||||
// For example, consider the following case:
|
// For example, consider the following case:
|
||||||
// Image layer is flushed to disk as a new delta layer, we update local metadata and start upload task but after that
|
// Image layer is flushed to disk as a new delta layer, we update local metadata and start upload task but after that
|
||||||
@@ -452,7 +281,7 @@ pub enum DeleteTimelineError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub enum SetStoppingError {
|
pub enum SetStoppingError {
|
||||||
AlreadyStopping,
|
AlreadyStopping(completion::Barrier),
|
||||||
Broken,
|
Broken,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,10 +318,6 @@ impl std::fmt::Display for WaitToBecomeActiveError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum ShutdownError {
|
|
||||||
AlreadyStopping,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DeletionGuard(OwnedMutexGuard<bool>);
|
struct DeletionGuard(OwnedMutexGuard<bool>);
|
||||||
|
|
||||||
impl DeletionGuard {
|
impl DeletionGuard {
|
||||||
@@ -501,6 +326,16 @@ impl DeletionGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum CreateTimelineError {
|
||||||
|
#[error("a timeline with the given ID already exists")]
|
||||||
|
AlreadyExists,
|
||||||
|
#[error(transparent)]
|
||||||
|
AncestorLsn(anyhow::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Other(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
impl Tenant {
|
impl Tenant {
|
||||||
/// Yet another helper for timeline initialization.
|
/// Yet another helper for timeline initialization.
|
||||||
/// Contains the common part of `load_local_timeline` and `load_remote_timeline`.
|
/// Contains the common part of `load_local_timeline` and `load_remote_timeline`.
|
||||||
@@ -590,6 +425,7 @@ impl Tenant {
|
|||||||
.layers
|
.layers
|
||||||
.read()
|
.read()
|
||||||
.await
|
.await
|
||||||
|
.layer_map()
|
||||||
.iter_historic_layers()
|
.iter_historic_layers()
|
||||||
.next()
|
.next()
|
||||||
.is_some(),
|
.is_some(),
|
||||||
@@ -600,8 +436,8 @@ impl Tenant {
|
|||||||
if !picked_local {
|
if !picked_local {
|
||||||
save_metadata(
|
save_metadata(
|
||||||
self.conf,
|
self.conf,
|
||||||
timeline_id,
|
&tenant_id,
|
||||||
tenant_id,
|
&timeline_id,
|
||||||
up_to_date_metadata,
|
up_to_date_metadata,
|
||||||
first_save,
|
first_save,
|
||||||
)
|
)
|
||||||
@@ -630,7 +466,7 @@ impl Tenant {
|
|||||||
) -> anyhow::Result<Arc<Tenant>> {
|
) -> anyhow::Result<Arc<Tenant>> {
|
||||||
// TODO dedup with spawn_load
|
// TODO dedup with spawn_load
|
||||||
let tenant_conf =
|
let tenant_conf =
|
||||||
Self::load_tenant_config(conf, tenant_id).context("load tenant config")?;
|
Self::load_tenant_config(conf, &tenant_id).context("load tenant config")?;
|
||||||
|
|
||||||
let wal_redo_manager = Arc::new(PostgresRedoManager::new(conf, tenant_id));
|
let wal_redo_manager = Arc::new(PostgresRedoManager::new(conf, tenant_id));
|
||||||
let tenant = Arc::new(Tenant::new(
|
let tenant = Arc::new(Tenant::new(
|
||||||
@@ -684,7 +520,7 @@ impl Tenant {
|
|||||||
/// No background tasks are started as part of this routine.
|
/// No background tasks are started as part of this routine.
|
||||||
///
|
///
|
||||||
async fn attach(self: &Arc<Tenant>, ctx: &RequestContext) -> anyhow::Result<()> {
|
async fn attach(self: &Arc<Tenant>, ctx: &RequestContext) -> anyhow::Result<()> {
|
||||||
debug_assert_current_span_has_tenant_id();
|
span::debug_assert_current_span_has_tenant_id();
|
||||||
|
|
||||||
let marker_file = self.conf.tenant_attaching_mark_file_path(&self.tenant_id);
|
let marker_file = self.conf.tenant_attaching_mark_file_path(&self.tenant_id);
|
||||||
if !tokio::fs::try_exists(&marker_file)
|
if !tokio::fs::try_exists(&marker_file)
|
||||||
@@ -739,7 +575,7 @@ impl Tenant {
|
|||||||
.map(move |res| {
|
.map(move |res| {
|
||||||
res.with_context(|| format!("download index part for timeline {timeline_id}"))
|
res.with_context(|| format!("download index part for timeline {timeline_id}"))
|
||||||
})
|
})
|
||||||
.instrument(info_span!("download_index_part", timeline=%timeline_id)),
|
.instrument(info_span!("download_index_part", %timeline_id)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Wait for all the download tasks to complete & collect results.
|
// Wait for all the download tasks to complete & collect results.
|
||||||
@@ -822,10 +658,10 @@ impl Tenant {
|
|||||||
remote_client: RemoteTimelineClient,
|
remote_client: RemoteTimelineClient,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
debug_assert_current_span_has_tenant_id();
|
span::debug_assert_current_span_has_tenant_id();
|
||||||
|
|
||||||
info!("downloading index file for timeline {}", timeline_id);
|
info!("downloading index file for timeline {}", timeline_id);
|
||||||
tokio::fs::create_dir_all(self.conf.timeline_path(&timeline_id, &self.tenant_id))
|
tokio::fs::create_dir_all(self.conf.timeline_path(&self.tenant_id, &timeline_id))
|
||||||
.await
|
.await
|
||||||
.context("Failed to create new timeline directory")?;
|
.context("Failed to create new timeline directory")?;
|
||||||
|
|
||||||
@@ -901,9 +737,9 @@ impl Tenant {
|
|||||||
init_order: Option<InitializationOrder>,
|
init_order: Option<InitializationOrder>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Arc<Tenant> {
|
) -> Arc<Tenant> {
|
||||||
debug_assert_current_span_has_tenant_id();
|
span::debug_assert_current_span_has_tenant_id();
|
||||||
|
|
||||||
let tenant_conf = match Self::load_tenant_config(conf, tenant_id) {
|
let tenant_conf = match Self::load_tenant_config(conf, &tenant_id) {
|
||||||
Ok(conf) => conf,
|
Ok(conf) => conf,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("load tenant config failed: {:?}", e);
|
error!("load tenant config failed: {:?}", e);
|
||||||
@@ -1014,7 +850,7 @@ impl Tenant {
|
|||||||
timeline_uninit_mark_file.display()
|
timeline_uninit_mark_file.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let timeline_dir = self.conf.timeline_path(&timeline_id, &self.tenant_id);
|
let timeline_dir = self.conf.timeline_path(&self.tenant_id, &timeline_id);
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
remove_timeline_and_uninit_mark(&timeline_dir, timeline_uninit_mark_file)
|
remove_timeline_and_uninit_mark(&timeline_dir, timeline_uninit_mark_file)
|
||||||
{
|
{
|
||||||
@@ -1059,7 +895,7 @@ impl Tenant {
|
|||||||
if let Ok(timeline_id) =
|
if let Ok(timeline_id) =
|
||||||
file_name.to_str().unwrap_or_default().parse::<TimelineId>()
|
file_name.to_str().unwrap_or_default().parse::<TimelineId>()
|
||||||
{
|
{
|
||||||
let metadata = load_metadata(self.conf, timeline_id, self.tenant_id)
|
let metadata = load_metadata(self.conf, &self.tenant_id, &timeline_id)
|
||||||
.context("failed to load metadata")?;
|
.context("failed to load metadata")?;
|
||||||
timelines_to_load.insert(timeline_id, metadata);
|
timelines_to_load.insert(timeline_id, metadata);
|
||||||
} else {
|
} else {
|
||||||
@@ -1087,7 +923,7 @@ impl Tenant {
|
|||||||
init_order: Option<&InitializationOrder>,
|
init_order: Option<&InitializationOrder>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
debug_assert_current_span_has_tenant_id();
|
span::debug_assert_current_span_has_tenant_id();
|
||||||
|
|
||||||
debug!("loading tenant task");
|
debug!("loading tenant task");
|
||||||
|
|
||||||
@@ -1133,7 +969,7 @@ impl Tenant {
|
|||||||
init_order: Option<&InitializationOrder>,
|
init_order: Option<&InitializationOrder>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
debug_assert_current_span_has_tenant_id();
|
span::debug_assert_current_span_has_tenant_id();
|
||||||
|
|
||||||
let remote_client = self.remote_storage.as_ref().map(|remote_storage| {
|
let remote_client = self.remote_storage.as_ref().map(|remote_storage| {
|
||||||
RemoteTimelineClient::new(
|
RemoteTimelineClient::new(
|
||||||
@@ -1332,7 +1168,7 @@ impl Tenant {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper for unit tests to create an emtpy timeline.
|
/// Helper for unit tests to create an empty timeline.
|
||||||
///
|
///
|
||||||
/// The timeline is has state value `Active` but its background loops are not running.
|
/// The timeline is has state value `Active` but its background loops are not running.
|
||||||
// This makes the various functions which anyhow::ensure! for Active state work in tests.
|
// This makes the various functions which anyhow::ensure! for Active state work in tests.
|
||||||
@@ -1374,8 +1210,7 @@ impl Tenant {
|
|||||||
/// Returns the new timeline ID and reference to its Timeline object.
|
/// Returns the new timeline ID and reference to its Timeline object.
|
||||||
///
|
///
|
||||||
/// If the caller specified the timeline ID to use (`new_timeline_id`), and timeline with
|
/// If the caller specified the timeline ID to use (`new_timeline_id`), and timeline with
|
||||||
/// the same timeline ID already exists, returns None. If `new_timeline_id` is not given,
|
/// the same timeline ID already exists, returns CreateTimelineError::AlreadyExists.
|
||||||
/// a new unique ID is generated.
|
|
||||||
pub async fn create_timeline(
|
pub async fn create_timeline(
|
||||||
&self,
|
&self,
|
||||||
new_timeline_id: TimelineId,
|
new_timeline_id: TimelineId,
|
||||||
@@ -1384,11 +1219,12 @@ impl Tenant {
|
|||||||
pg_version: u32,
|
pg_version: u32,
|
||||||
broker_client: storage_broker::BrokerClientChannel,
|
broker_client: storage_broker::BrokerClientChannel,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<Option<Arc<Timeline>>> {
|
) -> Result<Arc<Timeline>, CreateTimelineError> {
|
||||||
anyhow::ensure!(
|
if !self.is_active() {
|
||||||
self.is_active(),
|
return Err(CreateTimelineError::Other(anyhow::anyhow!(
|
||||||
"Cannot create timelines on inactive tenant"
|
"Cannot create timelines on inactive tenant"
|
||||||
);
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(existing) = self.get_timeline(new_timeline_id, false) {
|
if let Ok(existing) = self.get_timeline(new_timeline_id, false) {
|
||||||
debug!("timeline {new_timeline_id} already exists");
|
debug!("timeline {new_timeline_id} already exists");
|
||||||
@@ -1408,7 +1244,7 @@ impl Tenant {
|
|||||||
.context("wait for timeline uploads to complete")?;
|
.context("wait for timeline uploads to complete")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(None);
|
return Err(CreateTimelineError::AlreadyExists);
|
||||||
}
|
}
|
||||||
|
|
||||||
let loaded_timeline = match ancestor_timeline_id {
|
let loaded_timeline = match ancestor_timeline_id {
|
||||||
@@ -1423,12 +1259,12 @@ impl Tenant {
|
|||||||
let ancestor_ancestor_lsn = ancestor_timeline.get_ancestor_lsn();
|
let ancestor_ancestor_lsn = ancestor_timeline.get_ancestor_lsn();
|
||||||
if ancestor_ancestor_lsn > *lsn {
|
if ancestor_ancestor_lsn > *lsn {
|
||||||
// can we safely just branch from the ancestor instead?
|
// can we safely just branch from the ancestor instead?
|
||||||
bail!(
|
return Err(CreateTimelineError::AncestorLsn(anyhow::anyhow!(
|
||||||
"invalid start lsn {} for ancestor timeline {}: less than timeline ancestor lsn {}",
|
"invalid start lsn {} for ancestor timeline {}: less than timeline ancestor lsn {}",
|
||||||
lsn,
|
lsn,
|
||||||
ancestor_timeline_id,
|
ancestor_timeline_id,
|
||||||
ancestor_ancestor_lsn,
|
ancestor_ancestor_lsn,
|
||||||
);
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the WAL to arrive and be processed on the parent branch up
|
// Wait for the WAL to arrive and be processed on the parent branch up
|
||||||
@@ -1462,7 +1298,7 @@ impl Tenant {
|
|||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(loaded_timeline))
|
Ok(loaded_timeline)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// perform one garbage collection iteration, removing old data files from disk.
|
/// perform one garbage collection iteration, removing old data files from disk.
|
||||||
@@ -1528,7 +1364,7 @@ impl Tenant {
|
|||||||
for (timeline_id, timeline) in &timelines_to_compact {
|
for (timeline_id, timeline) in &timelines_to_compact {
|
||||||
timeline
|
timeline
|
||||||
.compact(ctx)
|
.compact(ctx)
|
||||||
.instrument(info_span!("compact_timeline", timeline = %timeline_id))
|
.instrument(info_span!("compact_timeline", %timeline_id))
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1619,12 +1455,12 @@ impl Tenant {
|
|||||||
let layer_removal_guard = timeline.layer_removal_cs.lock().await;
|
let layer_removal_guard = timeline.layer_removal_cs.lock().await;
|
||||||
info!("got layer_removal_cs.lock(), deleting layer files");
|
info!("got layer_removal_cs.lock(), deleting layer files");
|
||||||
|
|
||||||
// NB: storage_sync upload tasks that reference these layers have been cancelled
|
// NB: remote_timeline_client upload tasks that reference these layers have been cancelled
|
||||||
// by the caller.
|
// by the caller.
|
||||||
|
|
||||||
let local_timeline_directory = self
|
let local_timeline_directory = self
|
||||||
.conf
|
.conf
|
||||||
.timeline_path(&timeline.timeline_id, &self.tenant_id);
|
.timeline_path(&self.tenant_id, &timeline.timeline_id);
|
||||||
|
|
||||||
fail::fail_point!("timeline-delete-before-rm", |_| {
|
fail::fail_point!("timeline-delete-before-rm", |_| {
|
||||||
Err(anyhow::anyhow!("failpoint: timeline-delete-before-rm"))?
|
Err(anyhow::anyhow!("failpoint: timeline-delete-before-rm"))?
|
||||||
@@ -1677,20 +1513,7 @@ impl Tenant {
|
|||||||
remote_client.delete_all().await.context("delete_all")?
|
remote_client.delete_all().await.context("delete_all")?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Have a failpoint that can use the `pause` failpoint action.
|
pausable_failpoint!("in_progress_delete");
|
||||||
// We don't want to block the executor thread, hence, spawn_blocking + await.
|
|
||||||
if cfg!(feature = "testing") {
|
|
||||||
tokio::task::spawn_blocking({
|
|
||||||
let current = tracing::Span::current();
|
|
||||||
move || {
|
|
||||||
let _entered = current.entered();
|
|
||||||
tracing::info!("at failpoint in_progress_delete");
|
|
||||||
fail::fail_point!("in_progress_delete");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("spawn_blocking");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
// Remove the timeline from the map.
|
// Remove the timeline from the map.
|
||||||
@@ -1724,7 +1547,7 @@ impl Tenant {
|
|||||||
timeline_id: TimelineId,
|
timeline_id: TimelineId,
|
||||||
_ctx: &RequestContext,
|
_ctx: &RequestContext,
|
||||||
) -> Result<(), DeleteTimelineError> {
|
) -> Result<(), DeleteTimelineError> {
|
||||||
timeline::debug_assert_current_span_has_tenant_and_timeline_id();
|
debug_assert_current_span_has_tenant_and_timeline_id();
|
||||||
|
|
||||||
// Transition the timeline into TimelineState::Stopping.
|
// Transition the timeline into TimelineState::Stopping.
|
||||||
// This should prevent new operations from starting.
|
// This should prevent new operations from starting.
|
||||||
@@ -1888,13 +1711,13 @@ impl Tenant {
|
|||||||
background_jobs_can_start: Option<&completion::Barrier>,
|
background_jobs_can_start: Option<&completion::Barrier>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) {
|
) {
|
||||||
debug_assert_current_span_has_tenant_id();
|
span::debug_assert_current_span_has_tenant_id();
|
||||||
|
|
||||||
let mut activating = false;
|
let mut activating = false;
|
||||||
self.state.send_modify(|current_state| {
|
self.state.send_modify(|current_state| {
|
||||||
use pageserver_api::models::ActivatingFrom;
|
use pageserver_api::models::ActivatingFrom;
|
||||||
match &*current_state {
|
match &*current_state {
|
||||||
TenantState::Activating(_) | TenantState::Active | TenantState::Broken { .. } | TenantState::Stopping => {
|
TenantState::Activating(_) | TenantState::Active | TenantState::Broken { .. } | TenantState::Stopping { .. } => {
|
||||||
panic!("caller is responsible for calling activate() only on Loading / Attaching tenants, got {state:?}", state = current_state);
|
panic!("caller is responsible for calling activate() only on Loading / Attaching tenants, got {state:?}", state = current_state);
|
||||||
}
|
}
|
||||||
TenantState::Loading => {
|
TenantState::Loading => {
|
||||||
@@ -1958,8 +1781,17 @@ impl Tenant {
|
|||||||
/// - detach + ignore (freeze_and_flush == false)
|
/// - detach + ignore (freeze_and_flush == false)
|
||||||
///
|
///
|
||||||
/// This will attempt to shutdown even if tenant is broken.
|
/// This will attempt to shutdown even if tenant is broken.
|
||||||
pub(crate) async fn shutdown(&self, freeze_and_flush: bool) -> Result<(), ShutdownError> {
|
///
|
||||||
debug_assert_current_span_has_tenant_id();
|
/// `shutdown_progress` is a [`completion::Barrier`] for the shutdown initiated by this call.
|
||||||
|
/// If the tenant is already shutting down, we return a clone of the first shutdown call's
|
||||||
|
/// `Barrier` as an `Err`. This not-first caller can use the returned barrier to join with
|
||||||
|
/// the ongoing shutdown.
|
||||||
|
async fn shutdown(
|
||||||
|
&self,
|
||||||
|
shutdown_progress: completion::Barrier,
|
||||||
|
freeze_and_flush: bool,
|
||||||
|
) -> Result<(), completion::Barrier> {
|
||||||
|
span::debug_assert_current_span_has_tenant_id();
|
||||||
// Set tenant (and its timlines) to Stoppping state.
|
// Set tenant (and its timlines) to Stoppping state.
|
||||||
//
|
//
|
||||||
// Since we can only transition into Stopping state after activation is complete,
|
// Since we can only transition into Stopping state after activation is complete,
|
||||||
@@ -1977,12 +1809,16 @@ impl Tenant {
|
|||||||
// But the tenant background loops are joined-on in our caller.
|
// But the tenant background loops are joined-on in our caller.
|
||||||
// It's mesed up.
|
// It's mesed up.
|
||||||
// we just ignore the failure to stop
|
// we just ignore the failure to stop
|
||||||
match self.set_stopping().await {
|
|
||||||
|
match self.set_stopping(shutdown_progress).await {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(SetStoppingError::Broken) => {
|
Err(SetStoppingError::Broken) => {
|
||||||
// assume that this is acceptable
|
// assume that this is acceptable
|
||||||
}
|
}
|
||||||
Err(SetStoppingError::AlreadyStopping) => return Err(ShutdownError::AlreadyStopping),
|
Err(SetStoppingError::AlreadyStopping(other)) => {
|
||||||
|
// give caller the option to wait for this this shutdown
|
||||||
|
return Err(other);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if freeze_and_flush {
|
if freeze_and_flush {
|
||||||
@@ -2014,7 +1850,7 @@ impl Tenant {
|
|||||||
/// This function waits for the tenant to become active if it isn't already, before transitioning it into Stopping state.
|
/// This function waits for the tenant to become active if it isn't already, before transitioning it into Stopping state.
|
||||||
///
|
///
|
||||||
/// This function is not cancel-safe!
|
/// This function is not cancel-safe!
|
||||||
async fn set_stopping(&self) -> Result<(), SetStoppingError> {
|
async fn set_stopping(&self, progress: completion::Barrier) -> Result<(), SetStoppingError> {
|
||||||
let mut rx = self.state.subscribe();
|
let mut rx = self.state.subscribe();
|
||||||
|
|
||||||
// cannot stop before we're done activating, so wait out until we're done activating
|
// cannot stop before we're done activating, so wait out until we're done activating
|
||||||
@@ -2026,7 +1862,7 @@ impl Tenant {
|
|||||||
);
|
);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
TenantState::Active | TenantState::Broken { .. } | TenantState::Stopping {} => true,
|
TenantState::Active | TenantState::Broken { .. } | TenantState::Stopping { .. } => true,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("cannot drop self.state while on a &self method");
|
.expect("cannot drop self.state while on a &self method");
|
||||||
@@ -2041,7 +1877,7 @@ impl Tenant {
|
|||||||
// FIXME: due to time-of-check vs time-of-use issues, it can happen that new timelines
|
// FIXME: due to time-of-check vs time-of-use issues, it can happen that new timelines
|
||||||
// are created after the transition to Stopping. That's harmless, as the Timelines
|
// are created after the transition to Stopping. That's harmless, as the Timelines
|
||||||
// won't be accessible to anyone afterwards, because the Tenant is in Stopping state.
|
// won't be accessible to anyone afterwards, because the Tenant is in Stopping state.
|
||||||
*current_state = TenantState::Stopping;
|
*current_state = TenantState::Stopping { progress };
|
||||||
// Continue stopping outside the closure. We need to grab timelines.lock()
|
// Continue stopping outside the closure. We need to grab timelines.lock()
|
||||||
// and we plan to turn it into a tokio::sync::Mutex in a future patch.
|
// and we plan to turn it into a tokio::sync::Mutex in a future patch.
|
||||||
true
|
true
|
||||||
@@ -2053,9 +1889,9 @@ impl Tenant {
|
|||||||
err = Some(SetStoppingError::Broken);
|
err = Some(SetStoppingError::Broken);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
TenantState::Stopping => {
|
TenantState::Stopping { progress } => {
|
||||||
info!("Tenant is already in Stopping state");
|
info!("Tenant is already in Stopping state");
|
||||||
err = Some(SetStoppingError::AlreadyStopping);
|
err = Some(SetStoppingError::AlreadyStopping(progress.clone()));
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2099,7 +1935,7 @@ impl Tenant {
|
|||||||
);
|
);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
TenantState::Active | TenantState::Broken { .. } | TenantState::Stopping {} => true,
|
TenantState::Active | TenantState::Broken { .. } | TenantState::Stopping { .. } => true,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("cannot drop self.state while on a &self method");
|
.expect("cannot drop self.state while on a &self method");
|
||||||
@@ -2122,7 +1958,7 @@ impl Tenant {
|
|||||||
warn!("Tenant is already in Broken state");
|
warn!("Tenant is already in Broken state");
|
||||||
}
|
}
|
||||||
// This is the only "expected" path, any other path is a bug.
|
// This is the only "expected" path, any other path is a bug.
|
||||||
TenantState::Stopping => {
|
TenantState::Stopping { .. } => {
|
||||||
warn!(
|
warn!(
|
||||||
"Marking Stopping tenant as Broken state, reason: {}",
|
"Marking Stopping tenant as Broken state, reason: {}",
|
||||||
reason
|
reason
|
||||||
@@ -2155,7 +1991,7 @@ impl Tenant {
|
|||||||
TenantState::Active { .. } => {
|
TenantState::Active { .. } => {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
TenantState::Broken { .. } | TenantState::Stopping => {
|
TenantState::Broken { .. } | TenantState::Stopping { .. } => {
|
||||||
// There's no chance the tenant can transition back into ::Active
|
// There's no chance the tenant can transition back into ::Active
|
||||||
return Err(WaitToBecomeActiveError::WillNotBecomeActive {
|
return Err(WaitToBecomeActiveError::WillNotBecomeActive {
|
||||||
tenant_id: self.tenant_id,
|
tenant_id: self.tenant_id,
|
||||||
@@ -2405,7 +2241,7 @@ impl Tenant {
|
|||||||
/// Locate and load config
|
/// Locate and load config
|
||||||
pub(super) fn load_tenant_config(
|
pub(super) fn load_tenant_config(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
tenant_id: TenantId,
|
tenant_id: &TenantId,
|
||||||
) -> anyhow::Result<TenantConfOpt> {
|
) -> anyhow::Result<TenantConfOpt> {
|
||||||
let target_config_path = conf.tenant_config_path(tenant_id);
|
let target_config_path = conf.tenant_config_path(tenant_id);
|
||||||
let target_config_display = target_config_path.display();
|
let target_config_display = target_config_path.display();
|
||||||
@@ -2706,7 +2542,7 @@ impl Tenant {
|
|||||||
dst_id: TimelineId,
|
dst_id: TimelineId,
|
||||||
start_lsn: Option<Lsn>,
|
start_lsn: Option<Lsn>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<Arc<Timeline>> {
|
) -> Result<Arc<Timeline>, CreateTimelineError> {
|
||||||
let tl = self
|
let tl = self
|
||||||
.branch_timeline_impl(src_timeline, dst_id, start_lsn, ctx)
|
.branch_timeline_impl(src_timeline, dst_id, start_lsn, ctx)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -2723,7 +2559,7 @@ impl Tenant {
|
|||||||
dst_id: TimelineId,
|
dst_id: TimelineId,
|
||||||
start_lsn: Option<Lsn>,
|
start_lsn: Option<Lsn>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<Arc<Timeline>> {
|
) -> Result<Arc<Timeline>, CreateTimelineError> {
|
||||||
self.branch_timeline_impl(src_timeline, dst_id, start_lsn, ctx)
|
self.branch_timeline_impl(src_timeline, dst_id, start_lsn, ctx)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -2734,7 +2570,7 @@ impl Tenant {
|
|||||||
dst_id: TimelineId,
|
dst_id: TimelineId,
|
||||||
start_lsn: Option<Lsn>,
|
start_lsn: Option<Lsn>,
|
||||||
_ctx: &RequestContext,
|
_ctx: &RequestContext,
|
||||||
) -> anyhow::Result<Arc<Timeline>> {
|
) -> Result<Arc<Timeline>, CreateTimelineError> {
|
||||||
let src_id = src_timeline.timeline_id;
|
let src_id = src_timeline.timeline_id;
|
||||||
|
|
||||||
// If no start LSN is specified, we branch the new timeline from the source timeline's last record LSN
|
// If no start LSN is specified, we branch the new timeline from the source timeline's last record LSN
|
||||||
@@ -2774,16 +2610,17 @@ impl Tenant {
|
|||||||
.context(format!(
|
.context(format!(
|
||||||
"invalid branch start lsn: less than latest GC cutoff {}",
|
"invalid branch start lsn: less than latest GC cutoff {}",
|
||||||
*latest_gc_cutoff_lsn,
|
*latest_gc_cutoff_lsn,
|
||||||
))?;
|
))
|
||||||
|
.map_err(CreateTimelineError::AncestorLsn)?;
|
||||||
|
|
||||||
// and then the planned GC cutoff
|
// and then the planned GC cutoff
|
||||||
{
|
{
|
||||||
let gc_info = src_timeline.gc_info.read().unwrap();
|
let gc_info = src_timeline.gc_info.read().unwrap();
|
||||||
let cutoff = min(gc_info.pitr_cutoff, gc_info.horizon_cutoff);
|
let cutoff = min(gc_info.pitr_cutoff, gc_info.horizon_cutoff);
|
||||||
if start_lsn < cutoff {
|
if start_lsn < cutoff {
|
||||||
bail!(format!(
|
return Err(CreateTimelineError::AncestorLsn(anyhow::anyhow!(
|
||||||
"invalid branch start lsn: less than planned GC cutoff {cutoff}"
|
"invalid branch start lsn: less than planned GC cutoff {cutoff}"
|
||||||
));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2991,7 +2828,7 @@ impl Tenant {
|
|||||||
timeline_struct.init_empty_layer_map(start_lsn);
|
timeline_struct.init_empty_layer_map(start_lsn);
|
||||||
|
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
self.create_timeline_files(&uninit_mark.timeline_path, new_timeline_id, new_metadata)
|
self.create_timeline_files(&uninit_mark.timeline_path, &new_timeline_id, new_metadata)
|
||||||
{
|
{
|
||||||
error!("Failed to create initial files for timeline {tenant_id}/{new_timeline_id}, cleaning up: {e:?}");
|
error!("Failed to create initial files for timeline {tenant_id}/{new_timeline_id}, cleaning up: {e:?}");
|
||||||
cleanup_timeline_directory(uninit_mark);
|
cleanup_timeline_directory(uninit_mark);
|
||||||
@@ -3000,17 +2837,17 @@ impl Tenant {
|
|||||||
|
|
||||||
debug!("Successfully created initial files for timeline {tenant_id}/{new_timeline_id}");
|
debug!("Successfully created initial files for timeline {tenant_id}/{new_timeline_id}");
|
||||||
|
|
||||||
Ok(UninitializedTimeline {
|
Ok(UninitializedTimeline::new(
|
||||||
owning_tenant: self,
|
self,
|
||||||
timeline_id: new_timeline_id,
|
new_timeline_id,
|
||||||
raw_timeline: Some((timeline_struct, uninit_mark)),
|
Some((timeline_struct, uninit_mark)),
|
||||||
})
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_timeline_files(
|
fn create_timeline_files(
|
||||||
&self,
|
&self,
|
||||||
timeline_path: &Path,
|
timeline_path: &Path,
|
||||||
new_timeline_id: TimelineId,
|
new_timeline_id: &TimelineId,
|
||||||
new_metadata: &TimelineMetadata,
|
new_metadata: &TimelineMetadata,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
crashsafe::create_dir(timeline_path).context("Failed to create timeline directory")?;
|
crashsafe::create_dir(timeline_path).context("Failed to create timeline directory")?;
|
||||||
@@ -3021,8 +2858,8 @@ impl Tenant {
|
|||||||
|
|
||||||
save_metadata(
|
save_metadata(
|
||||||
self.conf,
|
self.conf,
|
||||||
|
&self.tenant_id,
|
||||||
new_timeline_id,
|
new_timeline_id,
|
||||||
self.tenant_id,
|
|
||||||
new_metadata,
|
new_metadata,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
@@ -3045,7 +2882,7 @@ impl Tenant {
|
|||||||
timelines.get(&timeline_id).is_none(),
|
timelines.get(&timeline_id).is_none(),
|
||||||
"Timeline {tenant_id}/{timeline_id} already exists in pageserver's memory"
|
"Timeline {tenant_id}/{timeline_id} already exists in pageserver's memory"
|
||||||
);
|
);
|
||||||
let timeline_path = self.conf.timeline_path(&timeline_id, &tenant_id);
|
let timeline_path = self.conf.timeline_path(&tenant_id, &timeline_id);
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
!timeline_path.exists(),
|
!timeline_path.exists(),
|
||||||
"Timeline {} already exists, cannot create its uninit mark file",
|
"Timeline {} already exists, cannot create its uninit mark file",
|
||||||
@@ -3176,10 +3013,10 @@ pub(crate) enum CreateTenantFilesMode {
|
|||||||
pub(crate) fn create_tenant_files(
|
pub(crate) fn create_tenant_files(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
tenant_conf: TenantConfOpt,
|
tenant_conf: TenantConfOpt,
|
||||||
tenant_id: TenantId,
|
tenant_id: &TenantId,
|
||||||
mode: CreateTenantFilesMode,
|
mode: CreateTenantFilesMode,
|
||||||
) -> anyhow::Result<PathBuf> {
|
) -> anyhow::Result<PathBuf> {
|
||||||
let target_tenant_directory = conf.tenant_path(&tenant_id);
|
let target_tenant_directory = conf.tenant_path(tenant_id);
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
!target_tenant_directory
|
!target_tenant_directory
|
||||||
.try_exists()
|
.try_exists()
|
||||||
@@ -3230,7 +3067,7 @@ pub(crate) fn create_tenant_files(
|
|||||||
fn try_create_target_tenant_dir(
|
fn try_create_target_tenant_dir(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
tenant_conf: TenantConfOpt,
|
tenant_conf: TenantConfOpt,
|
||||||
tenant_id: TenantId,
|
tenant_id: &TenantId,
|
||||||
mode: CreateTenantFilesMode,
|
mode: CreateTenantFilesMode,
|
||||||
temporary_tenant_dir: &Path,
|
temporary_tenant_dir: &Path,
|
||||||
target_tenant_directory: &Path,
|
target_tenant_directory: &Path,
|
||||||
@@ -3254,7 +3091,7 @@ fn try_create_target_tenant_dir(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let temporary_tenant_timelines_dir = rebase_directory(
|
let temporary_tenant_timelines_dir = rebase_directory(
|
||||||
&conf.timelines_path(&tenant_id),
|
&conf.timelines_path(tenant_id),
|
||||||
target_tenant_directory,
|
target_tenant_directory,
|
||||||
temporary_tenant_dir,
|
temporary_tenant_dir,
|
||||||
)
|
)
|
||||||
@@ -3266,7 +3103,7 @@ fn try_create_target_tenant_dir(
|
|||||||
)
|
)
|
||||||
.with_context(|| format!("resolve tenant {tenant_id} temporary config path"))?;
|
.with_context(|| format!("resolve tenant {tenant_id} temporary config path"))?;
|
||||||
|
|
||||||
Tenant::persist_tenant_config(&tenant_id, &temporary_tenant_config_path, tenant_conf, true)?;
|
Tenant::persist_tenant_config(tenant_id, &temporary_tenant_config_path, tenant_conf, true)?;
|
||||||
|
|
||||||
crashsafe::create_dir(&temporary_tenant_timelines_dir).with_context(|| {
|
crashsafe::create_dir(&temporary_tenant_timelines_dir).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
@@ -3522,14 +3359,18 @@ pub mod harness {
|
|||||||
pub async fn load(&self) -> (Arc<Tenant>, RequestContext) {
|
pub async fn load(&self) -> (Arc<Tenant>, RequestContext) {
|
||||||
let ctx = RequestContext::new(TaskKind::UnitTest, DownloadBehavior::Error);
|
let ctx = RequestContext::new(TaskKind::UnitTest, DownloadBehavior::Error);
|
||||||
(
|
(
|
||||||
self.try_load(&ctx)
|
self.try_load(&ctx, None)
|
||||||
.await
|
.await
|
||||||
.expect("failed to load test tenant"),
|
.expect("failed to load test tenant"),
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn try_load(&self, ctx: &RequestContext) -> anyhow::Result<Arc<Tenant>> {
|
pub async fn try_load(
|
||||||
|
&self,
|
||||||
|
ctx: &RequestContext,
|
||||||
|
remote_storage: Option<remote_storage::GenericRemoteStorage>,
|
||||||
|
) -> anyhow::Result<Arc<Tenant>> {
|
||||||
let walredo_mgr = Arc::new(TestRedoManager);
|
let walredo_mgr = Arc::new(TestRedoManager);
|
||||||
|
|
||||||
let tenant = Arc::new(Tenant::new(
|
let tenant = Arc::new(Tenant::new(
|
||||||
@@ -3538,7 +3379,7 @@ pub mod harness {
|
|||||||
TenantConfOpt::from(self.tenant_conf),
|
TenantConfOpt::from(self.tenant_conf),
|
||||||
walredo_mgr,
|
walredo_mgr,
|
||||||
self.tenant_id,
|
self.tenant_id,
|
||||||
None,
|
remote_storage,
|
||||||
));
|
));
|
||||||
tenant
|
tenant
|
||||||
.load(None, ctx)
|
.load(None, ctx)
|
||||||
@@ -3554,7 +3395,7 @@ pub mod harness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn timeline_path(&self, timeline_id: &TimelineId) -> PathBuf {
|
pub fn timeline_path(&self, timeline_id: &TimelineId) -> PathBuf {
|
||||||
self.conf.timeline_path(timeline_id, &self.tenant_id)
|
self.conf.timeline_path(&self.tenant_id, timeline_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3816,6 +3657,9 @@ mod tests {
|
|||||||
{
|
{
|
||||||
Ok(_) => panic!("branching should have failed"),
|
Ok(_) => panic!("branching should have failed"),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
let CreateTimelineError::AncestorLsn(err) = err else {
|
||||||
|
panic!("wrong error type")
|
||||||
|
};
|
||||||
assert!(err.to_string().contains("invalid branch start lsn"));
|
assert!(err.to_string().contains("invalid branch start lsn"));
|
||||||
assert!(err
|
assert!(err
|
||||||
.source()
|
.source()
|
||||||
@@ -3845,6 +3689,9 @@ mod tests {
|
|||||||
{
|
{
|
||||||
Ok(_) => panic!("branching should have failed"),
|
Ok(_) => panic!("branching should have failed"),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
let CreateTimelineError::AncestorLsn(err) = err else {
|
||||||
|
panic!("wrong error type");
|
||||||
|
};
|
||||||
assert!(&err.to_string().contains("invalid branch start lsn"));
|
assert!(&err.to_string().contains("invalid branch start lsn"));
|
||||||
assert!(&err
|
assert!(&err
|
||||||
.source()
|
.source()
|
||||||
@@ -4070,7 +3917,11 @@ mod tests {
|
|||||||
metadata_bytes[8] ^= 1;
|
metadata_bytes[8] ^= 1;
|
||||||
std::fs::write(metadata_path, metadata_bytes)?;
|
std::fs::write(metadata_path, metadata_bytes)?;
|
||||||
|
|
||||||
let err = harness.try_load(&ctx).await.err().expect("should fail");
|
let err = harness
|
||||||
|
.try_load(&ctx, None)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.expect("should fail");
|
||||||
// get all the stack with all .context, not tonly the last one
|
// get all the stack with all .context, not tonly the last one
|
||||||
let message = format!("{err:#}");
|
let message = format!("{err:#}");
|
||||||
let expected = "Failed to parse metadata bytes from path";
|
let expected = "Failed to parse metadata bytes from path";
|
||||||
@@ -4501,13 +4352,13 @@ mod tests {
|
|||||||
// assert freeze_and_flush exercised the initdb optimization
|
// assert freeze_and_flush exercised the initdb optimization
|
||||||
{
|
{
|
||||||
let state = tline.flush_loop_state.lock().unwrap();
|
let state = tline.flush_loop_state.lock().unwrap();
|
||||||
let
|
let timeline::FlushLoopState::Running {
|
||||||
timeline::FlushLoopState::Running {
|
expect_initdb_optimization,
|
||||||
expect_initdb_optimization,
|
initdb_optimization_count,
|
||||||
initdb_optimization_count,
|
} = *state
|
||||||
} = *state else {
|
else {
|
||||||
panic!("unexpected state: {:?}", *state);
|
panic!("unexpected state: {:?}", *state);
|
||||||
};
|
};
|
||||||
assert!(expect_initdb_optimization);
|
assert!(expect_initdb_optimization);
|
||||||
assert!(initdb_optimization_count > 0);
|
assert!(initdb_optimization_count > 0);
|
||||||
}
|
}
|
||||||
@@ -4542,7 +4393,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(!harness
|
assert!(!harness
|
||||||
.conf
|
.conf
|
||||||
.timeline_path(&TIMELINE_ID, &tenant.tenant_id)
|
.timeline_path(&tenant.tenant_id, &TIMELINE_ID)
|
||||||
.exists());
|
.exists());
|
||||||
|
|
||||||
assert!(!harness
|
assert!(!harness
|
||||||
@@ -4553,28 +4404,3 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn debug_assert_current_span_has_tenant_id() {}
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
pub static TENANT_ID_EXTRACTOR: once_cell::sync::Lazy<
|
|
||||||
utils::tracing_span_assert::MultiNameExtractor<2>,
|
|
||||||
> = once_cell::sync::Lazy::new(|| {
|
|
||||||
utils::tracing_span_assert::MultiNameExtractor::new("TenantId", ["tenant_id", "tenant"])
|
|
||||||
});
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn debug_assert_current_span_has_tenant_id() {
|
|
||||||
use utils::tracing_span_assert;
|
|
||||||
|
|
||||||
match tracing_span_assert::check_fields_present([&*TENANT_ID_EXTRACTOR]) {
|
|
||||||
Ok(()) => (),
|
|
||||||
Err(missing) => panic!(
|
|
||||||
"missing extractors: {:?}",
|
|
||||||
missing.into_iter().map(|e| e.name()).collect::<Vec<_>>()
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -442,7 +442,7 @@ where
|
|||||||
writer: W,
|
writer: W,
|
||||||
|
|
||||||
///
|
///
|
||||||
/// stack[0] is the current root page, stack.last() is the leaf.
|
/// `stack[0]` is the current root page, `stack.last()` is the leaf.
|
||||||
///
|
///
|
||||||
/// We maintain the length of the stack to be always greater than zero.
|
/// We maintain the length of the stack to be always greater than zero.
|
||||||
/// Two exceptions are:
|
/// Two exceptions are:
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ impl EphemeralFile {
|
|||||||
l.next_file_id += 1;
|
l.next_file_id += 1;
|
||||||
|
|
||||||
let filename = conf
|
let filename = conf
|
||||||
.timeline_path(&timeline_id, &tenant_id)
|
.timeline_path(&tenant_id, &timeline_id)
|
||||||
.join(PathBuf::from(format!("ephemeral-{}", file_id)));
|
.join(PathBuf::from(format!("ephemeral-{}", file_id)));
|
||||||
|
|
||||||
let file = VirtualFile::open_with_options(
|
let file = VirtualFile::open_with_options(
|
||||||
@@ -346,7 +346,7 @@ mod tests {
|
|||||||
|
|
||||||
let tenant_id = TenantId::from_str("11000000000000000000000000000000").unwrap();
|
let tenant_id = TenantId::from_str("11000000000000000000000000000000").unwrap();
|
||||||
let timeline_id = TimelineId::from_str("22000000000000000000000000000000").unwrap();
|
let timeline_id = TimelineId::from_str("22000000000000000000000000000000").unwrap();
|
||||||
fs::create_dir_all(conf.timeline_path(&timeline_id, &tenant_id))?;
|
fs::create_dir_all(conf.timeline_path(&tenant_id, &timeline_id))?;
|
||||||
|
|
||||||
Ok((conf, tenant_id, timeline_id))
|
Ok((conf, tenant_id, timeline_id))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
//! Other read methods are less critical but still impact performance of background tasks.
|
//! Other read methods are less critical but still impact performance of background tasks.
|
||||||
//!
|
//!
|
||||||
//! This data structure relies on a persistent/immutable binary search tree. See the
|
//! This data structure relies on a persistent/immutable binary search tree. See the
|
||||||
//! following lecture for an introduction https://www.youtube.com/watch?v=WqCWghETNDc&t=581s
|
//! following lecture for an introduction <https://www.youtube.com/watch?v=WqCWghETNDc&t=581s>
|
||||||
//! Summary: A persistent/immutable BST (and persistent data structures in general) allows
|
//! Summary: A persistent/immutable BST (and persistent data structures in general) allows
|
||||||
//! you to modify the tree in such a way that each modification creates a new "version"
|
//! you to modify the tree in such a way that each modification creates a new "version"
|
||||||
//! of the tree. When you modify it, you get a new version, but all previous versions are
|
//! of the tree. When you modify it, you get a new version, but all previous versions are
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
//! afterwards. We can add layers as long as they have larger LSNs than any previous layer in
|
//! afterwards. We can add layers as long as they have larger LSNs than any previous layer in
|
||||||
//! the map, but if we need to remove a layer, or insert anything with an older LSN, we need
|
//! the map, but if we need to remove a layer, or insert anything with an older LSN, we need
|
||||||
//! to throw away most of the persistent BST and build a new one, starting from the oldest
|
//! to throw away most of the persistent BST and build a new one, starting from the oldest
|
||||||
//! LSN. See `LayerMap::flush_updates()`.
|
//! LSN. See [`LayerMap::flush_updates()`].
|
||||||
//!
|
//!
|
||||||
|
|
||||||
mod historic_layer_coverage;
|
mod historic_layer_coverage;
|
||||||
@@ -51,25 +51,22 @@ use crate::keyspace::KeyPartitioning;
|
|||||||
use crate::repository::Key;
|
use crate::repository::Key;
|
||||||
use crate::tenant::storage_layer::InMemoryLayer;
|
use crate::tenant::storage_layer::InMemoryLayer;
|
||||||
use crate::tenant::storage_layer::Layer;
|
use crate::tenant::storage_layer::Layer;
|
||||||
use anyhow::Context;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use utils::lsn::Lsn;
|
use utils::lsn::Lsn;
|
||||||
|
|
||||||
use historic_layer_coverage::BufferedHistoricLayerCoverage;
|
use historic_layer_coverage::BufferedHistoricLayerCoverage;
|
||||||
pub use historic_layer_coverage::Replacement;
|
pub use historic_layer_coverage::LayerKey;
|
||||||
|
|
||||||
use super::storage_layer::range_eq;
|
|
||||||
use super::storage_layer::PersistentLayerDesc;
|
use super::storage_layer::PersistentLayerDesc;
|
||||||
use super::storage_layer::PersistentLayerKey;
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// LayerMap tracks what layers exist on a timeline.
|
/// LayerMap tracks what layers exist on a timeline.
|
||||||
///
|
///
|
||||||
pub struct LayerMap<L: ?Sized> {
|
#[derive(Default)]
|
||||||
|
pub struct LayerMap {
|
||||||
//
|
//
|
||||||
// 'open_layer' holds the current InMemoryLayer that is accepting new
|
// 'open_layer' holds the current InMemoryLayer that is accepting new
|
||||||
// records. If it is None, 'next_open_layer_at' will be set instead, indicating
|
// records. If it is None, 'next_open_layer_at' will be set instead, indicating
|
||||||
@@ -95,24 +92,6 @@ pub struct LayerMap<L: ?Sized> {
|
|||||||
/// L0 layers have key range Key::MIN..Key::MAX, and locating them using R-Tree search is very inefficient.
|
/// L0 layers have key range Key::MIN..Key::MAX, and locating them using R-Tree search is very inefficient.
|
||||||
/// So L0 layers are held in l0_delta_layers vector, in addition to the R-tree.
|
/// So L0 layers are held in l0_delta_layers vector, in addition to the R-tree.
|
||||||
l0_delta_layers: Vec<Arc<PersistentLayerDesc>>,
|
l0_delta_layers: Vec<Arc<PersistentLayerDesc>>,
|
||||||
|
|
||||||
/// Mapping from persistent layer key to the actual layer object. Currently, it stores delta, image, and
|
|
||||||
/// remote layers. In future refactors, this will be eventually moved out of LayerMap into Timeline, and
|
|
||||||
/// RemoteLayer will be removed.
|
|
||||||
mapping: HashMap<PersistentLayerKey, Arc<L>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<L: ?Sized> Default for LayerMap<L> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
open_layer: None,
|
|
||||||
next_open_layer_at: None,
|
|
||||||
frozen_layers: VecDeque::default(),
|
|
||||||
l0_delta_layers: Vec::default(),
|
|
||||||
historic: BufferedHistoricLayerCoverage::default(),
|
|
||||||
mapping: HashMap::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The primary update API for the layer map.
|
/// The primary update API for the layer map.
|
||||||
@@ -120,24 +99,21 @@ impl<L: ?Sized> Default for LayerMap<L> {
|
|||||||
/// Batching historic layer insertions and removals is good for
|
/// Batching historic layer insertions and removals is good for
|
||||||
/// performance and this struct helps us do that correctly.
|
/// performance and this struct helps us do that correctly.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub struct BatchedUpdates<'a, L: ?Sized + Layer> {
|
pub struct BatchedUpdates<'a> {
|
||||||
// While we hold this exclusive reference to the layer map the type checker
|
// While we hold this exclusive reference to the layer map the type checker
|
||||||
// will prevent us from accidentally reading any unflushed updates.
|
// will prevent us from accidentally reading any unflushed updates.
|
||||||
layer_map: &'a mut LayerMap<L>,
|
layer_map: &'a mut LayerMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provide ability to batch more updates while hiding the read
|
/// Provide ability to batch more updates while hiding the read
|
||||||
/// API so we don't accidentally read without flushing.
|
/// API so we don't accidentally read without flushing.
|
||||||
impl<L> BatchedUpdates<'_, L>
|
impl BatchedUpdates<'_> {
|
||||||
where
|
|
||||||
L: ?Sized + Layer,
|
|
||||||
{
|
|
||||||
///
|
///
|
||||||
/// Insert an on-disk layer.
|
/// Insert an on-disk layer.
|
||||||
///
|
///
|
||||||
// TODO remove the `layer` argument when `mapping` is refactored out of `LayerMap`
|
// TODO remove the `layer` argument when `mapping` is refactored out of `LayerMap`
|
||||||
pub fn insert_historic(&mut self, layer_desc: PersistentLayerDesc, layer: Arc<L>) {
|
pub fn insert_historic(&mut self, layer_desc: PersistentLayerDesc) {
|
||||||
self.layer_map.insert_historic_noflush(layer_desc, layer)
|
self.layer_map.insert_historic_noflush(layer_desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -145,31 +121,8 @@ where
|
|||||||
///
|
///
|
||||||
/// This should be called when the corresponding file on disk has been deleted.
|
/// This should be called when the corresponding file on disk has been deleted.
|
||||||
///
|
///
|
||||||
pub fn remove_historic(&mut self, layer_desc: PersistentLayerDesc, layer: Arc<L>) {
|
pub fn remove_historic(&mut self, layer_desc: PersistentLayerDesc) {
|
||||||
self.layer_map.remove_historic_noflush(layer_desc, layer)
|
self.layer_map.remove_historic_noflush(layer_desc)
|
||||||
}
|
|
||||||
|
|
||||||
/// Replaces existing layer iff it is the `expected`.
|
|
||||||
///
|
|
||||||
/// If the expected layer has been removed it will not be inserted by this function.
|
|
||||||
///
|
|
||||||
/// Returned `Replacement` describes succeeding in replacement or the reason why it could not
|
|
||||||
/// be done.
|
|
||||||
///
|
|
||||||
/// TODO replacement can be done without buffering and rebuilding layer map updates.
|
|
||||||
/// One way to do that is to add a layer of indirection for returned values, so
|
|
||||||
/// that we can replace values only by updating a hashmap.
|
|
||||||
pub fn replace_historic(
|
|
||||||
&mut self,
|
|
||||||
expected_desc: PersistentLayerDesc,
|
|
||||||
expected: &Arc<L>,
|
|
||||||
new_desc: PersistentLayerDesc,
|
|
||||||
new: Arc<L>,
|
|
||||||
) -> anyhow::Result<Replacement<Arc<L>>> {
|
|
||||||
fail::fail_point!("layermap-replace-notfound", |_| Ok(Replacement::NotFound));
|
|
||||||
|
|
||||||
self.layer_map
|
|
||||||
.replace_historic_noflush(expected_desc, expected, new_desc, new)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We will flush on drop anyway, but this method makes it
|
// We will flush on drop anyway, but this method makes it
|
||||||
@@ -185,25 +138,19 @@ where
|
|||||||
// than panic later or read without flushing.
|
// than panic later or read without flushing.
|
||||||
//
|
//
|
||||||
// TODO maybe warn if flush hasn't explicitly been called
|
// TODO maybe warn if flush hasn't explicitly been called
|
||||||
impl<L> Drop for BatchedUpdates<'_, L>
|
impl Drop for BatchedUpdates<'_> {
|
||||||
where
|
|
||||||
L: ?Sized + Layer,
|
|
||||||
{
|
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.layer_map.flush_updates();
|
self.layer_map.flush_updates();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return value of LayerMap::search
|
/// Return value of LayerMap::search
|
||||||
pub struct SearchResult<L: ?Sized> {
|
pub struct SearchResult {
|
||||||
pub layer: Arc<L>,
|
pub layer: Arc<PersistentLayerDesc>,
|
||||||
pub lsn_floor: Lsn,
|
pub lsn_floor: Lsn,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<L> LayerMap<L>
|
impl LayerMap {
|
||||||
where
|
|
||||||
L: ?Sized + Layer,
|
|
||||||
{
|
|
||||||
///
|
///
|
||||||
/// Find the latest layer (by lsn.end) that covers the given
|
/// Find the latest layer (by lsn.end) that covers the given
|
||||||
/// 'key', with lsn.start < 'end_lsn'.
|
/// 'key', with lsn.start < 'end_lsn'.
|
||||||
@@ -235,7 +182,7 @@ where
|
|||||||
/// NOTE: This only searches the 'historic' layers, *not* the
|
/// NOTE: This only searches the 'historic' layers, *not* the
|
||||||
/// 'open' and 'frozen' layers!
|
/// 'open' and 'frozen' layers!
|
||||||
///
|
///
|
||||||
pub fn search(&self, key: Key, end_lsn: Lsn) -> Option<SearchResult<L>> {
|
pub fn search(&self, key: Key, end_lsn: Lsn) -> Option<SearchResult> {
|
||||||
let version = self.historic.get().unwrap().get_version(end_lsn.0 - 1)?;
|
let version = self.historic.get().unwrap().get_version(end_lsn.0 - 1)?;
|
||||||
let latest_delta = version.delta_coverage.query(key.to_i128());
|
let latest_delta = version.delta_coverage.query(key.to_i128());
|
||||||
let latest_image = version.image_coverage.query(key.to_i128());
|
let latest_image = version.image_coverage.query(key.to_i128());
|
||||||
@@ -244,7 +191,6 @@ where
|
|||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
(None, Some(image)) => {
|
(None, Some(image)) => {
|
||||||
let lsn_floor = image.get_lsn_range().start;
|
let lsn_floor = image.get_lsn_range().start;
|
||||||
let image = self.get_layer_from_mapping(&image.key()).clone();
|
|
||||||
Some(SearchResult {
|
Some(SearchResult {
|
||||||
layer: image,
|
layer: image,
|
||||||
lsn_floor,
|
lsn_floor,
|
||||||
@@ -252,7 +198,6 @@ where
|
|||||||
}
|
}
|
||||||
(Some(delta), None) => {
|
(Some(delta), None) => {
|
||||||
let lsn_floor = delta.get_lsn_range().start;
|
let lsn_floor = delta.get_lsn_range().start;
|
||||||
let delta = self.get_layer_from_mapping(&delta.key()).clone();
|
|
||||||
Some(SearchResult {
|
Some(SearchResult {
|
||||||
layer: delta,
|
layer: delta,
|
||||||
lsn_floor,
|
lsn_floor,
|
||||||
@@ -263,7 +208,6 @@ where
|
|||||||
let image_is_newer = image.get_lsn_range().end >= delta.get_lsn_range().end;
|
let image_is_newer = image.get_lsn_range().end >= delta.get_lsn_range().end;
|
||||||
let image_exact_match = img_lsn + 1 == end_lsn;
|
let image_exact_match = img_lsn + 1 == end_lsn;
|
||||||
if image_is_newer || image_exact_match {
|
if image_is_newer || image_exact_match {
|
||||||
let image = self.get_layer_from_mapping(&image.key()).clone();
|
|
||||||
Some(SearchResult {
|
Some(SearchResult {
|
||||||
layer: image,
|
layer: image,
|
||||||
lsn_floor: img_lsn,
|
lsn_floor: img_lsn,
|
||||||
@@ -271,7 +215,6 @@ where
|
|||||||
} else {
|
} else {
|
||||||
let lsn_floor =
|
let lsn_floor =
|
||||||
std::cmp::max(delta.get_lsn_range().start, image.get_lsn_range().start + 1);
|
std::cmp::max(delta.get_lsn_range().start, image.get_lsn_range().start + 1);
|
||||||
let delta = self.get_layer_from_mapping(&delta.key()).clone();
|
|
||||||
Some(SearchResult {
|
Some(SearchResult {
|
||||||
layer: delta,
|
layer: delta,
|
||||||
lsn_floor,
|
lsn_floor,
|
||||||
@@ -282,7 +225,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Start a batch of updates, applied on drop
|
/// Start a batch of updates, applied on drop
|
||||||
pub fn batch_update(&mut self) -> BatchedUpdates<'_, L> {
|
pub fn batch_update(&mut self) -> BatchedUpdates<'_> {
|
||||||
BatchedUpdates { layer_map: self }
|
BatchedUpdates { layer_map: self }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,48 +235,32 @@ where
|
|||||||
/// Helper function for BatchedUpdates::insert_historic
|
/// Helper function for BatchedUpdates::insert_historic
|
||||||
///
|
///
|
||||||
/// TODO(chi): remove L generic so that we do not need to pass layer object.
|
/// TODO(chi): remove L generic so that we do not need to pass layer object.
|
||||||
pub(self) fn insert_historic_noflush(
|
pub(self) fn insert_historic_noflush(&mut self, layer_desc: PersistentLayerDesc) {
|
||||||
&mut self,
|
|
||||||
layer_desc: PersistentLayerDesc,
|
|
||||||
layer: Arc<L>,
|
|
||||||
) {
|
|
||||||
self.mapping.insert(layer_desc.key(), layer.clone());
|
|
||||||
|
|
||||||
// TODO: See #3869, resulting #4088, attempted fix and repro #4094
|
// TODO: See #3869, resulting #4088, attempted fix and repro #4094
|
||||||
|
|
||||||
if Self::is_l0(&layer) {
|
if Self::is_l0(&layer_desc) {
|
||||||
self.l0_delta_layers.push(layer_desc.clone().into());
|
self.l0_delta_layers.push(layer_desc.clone().into());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.historic.insert(
|
self.historic.insert(
|
||||||
historic_layer_coverage::LayerKey::from(&*layer),
|
historic_layer_coverage::LayerKey::from(&layer_desc),
|
||||||
layer_desc.into(),
|
layer_desc.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_layer_from_mapping(&self, key: &PersistentLayerKey) -> &Arc<L> {
|
|
||||||
let layer = self
|
|
||||||
.mapping
|
|
||||||
.get(key)
|
|
||||||
.with_context(|| format!("{key:?}"))
|
|
||||||
.expect("inconsistent layer mapping");
|
|
||||||
layer
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Remove an on-disk layer from the map.
|
/// Remove an on-disk layer from the map.
|
||||||
///
|
///
|
||||||
/// Helper function for BatchedUpdates::remove_historic
|
/// Helper function for BatchedUpdates::remove_historic
|
||||||
///
|
///
|
||||||
pub fn remove_historic_noflush(&mut self, layer_desc: PersistentLayerDesc, layer: Arc<L>) {
|
pub fn remove_historic_noflush(&mut self, layer_desc: PersistentLayerDesc) {
|
||||||
self.historic
|
self.historic
|
||||||
.remove(historic_layer_coverage::LayerKey::from(&*layer));
|
.remove(historic_layer_coverage::LayerKey::from(&layer_desc));
|
||||||
if Self::is_l0(&layer) {
|
let layer_key = layer_desc.key();
|
||||||
|
if Self::is_l0(&layer_desc) {
|
||||||
let len_before = self.l0_delta_layers.len();
|
let len_before = self.l0_delta_layers.len();
|
||||||
let mut l0_delta_layers = std::mem::take(&mut self.l0_delta_layers);
|
let mut l0_delta_layers = std::mem::take(&mut self.l0_delta_layers);
|
||||||
l0_delta_layers.retain(|other| {
|
l0_delta_layers.retain(|other| other.key() != layer_key);
|
||||||
!Self::compare_arced_layers(self.get_layer_from_mapping(&other.key()), &layer)
|
|
||||||
});
|
|
||||||
self.l0_delta_layers = l0_delta_layers;
|
self.l0_delta_layers = l0_delta_layers;
|
||||||
// this assertion is related to use of Arc::ptr_eq in Self::compare_arced_layers,
|
// this assertion is related to use of Arc::ptr_eq in Self::compare_arced_layers,
|
||||||
// there's a chance that the comparison fails at runtime due to it comparing (pointer,
|
// there's a chance that the comparison fails at runtime due to it comparing (pointer,
|
||||||
@@ -344,69 +271,6 @@ where
|
|||||||
"failed to locate removed historic layer from l0_delta_layers"
|
"failed to locate removed historic layer from l0_delta_layers"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.mapping.remove(&layer_desc.key());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(self) fn replace_historic_noflush(
|
|
||||||
&mut self,
|
|
||||||
expected_desc: PersistentLayerDesc,
|
|
||||||
expected: &Arc<L>,
|
|
||||||
new_desc: PersistentLayerDesc,
|
|
||||||
new: Arc<L>,
|
|
||||||
) -> anyhow::Result<Replacement<Arc<L>>> {
|
|
||||||
let key = historic_layer_coverage::LayerKey::from(&**expected);
|
|
||||||
let other = historic_layer_coverage::LayerKey::from(&*new);
|
|
||||||
|
|
||||||
let expected_l0 = Self::is_l0(expected);
|
|
||||||
let new_l0 = Self::is_l0(&new);
|
|
||||||
|
|
||||||
anyhow::ensure!(
|
|
||||||
key == other,
|
|
||||||
"expected and new must have equal LayerKeys: {key:?} != {other:?}"
|
|
||||||
);
|
|
||||||
|
|
||||||
anyhow::ensure!(
|
|
||||||
expected_l0 == new_l0,
|
|
||||||
"expected and new must both be l0 deltas or neither should be: {expected_l0} != {new_l0}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let l0_index = if expected_l0 {
|
|
||||||
// find the index in case replace worked, we need to replace that as well
|
|
||||||
let pos = self.l0_delta_layers.iter().position(|slot| {
|
|
||||||
Self::compare_arced_layers(self.get_layer_from_mapping(&slot.key()), expected)
|
|
||||||
});
|
|
||||||
|
|
||||||
if pos.is_none() {
|
|
||||||
return Ok(Replacement::NotFound);
|
|
||||||
}
|
|
||||||
pos
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_desc = Arc::new(new_desc);
|
|
||||||
let replaced = self.historic.replace(&key, new_desc.clone(), |existing| {
|
|
||||||
**existing == expected_desc
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Replacement::Replaced { .. } = &replaced {
|
|
||||||
self.mapping.remove(&expected_desc.key());
|
|
||||||
self.mapping.insert(new_desc.key(), new);
|
|
||||||
if let Some(index) = l0_index {
|
|
||||||
self.l0_delta_layers[index] = new_desc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let replaced = match replaced {
|
|
||||||
Replacement::Replaced { in_buffered } => Replacement::Replaced { in_buffered },
|
|
||||||
Replacement::NotFound => Replacement::NotFound,
|
|
||||||
Replacement::RemovalBuffered => Replacement::RemovalBuffered,
|
|
||||||
Replacement::Unexpected(x) => {
|
|
||||||
Replacement::Unexpected(self.get_layer_from_mapping(&x.key()).clone())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(replaced)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function for BatchedUpdates::drop.
|
/// Helper function for BatchedUpdates::drop.
|
||||||
@@ -454,10 +318,8 @@ where
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter_historic_layers(&self) -> impl '_ + Iterator<Item = Arc<L>> {
|
pub fn iter_historic_layers(&self) -> impl '_ + Iterator<Item = Arc<PersistentLayerDesc>> {
|
||||||
self.historic
|
self.historic.iter()
|
||||||
.iter()
|
|
||||||
.map(|x| self.get_layer_from_mapping(&x.key()).clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -472,7 +334,7 @@ where
|
|||||||
&self,
|
&self,
|
||||||
key_range: &Range<Key>,
|
key_range: &Range<Key>,
|
||||||
lsn: Lsn,
|
lsn: Lsn,
|
||||||
) -> Result<Vec<(Range<Key>, Option<Arc<L>>)>> {
|
) -> Result<Vec<(Range<Key>, Option<Arc<PersistentLayerDesc>>)>> {
|
||||||
let version = match self.historic.get().unwrap().get_version(lsn.0) {
|
let version = match self.historic.get().unwrap().get_version(lsn.0) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return Ok(vec![]),
|
None => return Ok(vec![]),
|
||||||
@@ -482,37 +344,27 @@ where
|
|||||||
let end = key_range.end.to_i128();
|
let end = key_range.end.to_i128();
|
||||||
|
|
||||||
// Initialize loop variables
|
// Initialize loop variables
|
||||||
let mut coverage: Vec<(Range<Key>, Option<Arc<L>>)> = vec![];
|
let mut coverage: Vec<(Range<Key>, Option<Arc<PersistentLayerDesc>>)> = vec![];
|
||||||
let mut current_key = start;
|
let mut current_key = start;
|
||||||
let mut current_val = version.image_coverage.query(start);
|
let mut current_val = version.image_coverage.query(start);
|
||||||
|
|
||||||
// Loop through the change events and push intervals
|
// Loop through the change events and push intervals
|
||||||
for (change_key, change_val) in version.image_coverage.range(start..end) {
|
for (change_key, change_val) in version.image_coverage.range(start..end) {
|
||||||
let kr = Key::from_i128(current_key)..Key::from_i128(change_key);
|
let kr = Key::from_i128(current_key)..Key::from_i128(change_key);
|
||||||
coverage.push((
|
coverage.push((kr, current_val.take()));
|
||||||
kr,
|
|
||||||
current_val
|
|
||||||
.take()
|
|
||||||
.map(|l| self.get_layer_from_mapping(&l.key()).clone()),
|
|
||||||
));
|
|
||||||
current_key = change_key;
|
current_key = change_key;
|
||||||
current_val = change_val.clone();
|
current_val = change_val.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the final interval
|
// Add the final interval
|
||||||
let kr = Key::from_i128(current_key)..Key::from_i128(end);
|
let kr = Key::from_i128(current_key)..Key::from_i128(end);
|
||||||
coverage.push((
|
coverage.push((kr, current_val.take()));
|
||||||
kr,
|
|
||||||
current_val
|
|
||||||
.take()
|
|
||||||
.map(|l| self.get_layer_from_mapping(&l.key()).clone()),
|
|
||||||
));
|
|
||||||
|
|
||||||
Ok(coverage)
|
Ok(coverage)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_l0(layer: &L) -> bool {
|
pub fn is_l0(layer: &PersistentLayerDesc) -> bool {
|
||||||
range_eq(&layer.get_key_range(), &(Key::MIN..Key::MAX))
|
layer.get_key_range() == (Key::MIN..Key::MAX)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This function determines which layers are counted in `count_deltas`:
|
/// This function determines which layers are counted in `count_deltas`:
|
||||||
@@ -537,14 +389,14 @@ where
|
|||||||
/// TODO The optimal number should probably be slightly higher than 1, but to
|
/// TODO The optimal number should probably be slightly higher than 1, but to
|
||||||
/// implement that we need to plumb a lot more context into this function
|
/// implement that we need to plumb a lot more context into this function
|
||||||
/// than just the current partition_range.
|
/// than just the current partition_range.
|
||||||
pub fn is_reimage_worthy(layer: &L, partition_range: &Range<Key>) -> bool {
|
pub fn is_reimage_worthy(layer: &PersistentLayerDesc, partition_range: &Range<Key>) -> bool {
|
||||||
// Case 1
|
// Case 1
|
||||||
if !Self::is_l0(layer) {
|
if !Self::is_l0(layer) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case 2
|
// Case 2
|
||||||
if range_eq(partition_range, &(Key::MIN..Key::MAX)) {
|
if partition_range == &(Key::MIN..Key::MAX) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,9 +447,7 @@ where
|
|||||||
let kr = Key::from_i128(current_key)..Key::from_i128(change_key);
|
let kr = Key::from_i128(current_key)..Key::from_i128(change_key);
|
||||||
let lr = lsn.start..val.get_lsn_range().start;
|
let lr = lsn.start..val.get_lsn_range().start;
|
||||||
if !kr.is_empty() {
|
if !kr.is_empty() {
|
||||||
let base_count =
|
let base_count = Self::is_reimage_worthy(&val, key) as usize;
|
||||||
Self::is_reimage_worthy(self.get_layer_from_mapping(&val.key()), key)
|
|
||||||
as usize;
|
|
||||||
let new_limit = limit.map(|l| l - base_count);
|
let new_limit = limit.map(|l| l - base_count);
|
||||||
let max_stacked_deltas_underneath =
|
let max_stacked_deltas_underneath =
|
||||||
self.count_deltas(&kr, &lr, new_limit)?;
|
self.count_deltas(&kr, &lr, new_limit)?;
|
||||||
@@ -620,9 +470,7 @@ where
|
|||||||
let lr = lsn.start..val.get_lsn_range().start;
|
let lr = lsn.start..val.get_lsn_range().start;
|
||||||
|
|
||||||
if !kr.is_empty() {
|
if !kr.is_empty() {
|
||||||
let base_count =
|
let base_count = Self::is_reimage_worthy(&val, key) as usize;
|
||||||
Self::is_reimage_worthy(self.get_layer_from_mapping(&val.key()), key)
|
|
||||||
as usize;
|
|
||||||
let new_limit = limit.map(|l| l - base_count);
|
let new_limit = limit.map(|l| l - base_count);
|
||||||
let max_stacked_deltas_underneath = self.count_deltas(&kr, &lr, new_limit)?;
|
let max_stacked_deltas_underneath = self.count_deltas(&kr, &lr, new_limit)?;
|
||||||
max_stacked_deltas = std::cmp::max(
|
max_stacked_deltas = std::cmp::max(
|
||||||
@@ -772,12 +620,8 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return all L0 delta layers
|
/// Return all L0 delta layers
|
||||||
pub fn get_level0_deltas(&self) -> Result<Vec<Arc<L>>> {
|
pub fn get_level0_deltas(&self) -> Result<Vec<Arc<PersistentLayerDesc>>> {
|
||||||
Ok(self
|
Ok(self.l0_delta_layers.to_vec())
|
||||||
.l0_delta_layers
|
|
||||||
.iter()
|
|
||||||
.map(|x| self.get_layer_from_mapping(&x.key()).clone())
|
|
||||||
.collect())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// debugging function to print out the contents of the layer map
|
/// debugging function to print out the contents of the layer map
|
||||||
@@ -802,72 +646,67 @@ where
|
|||||||
println!("End dump LayerMap");
|
println!("End dump LayerMap");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Similar to `Arc::ptr_eq`, but only compares the object pointers, not vtables.
|
|
||||||
///
|
|
||||||
/// Returns `true` if the two `Arc` point to the same layer, false otherwise.
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn compare_arced_layers(left: &Arc<L>, right: &Arc<L>) -> bool {
|
|
||||||
// "dyn Trait" objects are "fat pointers" in that they have two components:
|
|
||||||
// - pointer to the object
|
|
||||||
// - pointer to the vtable
|
|
||||||
//
|
|
||||||
// rust does not provide a guarantee that these vtables are unique, but however
|
|
||||||
// `Arc::ptr_eq` as of writing (at least up to 1.67) uses a comparison where both the
|
|
||||||
// pointer and the vtable need to be equal.
|
|
||||||
//
|
|
||||||
// See: https://github.com/rust-lang/rust/issues/103763
|
|
||||||
//
|
|
||||||
// A future version of rust will most likely use this form below, where we cast each
|
|
||||||
// pointer into a pointer to unit, which drops the inaccessible vtable pointer, making it
|
|
||||||
// not affect the comparison.
|
|
||||||
//
|
|
||||||
// See: https://github.com/rust-lang/rust/pull/106450
|
|
||||||
let left = Arc::as_ptr(left) as *const ();
|
|
||||||
let right = Arc::as_ptr(right) as *const ();
|
|
||||||
|
|
||||||
left == right
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{LayerMap, Replacement};
|
use super::LayerMap;
|
||||||
use crate::tenant::storage_layer::{Layer, LayerDescriptor, LayerFileName};
|
use crate::tenant::storage_layer::LayerFileName;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
mod l0_delta_layers_updated {
|
mod l0_delta_layers_updated {
|
||||||
|
|
||||||
|
use crate::tenant::{
|
||||||
|
storage_layer::{AsLayerDesc, PersistentLayerDesc},
|
||||||
|
timeline::layer_manager::LayerFileManager,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
struct LayerObject(PersistentLayerDesc);
|
||||||
|
|
||||||
|
impl AsLayerDesc for LayerObject {
|
||||||
|
fn layer_desc(&self) -> &PersistentLayerDesc {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayerObject {
|
||||||
|
fn new(desc: PersistentLayerDesc) -> Self {
|
||||||
|
LayerObject(desc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestLayerFileManager = LayerFileManager<LayerObject>;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn for_full_range_delta() {
|
fn for_full_range_delta() {
|
||||||
// l0_delta_layers are used by compaction, and should observe all buffered updates
|
// l0_delta_layers are used by compaction, and should observe all buffered updates
|
||||||
l0_delta_layers_updated_scenario(
|
l0_delta_layers_updated_scenario(
|
||||||
"000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000053423C21-0000000053424D69",
|
"000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000053423C21-0000000053424D69",
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn for_non_full_range_delta() {
|
fn for_non_full_range_delta() {
|
||||||
// has minimal uncovered areas compared to l0_delta_layers_updated_on_insert_replace_remove_for_full_range_delta
|
// has minimal uncovered areas compared to l0_delta_layers_updated_on_insert_replace_remove_for_full_range_delta
|
||||||
l0_delta_layers_updated_scenario(
|
l0_delta_layers_updated_scenario(
|
||||||
"000000000000000000000000000000000001-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE__0000000053423C21-0000000053424D69",
|
"000000000000000000000000000000000001-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE__0000000053423C21-0000000053424D69",
|
||||||
// because not full range
|
// because not full range
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn for_image() {
|
fn for_image() {
|
||||||
l0_delta_layers_updated_scenario(
|
l0_delta_layers_updated_scenario(
|
||||||
"000000000000000000000000000000000000-000000000000000000000000000000010000__0000000053424D69",
|
"000000000000000000000000000000000000-000000000000000000000000000000010000__0000000053424D69",
|
||||||
// code only checks if it is a full range layer, doesn't care about images, which must
|
// code only checks if it is a full range layer, doesn't care about images, which must
|
||||||
// mean we should in practice never have full range images
|
// mean we should in practice never have full range images
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -877,75 +716,70 @@ mod tests {
|
|||||||
|
|
||||||
let layer = "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000053423C21-0000000053424D69";
|
let layer = "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000053423C21-0000000053424D69";
|
||||||
let layer = LayerFileName::from_str(layer).unwrap();
|
let layer = LayerFileName::from_str(layer).unwrap();
|
||||||
let layer = LayerDescriptor::from(layer);
|
let layer = PersistentLayerDesc::from(layer);
|
||||||
|
|
||||||
// same skeletan construction; see scenario below
|
// same skeletan construction; see scenario below
|
||||||
let not_found = Arc::new(layer.clone());
|
let not_found = Arc::new(LayerObject::new(layer.clone()));
|
||||||
let new_version = Arc::new(layer);
|
let new_version = Arc::new(LayerObject::new(layer));
|
||||||
|
|
||||||
let mut map = LayerMap::default();
|
// after the immutable storage state refactor, the replace operation
|
||||||
|
// will not use layer map any more. We keep it here for consistency in test cases
|
||||||
|
// and can remove it in the future.
|
||||||
|
let _map = LayerMap::default();
|
||||||
|
|
||||||
let res = map.batch_update().replace_historic(
|
let mut mapping = TestLayerFileManager::new();
|
||||||
not_found.get_persistent_layer_desc(),
|
|
||||||
¬_found,
|
|
||||||
new_version.get_persistent_layer_desc(),
|
|
||||||
new_version,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(matches!(res, Ok(Replacement::NotFound)), "{res:?}");
|
mapping
|
||||||
|
.replace_and_verify(not_found, new_version)
|
||||||
|
.unwrap_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn l0_delta_layers_updated_scenario(layer_name: &str, expected_l0: bool) {
|
fn l0_delta_layers_updated_scenario(layer_name: &str, expected_l0: bool) {
|
||||||
let name = LayerFileName::from_str(layer_name).unwrap();
|
let name = LayerFileName::from_str(layer_name).unwrap();
|
||||||
let skeleton = LayerDescriptor::from(name);
|
let skeleton = PersistentLayerDesc::from(name);
|
||||||
|
|
||||||
let remote = Arc::new(skeleton.clone());
|
let remote = Arc::new(LayerObject::new(skeleton.clone()));
|
||||||
let downloaded = Arc::new(skeleton);
|
let downloaded = Arc::new(LayerObject::new(skeleton));
|
||||||
|
|
||||||
let mut map = LayerMap::default();
|
let mut map = LayerMap::default();
|
||||||
|
let mut mapping = LayerFileManager::new();
|
||||||
|
|
||||||
// two disjoint Arcs in different lifecycle phases. even if it seems they must be the
|
// two disjoint Arcs in different lifecycle phases. even if it seems they must be the
|
||||||
// same layer, we use LayerMap::compare_arced_layers as the identity of layers.
|
// same layer, we use LayerMap::compare_arced_layers as the identity of layers.
|
||||||
assert!(!LayerMap::compare_arced_layers(&remote, &downloaded));
|
assert_eq!(remote.layer_desc(), downloaded.layer_desc());
|
||||||
|
|
||||||
let expected_in_counts = (1, usize::from(expected_l0));
|
let expected_in_counts = (1, usize::from(expected_l0));
|
||||||
|
|
||||||
map.batch_update()
|
map.batch_update()
|
||||||
.insert_historic(remote.get_persistent_layer_desc(), remote.clone());
|
.insert_historic(remote.layer_desc().clone());
|
||||||
assert_eq!(count_layer_in(&map, &remote), expected_in_counts);
|
mapping.insert(remote.clone());
|
||||||
|
assert_eq!(
|
||||||
let replaced = map
|
count_layer_in(&map, remote.layer_desc()),
|
||||||
.batch_update()
|
expected_in_counts
|
||||||
.replace_historic(
|
);
|
||||||
remote.get_persistent_layer_desc(),
|
|
||||||
&remote,
|
mapping
|
||||||
downloaded.get_persistent_layer_desc(),
|
.replace_and_verify(remote, downloaded.clone())
|
||||||
downloaded.clone(),
|
.expect("name derived attributes are the same");
|
||||||
)
|
assert_eq!(
|
||||||
.expect("name derived attributes are the same");
|
count_layer_in(&map, downloaded.layer_desc()),
|
||||||
assert!(
|
expected_in_counts
|
||||||
matches!(replaced, Replacement::Replaced { .. }),
|
|
||||||
"{replaced:?}"
|
|
||||||
);
|
);
|
||||||
assert_eq!(count_layer_in(&map, &downloaded), expected_in_counts);
|
|
||||||
|
|
||||||
map.batch_update()
|
map.batch_update()
|
||||||
.remove_historic(downloaded.get_persistent_layer_desc(), downloaded.clone());
|
.remove_historic(downloaded.layer_desc().clone());
|
||||||
assert_eq!(count_layer_in(&map, &downloaded), (0, 0));
|
assert_eq!(count_layer_in(&map, downloaded.layer_desc()), (0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn count_layer_in<L: Layer + ?Sized>(map: &LayerMap<L>, layer: &Arc<L>) -> (usize, usize) {
|
fn count_layer_in(map: &LayerMap, layer: &PersistentLayerDesc) -> (usize, usize) {
|
||||||
let historic = map
|
let historic = map
|
||||||
.iter_historic_layers()
|
.iter_historic_layers()
|
||||||
.filter(|x| LayerMap::compare_arced_layers(x, layer))
|
.filter(|x| x.key() == layer.key())
|
||||||
.count();
|
.count();
|
||||||
let l0s = map
|
let l0s = map
|
||||||
.get_level0_deltas()
|
.get_level0_deltas()
|
||||||
.expect("why does this return a result");
|
.expect("why does this return a result");
|
||||||
let l0 = l0s
|
let l0 = l0s.iter().filter(|x| x.key() == layer.key()).count();
|
||||||
.iter()
|
|
||||||
.filter(|x| LayerMap::compare_arced_layers(x, layer))
|
|
||||||
.count();
|
|
||||||
|
|
||||||
(historic, l0)
|
(historic, l0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ use std::ops::Range;
|
|||||||
|
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::tenant::storage_layer::PersistentLayerDesc;
|
||||||
|
|
||||||
use super::layer_coverage::LayerCoverageTuple;
|
use super::layer_coverage::LayerCoverageTuple;
|
||||||
|
|
||||||
/// Layers in this module are identified and indexed by this data.
|
/// Layers in this module are identified and indexed by this data.
|
||||||
@@ -41,8 +43,8 @@ impl Ord for LayerKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, L: crate::tenant::storage_layer::Layer + ?Sized> From<&'a L> for LayerKey {
|
impl From<&PersistentLayerDesc> for LayerKey {
|
||||||
fn from(layer: &'a L) -> Self {
|
fn from(layer: &PersistentLayerDesc) -> Self {
|
||||||
let kr = layer.get_key_range();
|
let kr = layer.get_key_range();
|
||||||
let lr = layer.get_lsn_range();
|
let lr = layer.get_lsn_range();
|
||||||
LayerKey {
|
LayerKey {
|
||||||
@@ -120,8 +122,7 @@ impl<Value: Clone> HistoricLayerCoverage<Value> {
|
|||||||
self.head = self
|
self.head = self
|
||||||
.historic
|
.historic
|
||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.next_back()
|
||||||
.next()
|
|
||||||
.map(|(_, v)| v.clone())
|
.map(|(_, v)| v.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
}
|
}
|
||||||
@@ -410,7 +411,7 @@ fn test_persistent_overlapping() {
|
|||||||
/// still be more critical.
|
/// still be more critical.
|
||||||
///
|
///
|
||||||
/// See this for more on persistent and retroactive techniques:
|
/// See this for more on persistent and retroactive techniques:
|
||||||
/// https://www.youtube.com/watch?v=WqCWghETNDc&t=581s
|
/// <https://www.youtube.com/watch?v=WqCWghETNDc&t=581s>
|
||||||
pub struct BufferedHistoricLayerCoverage<Value> {
|
pub struct BufferedHistoricLayerCoverage<Value> {
|
||||||
/// A persistent layer map that we rebuild when we need to retroactively update
|
/// A persistent layer map that we rebuild when we need to retroactively update
|
||||||
historic_coverage: HistoricLayerCoverage<Value>,
|
historic_coverage: HistoricLayerCoverage<Value>,
|
||||||
@@ -454,59 +455,6 @@ impl<Value: Clone> BufferedHistoricLayerCoverage<Value> {
|
|||||||
self.buffer.insert(layer_key, None);
|
self.buffer.insert(layer_key, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces a previous layer with a new layer value.
|
|
||||||
///
|
|
||||||
/// The replacement is conditional on:
|
|
||||||
/// - there is an existing `LayerKey` record
|
|
||||||
/// - there is no buffered removal for the given `LayerKey`
|
|
||||||
/// - the given closure returns true for the current `Value`
|
|
||||||
///
|
|
||||||
/// The closure is used to compare the latest value (buffered insert, or existing layer)
|
|
||||||
/// against some expectation. This allows to use `Arc::ptr_eq` or similar which would be
|
|
||||||
/// inaccessible via `PartialEq` trait.
|
|
||||||
///
|
|
||||||
/// Returns a `Replacement` value describing the outcome; only the case of
|
|
||||||
/// `Replacement::Replaced` modifies the map and requires a rebuild.
|
|
||||||
pub fn replace<F>(
|
|
||||||
&mut self,
|
|
||||||
layer_key: &LayerKey,
|
|
||||||
new: Value,
|
|
||||||
check_expected: F,
|
|
||||||
) -> Replacement<Value>
|
|
||||||
where
|
|
||||||
F: FnOnce(&Value) -> bool,
|
|
||||||
{
|
|
||||||
let (slot, in_buffered) = match self.buffer.get(layer_key) {
|
|
||||||
Some(inner @ Some(_)) => {
|
|
||||||
// we compare against the buffered version, because there will be a later
|
|
||||||
// rebuild before querying
|
|
||||||
(inner.as_ref(), true)
|
|
||||||
}
|
|
||||||
Some(None) => {
|
|
||||||
// buffer has removal for this key; it will not be equivalent by any check_expected.
|
|
||||||
return Replacement::RemovalBuffered;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// no pending modification for the key, check layers
|
|
||||||
(self.layers.get(layer_key), false)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match slot {
|
|
||||||
Some(existing) if !check_expected(existing) => {
|
|
||||||
// unfortunate clone here, but otherwise the nll borrowck grows the region of
|
|
||||||
// 'a to cover the whole function, and we could not mutate in the other
|
|
||||||
// Some(existing) branch
|
|
||||||
Replacement::Unexpected(existing.clone())
|
|
||||||
}
|
|
||||||
None => Replacement::NotFound,
|
|
||||||
Some(_existing) => {
|
|
||||||
self.insert(layer_key.to_owned(), new);
|
|
||||||
Replacement::Replaced { in_buffered }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rebuild(&mut self) {
|
pub fn rebuild(&mut self) {
|
||||||
// Find the first LSN that needs to be rebuilt
|
// Find the first LSN that needs to be rebuilt
|
||||||
let rebuild_since: u64 = match self.buffer.iter().next() {
|
let rebuild_since: u64 = match self.buffer.iter().next() {
|
||||||
@@ -575,22 +523,6 @@ impl<Value: Clone> BufferedHistoricLayerCoverage<Value> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Outcome of the replace operation.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Replacement<Value> {
|
|
||||||
/// Previous value was replaced with the new value.
|
|
||||||
Replaced {
|
|
||||||
/// Replacement happened for a scheduled insert.
|
|
||||||
in_buffered: bool,
|
|
||||||
},
|
|
||||||
/// Key was not found buffered updates or existing layers.
|
|
||||||
NotFound,
|
|
||||||
/// Key has been scheduled for removal, it was not replaced.
|
|
||||||
RemovalBuffered,
|
|
||||||
/// Previous value was rejected by the closure.
|
|
||||||
Unexpected(Value),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_retroactive_regression_1() {
|
fn test_retroactive_regression_1() {
|
||||||
let mut map = BufferedHistoricLayerCoverage::new();
|
let mut map = BufferedHistoricLayerCoverage::new();
|
||||||
@@ -699,139 +631,3 @@ fn test_retroactive_simple() {
|
|||||||
assert_eq!(version.image_coverage.query(8), Some("Image 4".to_string()));
|
assert_eq!(version.image_coverage.query(8), Some("Image 4".to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_retroactive_replacement() {
|
|
||||||
let mut map = BufferedHistoricLayerCoverage::new();
|
|
||||||
|
|
||||||
let keys = [
|
|
||||||
LayerKey {
|
|
||||||
key: 0..5,
|
|
||||||
lsn: 100..101,
|
|
||||||
is_image: true,
|
|
||||||
},
|
|
||||||
LayerKey {
|
|
||||||
key: 3..9,
|
|
||||||
lsn: 110..111,
|
|
||||||
is_image: true,
|
|
||||||
},
|
|
||||||
LayerKey {
|
|
||||||
key: 4..6,
|
|
||||||
lsn: 120..121,
|
|
||||||
is_image: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let layers = [
|
|
||||||
"Image 1".to_string(),
|
|
||||||
"Image 2".to_string(),
|
|
||||||
"Image 3".to_string(),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (key, layer) in keys.iter().zip(layers.iter()) {
|
|
||||||
map.insert(key.to_owned(), layer.to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
// rebuild is not necessary here, because replace works for both buffered updates and existing
|
|
||||||
// layers.
|
|
||||||
|
|
||||||
for (key, orig_layer) in keys.iter().zip(layers.iter()) {
|
|
||||||
let replacement = format!("Remote {orig_layer}");
|
|
||||||
|
|
||||||
// evict
|
|
||||||
let ret = map.replace(key, replacement.clone(), |l| l == orig_layer);
|
|
||||||
assert!(
|
|
||||||
matches!(ret, Replacement::Replaced { .. }),
|
|
||||||
"replace {orig_layer}: {ret:?}"
|
|
||||||
);
|
|
||||||
map.rebuild();
|
|
||||||
|
|
||||||
let at = key.lsn.end + 1;
|
|
||||||
|
|
||||||
let version = map.get().expect("rebuilt").get_version(at).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
version.image_coverage.query(4).as_deref(),
|
|
||||||
Some(replacement.as_str()),
|
|
||||||
"query for 4 at version {at} after eviction",
|
|
||||||
);
|
|
||||||
|
|
||||||
// download
|
|
||||||
let ret = map.replace(key, orig_layer.clone(), |l| l == &replacement);
|
|
||||||
assert!(
|
|
||||||
matches!(ret, Replacement::Replaced { .. }),
|
|
||||||
"replace {orig_layer} back: {ret:?}"
|
|
||||||
);
|
|
||||||
map.rebuild();
|
|
||||||
let version = map.get().expect("rebuilt").get_version(at).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
version.image_coverage.query(4).as_deref(),
|
|
||||||
Some(orig_layer.as_str()),
|
|
||||||
"query for 4 at version {at} after download",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn missing_key_is_not_inserted_with_replace() {
|
|
||||||
let mut map = BufferedHistoricLayerCoverage::new();
|
|
||||||
let key = LayerKey {
|
|
||||||
key: 0..5,
|
|
||||||
lsn: 100..101,
|
|
||||||
is_image: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
let ret = map.replace(&key, "should not replace", |_| true);
|
|
||||||
assert!(matches!(ret, Replacement::NotFound), "{ret:?}");
|
|
||||||
map.rebuild();
|
|
||||||
assert!(map
|
|
||||||
.get()
|
|
||||||
.expect("no changes to rebuild")
|
|
||||||
.get_version(102)
|
|
||||||
.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn replacing_buffered_insert_and_remove() {
|
|
||||||
let mut map = BufferedHistoricLayerCoverage::new();
|
|
||||||
let key = LayerKey {
|
|
||||||
key: 0..5,
|
|
||||||
lsn: 100..101,
|
|
||||||
is_image: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
map.insert(key.clone(), "Image 1");
|
|
||||||
let ret = map.replace(&key, "Remote Image 1", |&l| l == "Image 1");
|
|
||||||
assert!(
|
|
||||||
matches!(ret, Replacement::Replaced { in_buffered: true }),
|
|
||||||
"{ret:?}"
|
|
||||||
);
|
|
||||||
map.rebuild();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
map.get()
|
|
||||||
.expect("rebuilt")
|
|
||||||
.get_version(102)
|
|
||||||
.unwrap()
|
|
||||||
.image_coverage
|
|
||||||
.query(4),
|
|
||||||
Some("Remote Image 1")
|
|
||||||
);
|
|
||||||
|
|
||||||
map.remove(key.clone());
|
|
||||||
let ret = map.replace(&key, "should not replace", |_| true);
|
|
||||||
assert!(
|
|
||||||
matches!(ret, Replacement::RemovalBuffered),
|
|
||||||
"cannot replace after scheduled remove: {ret:?}"
|
|
||||||
);
|
|
||||||
|
|
||||||
map.rebuild();
|
|
||||||
|
|
||||||
let ret = map.replace(&key, "should not replace", |_| true);
|
|
||||||
assert!(
|
|
||||||
matches!(ret, Replacement::NotFound),
|
|
||||||
"cannot replace after remove + rebuild: {ret:?}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let at_version = map.get().expect("rebuilt").get_version(102);
|
|
||||||
assert!(at_version.is_none());
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::ops::Range;
|
|||||||
|
|
||||||
// NOTE the `im` crate has 20x more downloads and also has
|
// NOTE the `im` crate has 20x more downloads and also has
|
||||||
// persistent/immutable BTree. But it's bugged so rpds is a
|
// persistent/immutable BTree. But it's bugged so rpds is a
|
||||||
// better choice https://github.com/neondatabase/neon/issues/3395
|
// better choice <https://github.com/neondatabase/neon/issues/3395>
|
||||||
use rpds::RedBlackTreeMapSync;
|
use rpds::RedBlackTreeMapSync;
|
||||||
|
|
||||||
/// Data structure that can efficiently:
|
/// Data structure that can efficiently:
|
||||||
@@ -11,7 +11,7 @@ use rpds::RedBlackTreeMapSync;
|
|||||||
/// - insert layers in non-decreasing lsn.start order
|
/// - insert layers in non-decreasing lsn.start order
|
||||||
///
|
///
|
||||||
/// For a detailed explanation and justification of this approach, see:
|
/// For a detailed explanation and justification of this approach, see:
|
||||||
/// https://neon.tech/blog/persistent-structures-in-neons-wal-indexing
|
/// <https://neon.tech/blog/persistent-structures-in-neons-wal-indexing>
|
||||||
///
|
///
|
||||||
/// NOTE The struct is parameterized over Value for easier
|
/// NOTE The struct is parameterized over Value for easier
|
||||||
/// testing, but in practice it's some sort of layer.
|
/// testing, but in practice it's some sort of layer.
|
||||||
@@ -113,8 +113,7 @@ impl<Value: Clone> LayerCoverage<Value> {
|
|||||||
pub fn query(&self, key: i128) -> Option<Value> {
|
pub fn query(&self, key: i128) -> Option<Value> {
|
||||||
self.nodes
|
self.nodes
|
||||||
.range(..=key)
|
.range(..=key)
|
||||||
.rev()
|
.next_back()?
|
||||||
.next()?
|
|
||||||
.1
|
.1
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|(_, v)| v.clone())
|
.map(|(_, v)| v.clone())
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
//! Currently, this is not used in the system. Future refactors will ensure
|
//! Currently, this is not used in the system. Future refactors will ensure
|
||||||
//! the storage state will be recorded in this file, and the system can be
|
//! the storage state will be recorded in this file, and the system can be
|
||||||
//! recovered from this file. This is tracked in
|
//! recovered from this file. This is tracked in
|
||||||
//! https://github.com/neondatabase/neon/issues/4418
|
//! <https://github.com/neondatabase/neon/issues/4418>
|
||||||
|
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
//! Every image of a certain timeline from [`crate::tenant::Tenant`]
|
//! Every image of a certain timeline from [`crate::tenant::Tenant`]
|
||||||
//! has a metadata that needs to be stored persistently.
|
//! has a metadata that needs to be stored persistently.
|
||||||
//!
|
//!
|
||||||
//! Later, the file gets is used in [`crate::remote_storage::storage_sync`] as a part of
|
//! Later, the file gets used in [`remote_timeline_client`] as a part of
|
||||||
//! external storage import and export operations.
|
//! external storage import and export operations.
|
||||||
//!
|
//!
|
||||||
//! The module contains all structs and related helper methods related to timeline metadata.
|
//! The module contains all structs and related helper methods related to timeline metadata.
|
||||||
|
//!
|
||||||
|
//! [`remote_timeline_client`]: super::remote_timeline_client
|
||||||
|
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@@ -232,13 +234,13 @@ impl TimelineMetadata {
|
|||||||
/// Save timeline metadata to file
|
/// Save timeline metadata to file
|
||||||
pub fn save_metadata(
|
pub fn save_metadata(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
timeline_id: TimelineId,
|
tenant_id: &TenantId,
|
||||||
tenant_id: TenantId,
|
timeline_id: &TimelineId,
|
||||||
data: &TimelineMetadata,
|
data: &TimelineMetadata,
|
||||||
first_save: bool,
|
first_save: bool,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let _enter = info_span!("saving metadata").entered();
|
let _enter = info_span!("saving metadata").entered();
|
||||||
let path = conf.metadata_path(timeline_id, tenant_id);
|
let path = conf.metadata_path(tenant_id, timeline_id);
|
||||||
// use OpenOptions to ensure file presence is consistent with first_save
|
// use OpenOptions to ensure file presence is consistent with first_save
|
||||||
let mut file = VirtualFile::open_with_options(
|
let mut file = VirtualFile::open_with_options(
|
||||||
&path,
|
&path,
|
||||||
@@ -267,10 +269,10 @@ pub fn save_metadata(
|
|||||||
|
|
||||||
pub fn load_metadata(
|
pub fn load_metadata(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
timeline_id: TimelineId,
|
tenant_id: &TenantId,
|
||||||
tenant_id: TenantId,
|
timeline_id: &TimelineId,
|
||||||
) -> anyhow::Result<TimelineMetadata> {
|
) -> anyhow::Result<TimelineMetadata> {
|
||||||
let metadata_path = conf.metadata_path(timeline_id, tenant_id);
|
let metadata_path = conf.metadata_path(tenant_id, timeline_id);
|
||||||
let metadata_bytes = std::fs::read(&metadata_path).with_context(|| {
|
let metadata_bytes = std::fs::read(&metadata_path).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to read metadata bytes from path {}",
|
"Failed to read metadata bytes from path {}",
|
||||||
|
|||||||
@@ -184,9 +184,9 @@ pub fn schedule_local_tenant_processing(
|
|||||||
format!("Could not parse tenant id out of the tenant dir name in path {tenant_path:?}")
|
format!("Could not parse tenant id out of the tenant dir name in path {tenant_path:?}")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let tenant_ignore_mark = conf.tenant_ignore_mark_file_path(tenant_id);
|
let tenant_ignore_mark = conf.tenant_ignore_mark_file_path(&tenant_id);
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
!conf.tenant_ignore_mark_file_path(tenant_id).exists(),
|
!conf.tenant_ignore_mark_file_path(&tenant_id).exists(),
|
||||||
"Cannot load tenant, ignore mark found at {tenant_ignore_mark:?}"
|
"Cannot load tenant, ignore mark found at {tenant_ignore_mark:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -233,11 +233,17 @@ pub fn schedule_local_tenant_processing(
|
|||||||
/// That could be easily misinterpreted by control plane, the consumer of the
|
/// That could be easily misinterpreted by control plane, the consumer of the
|
||||||
/// management API. For example, it could attach the tenant on a different pageserver.
|
/// management API. For example, it could attach the tenant on a different pageserver.
|
||||||
/// We would then be in split-brain once this pageserver restarts.
|
/// We would then be in split-brain once this pageserver restarts.
|
||||||
#[instrument]
|
#[instrument(skip_all)]
|
||||||
pub async fn shutdown_all_tenants() {
|
pub async fn shutdown_all_tenants() {
|
||||||
|
shutdown_all_tenants0(&TENANTS).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn shutdown_all_tenants0(tenants: &tokio::sync::RwLock<TenantsMap>) {
|
||||||
|
use utils::completion;
|
||||||
|
|
||||||
// Prevent new tenants from being created.
|
// Prevent new tenants from being created.
|
||||||
let tenants_to_shut_down = {
|
let tenants_to_shut_down = {
|
||||||
let mut m = TENANTS.write().await;
|
let mut m = tenants.write().await;
|
||||||
match &mut *m {
|
match &mut *m {
|
||||||
TenantsMap::Initializing => {
|
TenantsMap::Initializing => {
|
||||||
*m = TenantsMap::ShuttingDown(HashMap::default());
|
*m = TenantsMap::ShuttingDown(HashMap::default());
|
||||||
@@ -262,14 +268,41 @@ pub async fn shutdown_all_tenants() {
|
|||||||
for (tenant_id, tenant) in tenants_to_shut_down {
|
for (tenant_id, tenant) in tenants_to_shut_down {
|
||||||
join_set.spawn(
|
join_set.spawn(
|
||||||
async move {
|
async move {
|
||||||
let freeze_and_flush = true;
|
// ordering shouldn't matter for this, either we store true right away or never
|
||||||
|
let ordering = std::sync::atomic::Ordering::Relaxed;
|
||||||
|
let joined_other = std::sync::atomic::AtomicBool::new(false);
|
||||||
|
|
||||||
match tenant.shutdown(freeze_and_flush).await {
|
let mut shutdown = std::pin::pin!(async {
|
||||||
Ok(()) => debug!("tenant successfully stopped"),
|
let freeze_and_flush = true;
|
||||||
Err(super::ShutdownError::AlreadyStopping) => {
|
|
||||||
warn!("tenant was already shutting down")
|
let res = {
|
||||||
|
let (_guard, shutdown_progress) = completion::channel();
|
||||||
|
tenant.shutdown(shutdown_progress, freeze_and_flush).await
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(other_progress) = res {
|
||||||
|
// join the another shutdown in progress
|
||||||
|
joined_other.store(true, ordering);
|
||||||
|
other_progress.wait().await;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
// in practice we might not have a lot time to go, since systemd is going to
|
||||||
|
// SIGKILL us at 10s, but we can try. delete tenant might take a while, so put out
|
||||||
|
// a warning.
|
||||||
|
let warning = std::time::Duration::from_secs(5);
|
||||||
|
let mut warning = std::pin::pin!(tokio::time::sleep(warning));
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = &mut shutdown => {},
|
||||||
|
_ = &mut warning => {
|
||||||
|
let joined_other = joined_other.load(ordering);
|
||||||
|
warn!(%joined_other, "waiting for the shutdown to complete");
|
||||||
|
shutdown.await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("tenant successfully stopped");
|
||||||
}
|
}
|
||||||
.instrument(info_span!("shutdown", %tenant_id)),
|
.instrument(info_span!("shutdown", %tenant_id)),
|
||||||
);
|
);
|
||||||
@@ -310,7 +343,7 @@ pub async fn create_tenant(
|
|||||||
// We're holding the tenants lock in write mode while doing local IO.
|
// We're holding the tenants lock in write mode while doing local IO.
|
||||||
// If this section ever becomes contentious, introduce a new `TenantState::Creating`
|
// If this section ever becomes contentious, introduce a new `TenantState::Creating`
|
||||||
// and do the work in that state.
|
// and do the work in that state.
|
||||||
let tenant_directory = super::create_tenant_files(conf, tenant_conf, tenant_id, CreateTenantFilesMode::Create)?;
|
let tenant_directory = super::create_tenant_files(conf, tenant_conf, &tenant_id, CreateTenantFilesMode::Create)?;
|
||||||
// TODO: tenant directory remains on disk if we bail out from here on.
|
// TODO: tenant directory remains on disk if we bail out from here on.
|
||||||
// See https://github.com/neondatabase/neon/issues/4233
|
// See https://github.com/neondatabase/neon/issues/4233
|
||||||
|
|
||||||
@@ -344,14 +377,9 @@ pub async fn set_new_tenant_config(
|
|||||||
info!("configuring tenant {tenant_id}");
|
info!("configuring tenant {tenant_id}");
|
||||||
let tenant = get_tenant(tenant_id, true).await?;
|
let tenant = get_tenant(tenant_id, true).await?;
|
||||||
|
|
||||||
let tenant_config_path = conf.tenant_config_path(tenant_id);
|
let tenant_config_path = conf.tenant_config_path(&tenant_id);
|
||||||
Tenant::persist_tenant_config(
|
Tenant::persist_tenant_config(&tenant_id, &tenant_config_path, new_tenant_conf, false)
|
||||||
&tenant.tenant_id(),
|
.map_err(SetNewTenantConfigError::Persist)?;
|
||||||
&tenant_config_path,
|
|
||||||
new_tenant_conf,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.map_err(SetNewTenantConfigError::Persist)?;
|
|
||||||
tenant.set_new_tenant_config(new_tenant_conf);
|
tenant.set_new_tenant_config(new_tenant_conf);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -418,6 +446,15 @@ pub async fn detach_tenant(
|
|||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
detach_ignored: bool,
|
detach_ignored: bool,
|
||||||
|
) -> Result<(), TenantStateError> {
|
||||||
|
detach_tenant0(conf, &TENANTS, tenant_id, detach_ignored).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn detach_tenant0(
|
||||||
|
conf: &'static PageServerConf,
|
||||||
|
tenants: &tokio::sync::RwLock<TenantsMap>,
|
||||||
|
tenant_id: TenantId,
|
||||||
|
detach_ignored: bool,
|
||||||
) -> Result<(), TenantStateError> {
|
) -> Result<(), TenantStateError> {
|
||||||
let local_files_cleanup_operation = |tenant_id_to_clean| async move {
|
let local_files_cleanup_operation = |tenant_id_to_clean| async move {
|
||||||
let local_tenant_directory = conf.tenant_path(&tenant_id_to_clean);
|
let local_tenant_directory = conf.tenant_path(&tenant_id_to_clean);
|
||||||
@@ -430,12 +467,13 @@ pub async fn detach_tenant(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let removal_result =
|
let removal_result =
|
||||||
remove_tenant_from_memory(tenant_id, local_files_cleanup_operation(tenant_id)).await;
|
remove_tenant_from_memory(tenants, tenant_id, local_files_cleanup_operation(tenant_id))
|
||||||
|
.await;
|
||||||
|
|
||||||
// Ignored tenants are not present in memory and will bail the removal from memory operation.
|
// Ignored tenants are not present in memory and will bail the removal from memory operation.
|
||||||
// Before returning the error, check for ignored tenant removal case — we only need to clean its local files then.
|
// Before returning the error, check for ignored tenant removal case — we only need to clean its local files then.
|
||||||
if detach_ignored && matches!(removal_result, Err(TenantStateError::NotFound(_))) {
|
if detach_ignored && matches!(removal_result, Err(TenantStateError::NotFound(_))) {
|
||||||
let tenant_ignore_mark = conf.tenant_ignore_mark_file_path(tenant_id);
|
let tenant_ignore_mark = conf.tenant_ignore_mark_file_path(&tenant_id);
|
||||||
if tenant_ignore_mark.exists() {
|
if tenant_ignore_mark.exists() {
|
||||||
info!("Detaching an ignored tenant");
|
info!("Detaching an ignored tenant");
|
||||||
local_files_cleanup_operation(tenant_id)
|
local_files_cleanup_operation(tenant_id)
|
||||||
@@ -457,7 +495,7 @@ pub async fn load_tenant(
|
|||||||
) -> Result<(), TenantMapInsertError> {
|
) -> Result<(), TenantMapInsertError> {
|
||||||
tenant_map_insert(tenant_id, || {
|
tenant_map_insert(tenant_id, || {
|
||||||
let tenant_path = conf.tenant_path(&tenant_id);
|
let tenant_path = conf.tenant_path(&tenant_id);
|
||||||
let tenant_ignore_mark = conf.tenant_ignore_mark_file_path(tenant_id);
|
let tenant_ignore_mark = conf.tenant_ignore_mark_file_path(&tenant_id);
|
||||||
if tenant_ignore_mark.exists() {
|
if tenant_ignore_mark.exists() {
|
||||||
std::fs::remove_file(&tenant_ignore_mark)
|
std::fs::remove_file(&tenant_ignore_mark)
|
||||||
.with_context(|| format!("Failed to remove tenant ignore mark {tenant_ignore_mark:?} during tenant loading"))?;
|
.with_context(|| format!("Failed to remove tenant ignore mark {tenant_ignore_mark:?} during tenant loading"))?;
|
||||||
@@ -477,8 +515,16 @@ pub async fn ignore_tenant(
|
|||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
) -> Result<(), TenantStateError> {
|
) -> Result<(), TenantStateError> {
|
||||||
remove_tenant_from_memory(tenant_id, async {
|
ignore_tenant0(conf, &TENANTS, tenant_id).await
|
||||||
let ignore_mark_file = conf.tenant_ignore_mark_file_path(tenant_id);
|
}
|
||||||
|
|
||||||
|
async fn ignore_tenant0(
|
||||||
|
conf: &'static PageServerConf,
|
||||||
|
tenants: &tokio::sync::RwLock<TenantsMap>,
|
||||||
|
tenant_id: TenantId,
|
||||||
|
) -> Result<(), TenantStateError> {
|
||||||
|
remove_tenant_from_memory(tenants, tenant_id, async {
|
||||||
|
let ignore_mark_file = conf.tenant_ignore_mark_file_path(&tenant_id);
|
||||||
fs::File::create(&ignore_mark_file)
|
fs::File::create(&ignore_mark_file)
|
||||||
.await
|
.await
|
||||||
.context("Failed to create ignore mark file")
|
.context("Failed to create ignore mark file")
|
||||||
@@ -525,7 +571,7 @@ pub async fn attach_tenant(
|
|||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<(), TenantMapInsertError> {
|
) -> Result<(), TenantMapInsertError> {
|
||||||
tenant_map_insert(tenant_id, || {
|
tenant_map_insert(tenant_id, || {
|
||||||
let tenant_dir = create_tenant_files(conf, tenant_conf, tenant_id, CreateTenantFilesMode::Attach)?;
|
let tenant_dir = create_tenant_files(conf, tenant_conf, &tenant_id, CreateTenantFilesMode::Attach)?;
|
||||||
// TODO: tenant directory remains on disk if we bail out from here on.
|
// TODO: tenant directory remains on disk if we bail out from here on.
|
||||||
// See https://github.com/neondatabase/neon/issues/4233
|
// See https://github.com/neondatabase/neon/issues/4233
|
||||||
|
|
||||||
@@ -602,18 +648,21 @@ where
|
|||||||
/// If the cleanup fails, tenant will stay in memory in [`TenantState::Broken`] state, and another removal
|
/// If the cleanup fails, tenant will stay in memory in [`TenantState::Broken`] state, and another removal
|
||||||
/// operation would be needed to remove it.
|
/// operation would be needed to remove it.
|
||||||
async fn remove_tenant_from_memory<V, F>(
|
async fn remove_tenant_from_memory<V, F>(
|
||||||
|
tenants: &tokio::sync::RwLock<TenantsMap>,
|
||||||
tenant_id: TenantId,
|
tenant_id: TenantId,
|
||||||
tenant_cleanup: F,
|
tenant_cleanup: F,
|
||||||
) -> Result<V, TenantStateError>
|
) -> Result<V, TenantStateError>
|
||||||
where
|
where
|
||||||
F: std::future::Future<Output = anyhow::Result<V>>,
|
F: std::future::Future<Output = anyhow::Result<V>>,
|
||||||
{
|
{
|
||||||
|
use utils::completion;
|
||||||
|
|
||||||
// It's important to keep the tenant in memory after the final cleanup, to avoid cleanup races.
|
// It's important to keep the tenant in memory after the final cleanup, to avoid cleanup races.
|
||||||
// The exclusive lock here ensures we don't miss the tenant state updates before trying another removal.
|
// The exclusive lock here ensures we don't miss the tenant state updates before trying another removal.
|
||||||
// tenant-wde cleanup operations may take some time (removing the entire tenant directory), we want to
|
// tenant-wde cleanup operations may take some time (removing the entire tenant directory), we want to
|
||||||
// avoid holding the lock for the entire process.
|
// avoid holding the lock for the entire process.
|
||||||
let tenant = {
|
let tenant = {
|
||||||
TENANTS
|
tenants
|
||||||
.write()
|
.write()
|
||||||
.await
|
.await
|
||||||
.get(&tenant_id)
|
.get(&tenant_id)
|
||||||
@@ -621,14 +670,20 @@ where
|
|||||||
.ok_or(TenantStateError::NotFound(tenant_id))?
|
.ok_or(TenantStateError::NotFound(tenant_id))?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// allow pageserver shutdown to await for our completion
|
||||||
|
let (_guard, progress) = completion::channel();
|
||||||
|
|
||||||
|
// whenever we remove a tenant from memory, we don't want to flush and wait for upload
|
||||||
let freeze_and_flush = false;
|
let freeze_and_flush = false;
|
||||||
|
|
||||||
// shutdown is sure to transition tenant to stopping, and wait for all tasks to complete, so
|
// shutdown is sure to transition tenant to stopping, and wait for all tasks to complete, so
|
||||||
// that we can continue safely to cleanup.
|
// that we can continue safely to cleanup.
|
||||||
match tenant.shutdown(freeze_and_flush).await {
|
match tenant.shutdown(progress, freeze_and_flush).await {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(super::ShutdownError::AlreadyStopping) => {
|
Err(_other) => {
|
||||||
return Err(TenantStateError::IsStopping(tenant_id))
|
// if pageserver shutdown or other detach/ignore is already ongoing, we don't want to
|
||||||
|
// wait for it but return an error right away because these are distinct requests.
|
||||||
|
return Err(TenantStateError::IsStopping(tenant_id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,14 +692,14 @@ where
|
|||||||
.with_context(|| format!("Failed to run cleanup for tenant {tenant_id}"))
|
.with_context(|| format!("Failed to run cleanup for tenant {tenant_id}"))
|
||||||
{
|
{
|
||||||
Ok(hook_value) => {
|
Ok(hook_value) => {
|
||||||
let mut tenants_accessor = TENANTS.write().await;
|
let mut tenants_accessor = tenants.write().await;
|
||||||
if tenants_accessor.remove(&tenant_id).is_none() {
|
if tenants_accessor.remove(&tenant_id).is_none() {
|
||||||
warn!("Tenant {tenant_id} got removed from memory before operation finished");
|
warn!("Tenant {tenant_id} got removed from memory before operation finished");
|
||||||
}
|
}
|
||||||
Ok(hook_value)
|
Ok(hook_value)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let tenants_accessor = TENANTS.read().await;
|
let tenants_accessor = tenants.read().await;
|
||||||
match tenants_accessor.get(&tenant_id) {
|
match tenants_accessor.get(&tenant_id) {
|
||||||
Some(tenant) => {
|
Some(tenant) => {
|
||||||
tenant.set_broken(e.to_string()).await;
|
tenant.set_broken(e.to_string()).await;
|
||||||
@@ -695,7 +750,7 @@ pub async fn immediate_gc(
|
|||||||
fail::fail_point!("immediate_gc_task_pre");
|
fail::fail_point!("immediate_gc_task_pre");
|
||||||
let result = tenant
|
let result = tenant
|
||||||
.gc_iteration(Some(timeline_id), gc_horizon, pitr, &ctx)
|
.gc_iteration(Some(timeline_id), gc_horizon, pitr, &ctx)
|
||||||
.instrument(info_span!("manual_gc", tenant = %tenant_id, timeline = %timeline_id))
|
.instrument(info_span!("manual_gc", %tenant_id, %timeline_id))
|
||||||
.await;
|
.await;
|
||||||
// FIXME: `gc_iteration` can return an error for multiple reasons; we should handle it
|
// FIXME: `gc_iteration` can return an error for multiple reasons; we should handle it
|
||||||
// better once the types support it.
|
// better once the types support it.
|
||||||
@@ -745,9 +800,7 @@ pub async fn immediate_compact(
|
|||||||
async move {
|
async move {
|
||||||
let result = timeline
|
let result = timeline
|
||||||
.compact(&ctx)
|
.compact(&ctx)
|
||||||
.instrument(
|
.instrument(info_span!("manual_compact", %tenant_id, %timeline_id))
|
||||||
info_span!("manual_compact", tenant = %tenant_id, timeline = %timeline_id),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match task_done.send(result) {
|
match task_done.send(result) {
|
||||||
@@ -763,3 +816,109 @@ pub async fn immediate_compact(
|
|||||||
|
|
||||||
Ok(wait_task_done)
|
Ok(wait_task_done)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tracing::{info_span, Instrument};
|
||||||
|
|
||||||
|
use super::{super::harness::TenantHarness, TenantsMap};
|
||||||
|
|
||||||
|
#[tokio::test(start_paused = true)]
|
||||||
|
async fn shutdown_joins_remove_tenant_from_memory() {
|
||||||
|
// the test is a bit ugly with the lockstep together with spawned tasks. the aim is to make
|
||||||
|
// sure `shutdown_all_tenants0` per-tenant processing joins in any active
|
||||||
|
// remove_tenant_from_memory calls, which is enforced by making the operation last until
|
||||||
|
// we've ran `shutdown_all_tenants0` for a long time.
|
||||||
|
|
||||||
|
let (t, _ctx) = TenantHarness::create("shutdown_joins_detach")
|
||||||
|
.unwrap()
|
||||||
|
.load()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// harness loads it to active, which is forced and nothing is running on the tenant
|
||||||
|
|
||||||
|
let id = t.tenant_id();
|
||||||
|
|
||||||
|
// tenant harness configures the logging and we cannot escape it
|
||||||
|
let _e = info_span!("testing", tenant_id = %id).entered();
|
||||||
|
|
||||||
|
let tenants = HashMap::from([(id, t.clone())]);
|
||||||
|
let tenants = Arc::new(tokio::sync::RwLock::new(TenantsMap::Open(tenants)));
|
||||||
|
|
||||||
|
let (until_cleanup_completed, can_complete_cleanup) = utils::completion::channel();
|
||||||
|
let (until_cleanup_started, cleanup_started) = utils::completion::channel();
|
||||||
|
|
||||||
|
// start a "detaching operation", which will take a while, until can_complete_cleanup
|
||||||
|
let cleanup_task = {
|
||||||
|
let jh = tokio::spawn({
|
||||||
|
let tenants = tenants.clone();
|
||||||
|
async move {
|
||||||
|
let cleanup = async move {
|
||||||
|
drop(until_cleanup_started);
|
||||||
|
can_complete_cleanup.wait().await;
|
||||||
|
anyhow::Ok(())
|
||||||
|
};
|
||||||
|
super::remove_tenant_from_memory(&tenants, id, cleanup).await
|
||||||
|
}
|
||||||
|
.instrument(info_span!("foobar", tenant_id = %id))
|
||||||
|
});
|
||||||
|
|
||||||
|
// now the long cleanup should be in place, with the stopping state
|
||||||
|
cleanup_started.wait().await;
|
||||||
|
jh
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cleanup_progress = std::pin::pin!(t
|
||||||
|
.shutdown(utils::completion::Barrier::default(), false)
|
||||||
|
.await
|
||||||
|
.unwrap_err()
|
||||||
|
.wait());
|
||||||
|
|
||||||
|
let mut shutdown_task = {
|
||||||
|
let (until_shutdown_started, shutdown_started) = utils::completion::channel();
|
||||||
|
|
||||||
|
let shutdown_task = tokio::spawn(async move {
|
||||||
|
drop(until_shutdown_started);
|
||||||
|
super::shutdown_all_tenants0(&tenants).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
shutdown_started.wait().await;
|
||||||
|
shutdown_task
|
||||||
|
};
|
||||||
|
|
||||||
|
// if the joining in is removed from shutdown_all_tenants0, the shutdown_task should always
|
||||||
|
// get to complete within timeout and fail the test. it is expected to continue awaiting
|
||||||
|
// until completion or SIGKILL during normal shutdown.
|
||||||
|
//
|
||||||
|
// the timeout is long to cover anything that shutdown_task could be doing, but it is
|
||||||
|
// handled instantly because we use tokio's time pausing in this test. 100s is much more than
|
||||||
|
// what we get from systemd on shutdown (10s).
|
||||||
|
let long_time = std::time::Duration::from_secs(100);
|
||||||
|
tokio::select! {
|
||||||
|
_ = &mut shutdown_task => unreachable!("shutdown must continue, until_cleanup_completed is not dropped"),
|
||||||
|
_ = &mut cleanup_progress => unreachable!("cleanup progress must continue, until_cleanup_completed is not dropped"),
|
||||||
|
_ = tokio::time::sleep(long_time) => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow the remove_tenant_from_memory and thus eventually the shutdown to continue
|
||||||
|
drop(until_cleanup_completed);
|
||||||
|
|
||||||
|
let (je, ()) = tokio::join!(shutdown_task, cleanup_progress);
|
||||||
|
je.expect("Tenant::shutdown shutdown not have panicked");
|
||||||
|
cleanup_task
|
||||||
|
.await
|
||||||
|
.expect("no panicking")
|
||||||
|
.expect("remove_tenant_from_memory failed");
|
||||||
|
|
||||||
|
futures::future::poll_immediate(
|
||||||
|
t.shutdown(utils::completion::Barrier::default(), false)
|
||||||
|
.await
|
||||||
|
.unwrap_err()
|
||||||
|
.wait(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("the stopping progress must still be complete");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@
|
|||||||
//! - Initiate upload queue with that [`IndexPart`].
|
//! - Initiate upload queue with that [`IndexPart`].
|
||||||
//! - Reschedule all lost operations by comparing the local filesystem state
|
//! - Reschedule all lost operations by comparing the local filesystem state
|
||||||
//! and remote state as per [`IndexPart`]. This is done in
|
//! and remote state as per [`IndexPart`]. This is done in
|
||||||
//! [`Timeline::timeline_init_and_sync`] and [`Timeline::reconcile_with_remote`].
|
//! [`Tenant::timeline_init_and_sync`] and [`Timeline::reconcile_with_remote`].
|
||||||
//!
|
//!
|
||||||
//! Note that if we crash during file deletion between the index update
|
//! Note that if we crash during file deletion between the index update
|
||||||
//! that removes the file from the list of files, and deleting the remote file,
|
//! that removes the file from the list of files, and deleting the remote file,
|
||||||
@@ -163,8 +163,8 @@
|
|||||||
//! - download their remote [`IndexPart`]s
|
//! - download their remote [`IndexPart`]s
|
||||||
//! - create `Timeline` struct and a `RemoteTimelineClient`
|
//! - create `Timeline` struct and a `RemoteTimelineClient`
|
||||||
//! - initialize the client's upload queue with its `IndexPart`
|
//! - initialize the client's upload queue with its `IndexPart`
|
||||||
//! - create [`RemoteLayer`] instances for layers that are referenced by `IndexPart`
|
//! - create [`RemoteLayer`](super::storage_layer::RemoteLayer) instances
|
||||||
//! but not present locally
|
//! for layers that are referenced by `IndexPart` but not present locally
|
||||||
//! - schedule uploads for layers that are only present locally.
|
//! - schedule uploads for layers that are only present locally.
|
||||||
//! - if the remote `IndexPart`'s metadata was newer than the metadata in
|
//! - if the remote `IndexPart`'s metadata was newer than the metadata in
|
||||||
//! the local filesystem, write the remote metadata to the local filesystem
|
//! the local filesystem, write the remote metadata to the local filesystem
|
||||||
@@ -198,6 +198,8 @@
|
|||||||
//! in remote storage.
|
//! in remote storage.
|
||||||
//! But note that we don't test any of this right now.
|
//! But note that we don't test any of this right now.
|
||||||
//!
|
//!
|
||||||
|
//! [`Tenant::timeline_init_and_sync`]: super::Tenant::timeline_init_and_sync
|
||||||
|
//! [`Timeline::reconcile_with_remote`]: super::Timeline::reconcile_with_remote
|
||||||
|
|
||||||
mod delete;
|
mod delete;
|
||||||
mod download;
|
mod download;
|
||||||
@@ -442,8 +444,8 @@ impl RemoteTimelineClient {
|
|||||||
let index_part = download::download_index_part(
|
let index_part = download::download_index_part(
|
||||||
self.conf,
|
self.conf,
|
||||||
&self.storage_impl,
|
&self.storage_impl,
|
||||||
self.tenant_id,
|
&self.tenant_id,
|
||||||
self.timeline_id,
|
&self.timeline_id,
|
||||||
)
|
)
|
||||||
.measure_remote_op(
|
.measure_remote_op(
|
||||||
self.tenant_id,
|
self.tenant_id,
|
||||||
@@ -608,10 +610,7 @@ impl RemoteTimelineClient {
|
|||||||
self.calls_unfinished_metric_begin(&op);
|
self.calls_unfinished_metric_begin(&op);
|
||||||
upload_queue.queued_operations.push_back(op);
|
upload_queue.queued_operations.push_back(op);
|
||||||
|
|
||||||
info!(
|
info!("scheduled layer file upload {layer_file_name}");
|
||||||
"scheduled layer file upload {}",
|
|
||||||
layer_file_name.file_name()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Launch the task immediately, if possible
|
// Launch the task immediately, if possible
|
||||||
self.launch_queued_tasks(upload_queue);
|
self.launch_queued_tasks(upload_queue);
|
||||||
@@ -664,7 +663,7 @@ impl RemoteTimelineClient {
|
|||||||
});
|
});
|
||||||
self.calls_unfinished_metric_begin(&op);
|
self.calls_unfinished_metric_begin(&op);
|
||||||
upload_queue.queued_operations.push_back(op);
|
upload_queue.queued_operations.push_back(op);
|
||||||
info!("scheduled layer file deletion {}", name.file_name());
|
info!("scheduled layer file deletion {name}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch the tasks immediately, if possible
|
// Launch the tasks immediately, if possible
|
||||||
@@ -751,25 +750,13 @@ impl RemoteTimelineClient {
|
|||||||
stopped.deleted_at = SetDeletedFlagProgress::NotRunning;
|
stopped.deleted_at = SetDeletedFlagProgress::NotRunning;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Have a failpoint that can use the `pause` failpoint action.
|
pausable_failpoint!("persist_deleted_index_part");
|
||||||
// We don't want to block the executor thread, hence, spawn_blocking + await.
|
|
||||||
if cfg!(feature = "testing") {
|
|
||||||
tokio::task::spawn_blocking({
|
|
||||||
let current = tracing::Span::current();
|
|
||||||
move || {
|
|
||||||
let _entered = current.entered();
|
|
||||||
tracing::info!("at failpoint persist_deleted_index_part");
|
|
||||||
fail::fail_point!("persist_deleted_index_part");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("spawn_blocking");
|
|
||||||
}
|
|
||||||
upload::upload_index_part(
|
upload::upload_index_part(
|
||||||
self.conf,
|
self.conf,
|
||||||
&self.storage_impl,
|
&self.storage_impl,
|
||||||
self.tenant_id,
|
&self.tenant_id,
|
||||||
self.timeline_id,
|
&self.timeline_id,
|
||||||
&index_part_with_deleted_at,
|
&index_part_with_deleted_at,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -828,7 +815,7 @@ impl RemoteTimelineClient {
|
|||||||
.queued_operations
|
.queued_operations
|
||||||
.push_back(op);
|
.push_back(op);
|
||||||
|
|
||||||
info!("scheduled layer file deletion {}", name.file_name());
|
info!("scheduled layer file deletion {name}");
|
||||||
deletions_queued += 1;
|
deletions_queued += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,7 +831,7 @@ impl RemoteTimelineClient {
|
|||||||
|
|
||||||
// Do not delete index part yet, it is needed for possible retry. If we remove it first
|
// Do not delete index part yet, it is needed for possible retry. If we remove it first
|
||||||
// and retry will arrive to different pageserver there wont be any traces of it on remote storage
|
// and retry will arrive to different pageserver there wont be any traces of it on remote storage
|
||||||
let timeline_path = self.conf.timeline_path(&self.timeline_id, &self.tenant_id);
|
let timeline_path = self.conf.timeline_path(&self.tenant_id, &self.timeline_id);
|
||||||
let timeline_storage_path = self.conf.remote_path(&timeline_path)?;
|
let timeline_storage_path = self.conf.remote_path(&timeline_path)?;
|
||||||
|
|
||||||
let remaining = self
|
let remaining = self
|
||||||
@@ -855,14 +842,16 @@ impl RemoteTimelineClient {
|
|||||||
let remaining: Vec<RemotePath> = remaining
|
let remaining: Vec<RemotePath> = remaining
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|p| p.object_name() != Some(IndexPart::FILE_NAME))
|
.filter(|p| p.object_name() != Some(IndexPart::FILE_NAME))
|
||||||
|
.inspect(|path| {
|
||||||
|
if let Some(name) = path.object_name() {
|
||||||
|
info!(%name, "deleting a file not referenced from index_part.json");
|
||||||
|
} else {
|
||||||
|
warn!(%path, "deleting a nameless or non-utf8 object not referenced from index_part.json");
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !remaining.is_empty() {
|
if !remaining.is_empty() {
|
||||||
warn!(
|
|
||||||
"Found {} files not bound to index_file.json, proceeding with their deletion",
|
|
||||||
remaining.len()
|
|
||||||
);
|
|
||||||
warn!("About to remove {} files", remaining.len());
|
|
||||||
self.storage_impl.delete_objects(&remaining).await?;
|
self.storage_impl.delete_objects(&remaining).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -871,7 +860,7 @@ impl RemoteTimelineClient {
|
|||||||
debug!("deleting index part");
|
debug!("deleting index part");
|
||||||
self.storage_impl.delete(&index_file_path).await?;
|
self.storage_impl.delete(&index_file_path).await?;
|
||||||
|
|
||||||
info!(deletions_queued, "done deleting, including index_part.json");
|
info!(prefix=%timeline_storage_path, referenced=deletions_queued, not_referenced=%remaining.len(), "done deleting in timeline prefix, including index_part.json");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -936,11 +925,11 @@ impl RemoteTimelineClient {
|
|||||||
|
|
||||||
// Assign unique ID to this task
|
// Assign unique ID to this task
|
||||||
upload_queue.task_counter += 1;
|
upload_queue.task_counter += 1;
|
||||||
let task_id = upload_queue.task_counter;
|
let upload_task_id = upload_queue.task_counter;
|
||||||
|
|
||||||
// Add it to the in-progress map
|
// Add it to the in-progress map
|
||||||
let task = Arc::new(UploadTask {
|
let task = Arc::new(UploadTask {
|
||||||
task_id,
|
task_id: upload_task_id,
|
||||||
op: next_op,
|
op: next_op,
|
||||||
retries: AtomicU32::new(0),
|
retries: AtomicU32::new(0),
|
||||||
});
|
});
|
||||||
@@ -950,6 +939,8 @@ impl RemoteTimelineClient {
|
|||||||
|
|
||||||
// Spawn task to perform the task
|
// Spawn task to perform the task
|
||||||
let self_rc = Arc::clone(self);
|
let self_rc = Arc::clone(self);
|
||||||
|
let tenant_id = self.tenant_id;
|
||||||
|
let timeline_id = self.timeline_id;
|
||||||
task_mgr::spawn(
|
task_mgr::spawn(
|
||||||
self.runtime.handle(),
|
self.runtime.handle(),
|
||||||
TaskKind::RemoteUploadTask,
|
TaskKind::RemoteUploadTask,
|
||||||
@@ -961,7 +952,7 @@ impl RemoteTimelineClient {
|
|||||||
self_rc.perform_upload_task(task).await;
|
self_rc.perform_upload_task(task).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
.instrument(info_span!(parent: None, "remote_upload", tenant = %self.tenant_id, timeline = %self.timeline_id, upload_task_id = %task_id)),
|
.instrument(info_span!(parent: None, "remote_upload", %tenant_id, %timeline_id, %upload_task_id)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Loop back to process next task
|
// Loop back to process next task
|
||||||
@@ -1006,7 +997,7 @@ impl RemoteTimelineClient {
|
|||||||
UploadOp::UploadLayer(ref layer_file_name, ref layer_metadata) => {
|
UploadOp::UploadLayer(ref layer_file_name, ref layer_metadata) => {
|
||||||
let path = &self
|
let path = &self
|
||||||
.conf
|
.conf
|
||||||
.timeline_path(&self.timeline_id, &self.tenant_id)
|
.timeline_path(&self.tenant_id, &self.timeline_id)
|
||||||
.join(layer_file_name.file_name());
|
.join(layer_file_name.file_name());
|
||||||
upload::upload_timeline_layer(
|
upload::upload_timeline_layer(
|
||||||
self.conf,
|
self.conf,
|
||||||
@@ -1027,8 +1018,8 @@ impl RemoteTimelineClient {
|
|||||||
let res = upload::upload_index_part(
|
let res = upload::upload_index_part(
|
||||||
self.conf,
|
self.conf,
|
||||||
&self.storage_impl,
|
&self.storage_impl,
|
||||||
self.tenant_id,
|
&self.tenant_id,
|
||||||
self.timeline_id,
|
&self.timeline_id,
|
||||||
index_part,
|
index_part,
|
||||||
)
|
)
|
||||||
.measure_remote_op(
|
.measure_remote_op(
|
||||||
@@ -1047,7 +1038,7 @@ impl RemoteTimelineClient {
|
|||||||
UploadOp::Delete(delete) => {
|
UploadOp::Delete(delete) => {
|
||||||
let path = &self
|
let path = &self
|
||||||
.conf
|
.conf
|
||||||
.timeline_path(&self.timeline_id, &self.tenant_id)
|
.timeline_path(&self.tenant_id, &self.timeline_id)
|
||||||
.join(delete.layer_file_name.file_name());
|
.join(delete.layer_file_name.file_name());
|
||||||
delete::delete_layer(self.conf, &self.storage_impl, path)
|
delete::delete_layer(self.conf, &self.storage_impl, path)
|
||||||
.measure_remote_op(
|
.measure_remote_op(
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ pub(super) async fn delete_layer<'a>(
|
|||||||
|
|
||||||
let path_to_delete = conf.remote_path(local_layer_path)?;
|
let path_to_delete = conf.remote_path(local_layer_path)?;
|
||||||
|
|
||||||
// XXX: If the deletion fails because the object already didn't exist,
|
// We don't want to print an error if the delete failed if the file has
|
||||||
// it would be good to just issue a warning but consider it success.
|
// already been deleted. Thankfully, in this situation S3 already
|
||||||
// https://github.com/neondatabase/neon/issues/2934
|
// does not yield an error. While OS-provided local file system APIs do yield
|
||||||
|
// errors, we avoid them in the `LocalFs` wrapper.
|
||||||
storage.delete(&path_to_delete).await.with_context(|| {
|
storage.delete(&path_to_delete).await.with_context(|| {
|
||||||
format!("Failed to delete remote layer from storage at {path_to_delete:?}")
|
format!("Failed to delete remote layer from storage at {path_to_delete:?}")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use tracing::{info, warn};
|
|||||||
|
|
||||||
use crate::config::PageServerConf;
|
use crate::config::PageServerConf;
|
||||||
use crate::tenant::storage_layer::LayerFileName;
|
use crate::tenant::storage_layer::LayerFileName;
|
||||||
use crate::tenant::timeline::debug_assert_current_span_has_tenant_and_timeline_id;
|
use crate::tenant::timeline::span::debug_assert_current_span_has_tenant_and_timeline_id;
|
||||||
use crate::{exponential_backoff, DEFAULT_BASE_BACKOFF_SECONDS, DEFAULT_MAX_BACKOFF_SECONDS};
|
use crate::{exponential_backoff, DEFAULT_BASE_BACKOFF_SECONDS, DEFAULT_MAX_BACKOFF_SECONDS};
|
||||||
use remote_storage::{DownloadError, GenericRemoteStorage};
|
use remote_storage::{DownloadError, GenericRemoteStorage};
|
||||||
use utils::crashsafe::path_with_suffix_extension;
|
use utils::crashsafe::path_with_suffix_extension;
|
||||||
@@ -46,7 +46,7 @@ pub async fn download_layer_file<'a>(
|
|||||||
) -> Result<u64, DownloadError> {
|
) -> Result<u64, DownloadError> {
|
||||||
debug_assert_current_span_has_tenant_and_timeline_id();
|
debug_assert_current_span_has_tenant_and_timeline_id();
|
||||||
|
|
||||||
let timeline_path = conf.timeline_path(&timeline_id, &tenant_id);
|
let timeline_path = conf.timeline_path(&tenant_id, &timeline_id);
|
||||||
|
|
||||||
let local_path = timeline_path.join(layer_file_name.file_name());
|
let local_path = timeline_path.join(layer_file_name.file_name());
|
||||||
|
|
||||||
@@ -229,11 +229,11 @@ pub async fn list_remote_timelines<'a>(
|
|||||||
pub(super) async fn download_index_part(
|
pub(super) async fn download_index_part(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
storage: &GenericRemoteStorage,
|
storage: &GenericRemoteStorage,
|
||||||
tenant_id: TenantId,
|
tenant_id: &TenantId,
|
||||||
timeline_id: TimelineId,
|
timeline_id: &TimelineId,
|
||||||
) -> Result<IndexPart, DownloadError> {
|
) -> Result<IndexPart, DownloadError> {
|
||||||
let index_part_path = conf
|
let index_part_path = conf
|
||||||
.metadata_path(timeline_id, tenant_id)
|
.metadata_path(tenant_id, timeline_id)
|
||||||
.with_file_name(IndexPart::FILE_NAME);
|
.with_file_name(IndexPart::FILE_NAME);
|
||||||
let part_storage_path = conf
|
let part_storage_path = conf
|
||||||
.remote_path(&index_part_path)
|
.remote_path(&index_part_path)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use fail::fail_point;
|
use fail::fail_point;
|
||||||
use std::path::Path;
|
use std::{io::ErrorKind, path::Path};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
use crate::{config::PageServerConf, tenant::remote_timeline_client::index::IndexPart};
|
use crate::{config::PageServerConf, tenant::remote_timeline_client::index::IndexPart};
|
||||||
@@ -11,12 +11,14 @@ use utils::id::{TenantId, TimelineId};
|
|||||||
|
|
||||||
use super::index::LayerFileMetadata;
|
use super::index::LayerFileMetadata;
|
||||||
|
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
/// Serializes and uploads the given index part data to the remote storage.
|
/// Serializes and uploads the given index part data to the remote storage.
|
||||||
pub(super) async fn upload_index_part<'a>(
|
pub(super) async fn upload_index_part<'a>(
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
storage: &'a GenericRemoteStorage,
|
storage: &'a GenericRemoteStorage,
|
||||||
tenant_id: TenantId,
|
tenant_id: &TenantId,
|
||||||
timeline_id: TimelineId,
|
timeline_id: &TimelineId,
|
||||||
index_part: &'a IndexPart,
|
index_part: &'a IndexPart,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
tracing::trace!("uploading new index part");
|
tracing::trace!("uploading new index part");
|
||||||
@@ -31,7 +33,7 @@ pub(super) async fn upload_index_part<'a>(
|
|||||||
let index_part_bytes = tokio::io::BufReader::new(std::io::Cursor::new(index_part_bytes));
|
let index_part_bytes = tokio::io::BufReader::new(std::io::Cursor::new(index_part_bytes));
|
||||||
|
|
||||||
let index_part_path = conf
|
let index_part_path = conf
|
||||||
.metadata_path(timeline_id, tenant_id)
|
.metadata_path(tenant_id, timeline_id)
|
||||||
.with_file_name(IndexPart::FILE_NAME);
|
.with_file_name(IndexPart::FILE_NAME);
|
||||||
let storage_path = conf.remote_path(&index_part_path)?;
|
let storage_path = conf.remote_path(&index_part_path)?;
|
||||||
|
|
||||||
@@ -56,9 +58,21 @@ pub(super) async fn upload_timeline_layer<'a>(
|
|||||||
});
|
});
|
||||||
let storage_path = conf.remote_path(source_path)?;
|
let storage_path = conf.remote_path(source_path)?;
|
||||||
|
|
||||||
let source_file = fs::File::open(&source_path)
|
let source_file_res = fs::File::open(&source_path).await;
|
||||||
.await
|
let source_file = match source_file_res {
|
||||||
.with_context(|| format!("Failed to open a source file for layer {source_path:?}"))?;
|
Ok(source_file) => source_file,
|
||||||
|
Err(e) if e.kind() == ErrorKind::NotFound => {
|
||||||
|
// If we encounter this arm, it wasn't intended, but it's also not
|
||||||
|
// a big problem, if it's because the file was deleted before an
|
||||||
|
// upload. However, a nonexistent file can also be indicative of
|
||||||
|
// something worse, like when a file is scheduled for upload before
|
||||||
|
// it has been written to disk yet.
|
||||||
|
info!(path = %source_path.display(), "File to upload doesn't exist. Likely the file has been deleted and an upload is not required any more.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => Err(e)
|
||||||
|
.with_context(|| format!("Failed to open a source file for layer {source_path:?}"))?,
|
||||||
|
};
|
||||||
|
|
||||||
let fs_size = source_file
|
let fs_size = source_file
|
||||||
.metadata()
|
.metadata()
|
||||||
|
|||||||
@@ -110,11 +110,11 @@ pub struct TimelineInputs {
|
|||||||
///
|
///
|
||||||
/// Tenant size does not consider the latest state, but only the state until next_gc_cutoff, which
|
/// Tenant size does not consider the latest state, but only the state until next_gc_cutoff, which
|
||||||
/// is updated on-demand, during the start of this calculation and separate from the
|
/// is updated on-demand, during the start of this calculation and separate from the
|
||||||
/// [`Timeline::latest_gc_cutoff`].
|
/// [`TimelineInputs::latest_gc_cutoff`].
|
||||||
///
|
///
|
||||||
/// For timelines in general:
|
/// For timelines in general:
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```text
|
||||||
/// 0-----|---------|----|------------| · · · · · |·> lsn
|
/// 0-----|---------|----|------------| · · · · · |·> lsn
|
||||||
/// initdb_lsn branchpoints* next_gc_cutoff latest
|
/// initdb_lsn branchpoints* next_gc_cutoff latest
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
17
pageserver/src/tenant/span.rs
Normal file
17
pageserver/src/tenant/span.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#[cfg(debug_assertions)]
|
||||||
|
use utils::tracing_span_assert::{check_fields_present, MultiNameExtractor};
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
pub(crate) fn debug_assert_current_span_has_tenant_id() {}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
pub(crate) static TENANT_ID_EXTRACTOR: once_cell::sync::Lazy<MultiNameExtractor<1>> =
|
||||||
|
once_cell::sync::Lazy::new(|| MultiNameExtractor::new("TenantId", ["tenant_id"]));
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
#[track_caller]
|
||||||
|
pub(crate) fn debug_assert_current_span_has_tenant_id() {
|
||||||
|
if let Err(missing) = check_fields_present!([&*TENANT_ID_EXTRACTOR]) {
|
||||||
|
panic!("missing extractors: {missing:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ pub use inmemory_layer::InMemoryLayer;
|
|||||||
pub use layer_desc::{PersistentLayerDesc, PersistentLayerKey};
|
pub use layer_desc::{PersistentLayerDesc, PersistentLayerKey};
|
||||||
pub use remote_layer::RemoteLayer;
|
pub use remote_layer::RemoteLayer;
|
||||||
|
|
||||||
use super::layer_map::BatchedUpdates;
|
use super::timeline::layer_manager::LayerManager;
|
||||||
|
|
||||||
pub fn range_overlaps<T>(a: &Range<T>, b: &Range<T>) -> bool
|
pub fn range_overlaps<T>(a: &Range<T>, b: &Range<T>) -> bool
|
||||||
where
|
where
|
||||||
@@ -54,13 +54,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn range_eq<T>(a: &Range<T>, b: &Range<T>) -> bool
|
|
||||||
where
|
|
||||||
T: PartialEq<T>,
|
|
||||||
{
|
|
||||||
a.start == b.start && a.end == b.end
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Struct used to communicate across calls to 'get_value_reconstruct_data'.
|
/// Struct used to communicate across calls to 'get_value_reconstruct_data'.
|
||||||
///
|
///
|
||||||
/// Before first call, you can fill in 'page_img' if you have an older cached
|
/// Before first call, you can fill in 'page_img' if you have an older cached
|
||||||
@@ -169,6 +162,9 @@ impl LayerAccessStats {
|
|||||||
/// The caller is responsible for recording a residence event
|
/// The caller is responsible for recording a residence event
|
||||||
/// using [`record_residence_event`] before calling `latest_activity`.
|
/// using [`record_residence_event`] before calling `latest_activity`.
|
||||||
/// If they don't, [`latest_activity`] will return `None`.
|
/// If they don't, [`latest_activity`] will return `None`.
|
||||||
|
///
|
||||||
|
/// [`record_residence_event`]: Self::record_residence_event
|
||||||
|
/// [`latest_activity`]: Self::latest_activity
|
||||||
pub(crate) fn empty_will_record_residence_event_later() -> Self {
|
pub(crate) fn empty_will_record_residence_event_later() -> Self {
|
||||||
LayerAccessStats(Mutex::default())
|
LayerAccessStats(Mutex::default())
|
||||||
}
|
}
|
||||||
@@ -176,13 +172,13 @@ impl LayerAccessStats {
|
|||||||
/// Create an empty stats object and record a [`LayerLoad`] event with the given residence status.
|
/// Create an empty stats object and record a [`LayerLoad`] event with the given residence status.
|
||||||
///
|
///
|
||||||
/// See [`record_residence_event`] for why you need to do this while holding the layer map lock.
|
/// See [`record_residence_event`] for why you need to do this while holding the layer map lock.
|
||||||
pub(crate) fn for_loading_layer<L>(
|
///
|
||||||
layer_map_lock_held_witness: &BatchedUpdates<'_, L>,
|
/// [`LayerLoad`]: LayerResidenceEventReason::LayerLoad
|
||||||
|
/// [`record_residence_event`]: Self::record_residence_event
|
||||||
|
pub(crate) fn for_loading_layer(
|
||||||
|
layer_map_lock_held_witness: &LayerManager,
|
||||||
status: LayerResidenceStatus,
|
status: LayerResidenceStatus,
|
||||||
) -> Self
|
) -> Self {
|
||||||
where
|
|
||||||
L: ?Sized + Layer,
|
|
||||||
{
|
|
||||||
let new = LayerAccessStats(Mutex::new(LayerAccessStatsLocked::default()));
|
let new = LayerAccessStats(Mutex::new(LayerAccessStatsLocked::default()));
|
||||||
new.record_residence_event(
|
new.record_residence_event(
|
||||||
layer_map_lock_held_witness,
|
layer_map_lock_held_witness,
|
||||||
@@ -197,14 +193,13 @@ impl LayerAccessStats {
|
|||||||
/// The `new_status` is not recorded in `self`.
|
/// The `new_status` is not recorded in `self`.
|
||||||
///
|
///
|
||||||
/// See [`record_residence_event`] for why you need to do this while holding the layer map lock.
|
/// See [`record_residence_event`] for why you need to do this while holding the layer map lock.
|
||||||
pub(crate) fn clone_for_residence_change<L>(
|
///
|
||||||
|
/// [`record_residence_event`]: Self::record_residence_event
|
||||||
|
pub(crate) fn clone_for_residence_change(
|
||||||
&self,
|
&self,
|
||||||
layer_map_lock_held_witness: &BatchedUpdates<'_, L>,
|
layer_map_lock_held_witness: &LayerManager,
|
||||||
new_status: LayerResidenceStatus,
|
new_status: LayerResidenceStatus,
|
||||||
) -> LayerAccessStats
|
) -> LayerAccessStats {
|
||||||
where
|
|
||||||
L: ?Sized + Layer,
|
|
||||||
{
|
|
||||||
let clone = {
|
let clone = {
|
||||||
let inner = self.0.lock().unwrap();
|
let inner = self.0.lock().unwrap();
|
||||||
inner.clone()
|
inner.clone()
|
||||||
@@ -232,14 +227,12 @@ impl LayerAccessStats {
|
|||||||
/// - Compact: Grab layer map lock, add the new L1 to layer map and remove the L0s, release layer map lock.
|
/// - Compact: Grab layer map lock, add the new L1 to layer map and remove the L0s, release layer map lock.
|
||||||
/// - Eviction: observes the new L1 layer whose only activity timestamp is the LayerCreate event.
|
/// - Eviction: observes the new L1 layer whose only activity timestamp is the LayerCreate event.
|
||||||
///
|
///
|
||||||
pub(crate) fn record_residence_event<L>(
|
pub(crate) fn record_residence_event(
|
||||||
&self,
|
&self,
|
||||||
_layer_map_lock_held_witness: &BatchedUpdates<'_, L>,
|
_layer_map_lock_held_witness: &LayerManager,
|
||||||
status: LayerResidenceStatus,
|
status: LayerResidenceStatus,
|
||||||
reason: LayerResidenceEventReason,
|
reason: LayerResidenceEventReason,
|
||||||
) where
|
) {
|
||||||
L: ?Sized + Layer,
|
|
||||||
{
|
|
||||||
let mut locked = self.0.lock().unwrap();
|
let mut locked = self.0.lock().unwrap();
|
||||||
locked.iter_mut().for_each(|inner| {
|
locked.iter_mut().for_each(|inner| {
|
||||||
inner
|
inner
|
||||||
@@ -309,11 +302,13 @@ impl LayerAccessStats {
|
|||||||
/// implementation error. This function logs a rate-limited warning in that case.
|
/// implementation error. This function logs a rate-limited warning in that case.
|
||||||
///
|
///
|
||||||
/// TODO: use type system to avoid the need for `fallback`.
|
/// TODO: use type system to avoid the need for `fallback`.
|
||||||
/// The approach in https://github.com/neondatabase/neon/pull/3775
|
/// The approach in <https://github.com/neondatabase/neon/pull/3775>
|
||||||
/// could be used to enforce that a residence event is recorded
|
/// could be used to enforce that a residence event is recorded
|
||||||
/// before a layer is added to the layer map. We could also have
|
/// before a layer is added to the layer map. We could also have
|
||||||
/// a layer wrapper type that holds the LayerAccessStats, and ensure
|
/// a layer wrapper type that holds the LayerAccessStats, and ensure
|
||||||
/// that that type can only be produced by inserting into the layer map.
|
/// that that type can only be produced by inserting into the layer map.
|
||||||
|
///
|
||||||
|
/// [`record_residence_event`]: Self::record_residence_event
|
||||||
pub(crate) fn latest_activity(&self) -> Option<SystemTime> {
|
pub(crate) fn latest_activity(&self) -> Option<SystemTime> {
|
||||||
let locked = self.0.lock().unwrap();
|
let locked = self.0.lock().unwrap();
|
||||||
let inner = &locked.for_eviction_policy;
|
let inner = &locked.for_eviction_policy;
|
||||||
@@ -338,12 +333,12 @@ impl LayerAccessStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Supertrait of the [`Layer`] trait that captures the bare minimum interface
|
/// Supertrait of the [`Layer`] trait that captures the bare minimum interface
|
||||||
/// required by [`LayerMap`].
|
/// required by [`LayerMap`](super::layer_map::LayerMap).
|
||||||
///
|
///
|
||||||
/// All layers should implement a minimal `std::fmt::Debug` without tenant or
|
/// All layers should implement a minimal `std::fmt::Debug` without tenant or
|
||||||
/// timeline names, because those are known in the context of which the layers
|
/// timeline names, because those are known in the context of which the layers
|
||||||
/// are used in (timeline).
|
/// are used in (timeline).
|
||||||
pub trait Layer: std::fmt::Debug + Send + Sync {
|
pub trait Layer: std::fmt::Debug + std::fmt::Display + Send + Sync {
|
||||||
/// Range of keys that this layer covers
|
/// Range of keys that this layer covers
|
||||||
fn get_key_range(&self) -> Range<Key>;
|
fn get_key_range(&self) -> Range<Key>;
|
||||||
|
|
||||||
@@ -381,19 +376,22 @@ pub trait Layer: std::fmt::Debug + Send + Sync {
|
|||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<ValueReconstructResult>;
|
) -> Result<ValueReconstructResult>;
|
||||||
|
|
||||||
/// A short ID string that uniquely identifies the given layer within a [`LayerMap`].
|
|
||||||
fn short_id(&self) -> String;
|
|
||||||
|
|
||||||
/// Dump summary of the contents of the layer to stdout
|
/// Dump summary of the contents of the layer to stdout
|
||||||
fn dump(&self, verbose: bool, ctx: &RequestContext) -> Result<()>;
|
fn dump(&self, verbose: bool, ctx: &RequestContext) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returned by [`Layer::iter`]
|
/// Returned by [`PersistentLayer::iter`]
|
||||||
pub type LayerIter<'i> = Box<dyn Iterator<Item = Result<(Key, Lsn, Value)>> + 'i + Send>;
|
pub type LayerIter<'i> = Box<dyn Iterator<Item = Result<(Key, Lsn, Value)>> + 'i + Send>;
|
||||||
|
|
||||||
/// Returned by [`Layer::key_iter`]
|
/// Returned by [`PersistentLayer::key_iter`]
|
||||||
pub type LayerKeyIter<'i> = Box<dyn Iterator<Item = (Key, Lsn, u64)> + 'i + Send>;
|
pub type LayerKeyIter<'i> = Box<dyn Iterator<Item = (Key, Lsn, u64)> + 'i + Send>;
|
||||||
|
|
||||||
|
/// Get a layer descriptor from a layer.
|
||||||
|
pub trait AsLayerDesc {
|
||||||
|
/// Get the layer descriptor.
|
||||||
|
fn layer_desc(&self) -> &PersistentLayerDesc;
|
||||||
|
}
|
||||||
|
|
||||||
/// A Layer contains all data in a "rectangle" consisting of a range of keys and
|
/// A Layer contains all data in a "rectangle" consisting of a range of keys and
|
||||||
/// range of LSNs.
|
/// range of LSNs.
|
||||||
///
|
///
|
||||||
@@ -407,10 +405,8 @@ pub type LayerKeyIter<'i> = Box<dyn Iterator<Item = (Key, Lsn, u64)> + 'i + Send
|
|||||||
/// A delta layer contains all modifications within a range of LSNs and keys.
|
/// A delta layer contains all modifications within a range of LSNs and keys.
|
||||||
/// An image layer is a snapshot of all the data in a key-range, at a single
|
/// An image layer is a snapshot of all the data in a key-range, at a single
|
||||||
/// LSN.
|
/// LSN.
|
||||||
pub trait PersistentLayer: Layer {
|
pub trait PersistentLayer: Layer + AsLayerDesc {
|
||||||
/// Get the layer descriptor.
|
/// Identify the tenant this layer belongs to
|
||||||
fn layer_desc(&self) -> &PersistentLayerDesc;
|
|
||||||
|
|
||||||
fn get_tenant_id(&self) -> TenantId {
|
fn get_tenant_id(&self) -> TenantId {
|
||||||
self.layer_desc().tenant_id
|
self.layer_desc().tenant_id
|
||||||
}
|
}
|
||||||
@@ -446,6 +442,10 @@ pub trait PersistentLayer: Layer {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn downcast_delta_layer(self: Arc<Self>) -> Option<std::sync::Arc<DeltaLayer>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn is_remote_layer(&self) -> bool {
|
fn is_remote_layer(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -473,94 +473,40 @@ pub fn downcast_remote_layer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Holds metadata about a layer without any content. Used mostly for testing.
|
pub mod tests {
|
||||||
///
|
use super::*;
|
||||||
/// To use filenames as fixtures, parse them as [`LayerFileName`] then convert from that to a
|
|
||||||
/// LayerDescriptor.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct LayerDescriptor {
|
|
||||||
pub key: Range<Key>,
|
|
||||||
pub lsn: Range<Lsn>,
|
|
||||||
pub is_incremental: bool,
|
|
||||||
pub short_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayerDescriptor {
|
impl From<DeltaFileName> for PersistentLayerDesc {
|
||||||
/// `LayerDescriptor` is only used for testing purpose so it does not matter whether it is image / delta,
|
fn from(value: DeltaFileName) -> Self {
|
||||||
/// and the tenant / timeline id does not matter.
|
PersistentLayerDesc::new_delta(
|
||||||
pub fn get_persistent_layer_desc(&self) -> PersistentLayerDesc {
|
TenantId::from_array([0; 16]),
|
||||||
PersistentLayerDesc::new_delta(
|
TimelineId::from_array([0; 16]),
|
||||||
TenantId::from_array([0; 16]),
|
value.key_range,
|
||||||
TimelineId::from_array([0; 16]),
|
value.lsn_range,
|
||||||
self.key.clone(),
|
233,
|
||||||
self.lsn.clone(),
|
)
|
||||||
233,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Layer for LayerDescriptor {
|
|
||||||
fn get_key_range(&self) -> Range<Key> {
|
|
||||||
self.key.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_lsn_range(&self) -> Range<Lsn> {
|
|
||||||
self.lsn.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_incremental(&self) -> bool {
|
|
||||||
self.is_incremental
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_value_reconstruct_data(
|
|
||||||
&self,
|
|
||||||
_key: Key,
|
|
||||||
_lsn_range: Range<Lsn>,
|
|
||||||
_reconstruct_data: &mut ValueReconstructState,
|
|
||||||
_ctx: &RequestContext,
|
|
||||||
) -> Result<ValueReconstructResult> {
|
|
||||||
todo!("This method shouldn't be part of the Layer trait")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn short_id(&self) -> String {
|
|
||||||
self.short_id.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dump(&self, _verbose: bool, _ctx: &RequestContext) -> Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DeltaFileName> for LayerDescriptor {
|
|
||||||
fn from(value: DeltaFileName) -> Self {
|
|
||||||
let short_id = value.to_string();
|
|
||||||
LayerDescriptor {
|
|
||||||
key: value.key_range,
|
|
||||||
lsn: value.lsn_range,
|
|
||||||
is_incremental: true,
|
|
||||||
short_id,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ImageFileName> for LayerDescriptor {
|
impl From<ImageFileName> for PersistentLayerDesc {
|
||||||
fn from(value: ImageFileName) -> Self {
|
fn from(value: ImageFileName) -> Self {
|
||||||
let short_id = value.to_string();
|
PersistentLayerDesc::new_img(
|
||||||
let lsn = value.lsn_as_range();
|
TenantId::from_array([0; 16]),
|
||||||
LayerDescriptor {
|
TimelineId::from_array([0; 16]),
|
||||||
key: value.key_range,
|
value.key_range,
|
||||||
lsn,
|
value.lsn,
|
||||||
is_incremental: false,
|
false,
|
||||||
short_id,
|
233,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl From<LayerFileName> for LayerDescriptor {
|
impl From<LayerFileName> for PersistentLayerDesc {
|
||||||
fn from(value: LayerFileName) -> Self {
|
fn from(value: LayerFileName) -> Self {
|
||||||
match value {
|
match value {
|
||||||
LayerFileName::Delta(d) => Self::from(d),
|
LayerFileName::Delta(d) => Self::from(d),
|
||||||
LayerFileName::Image(i) => Self::from(i),
|
LayerFileName::Image(i) => Self::from(i),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,18 @@
|
|||||||
//! must be page images or WAL records with the 'will_init' flag set, so that
|
//! must be page images or WAL records with the 'will_init' flag set, so that
|
||||||
//! they can be replayed without referring to an older page version.
|
//! they can be replayed without referring to an older page version.
|
||||||
//!
|
//!
|
||||||
//! The delta files are stored in timelines/<timeline_id> directory. Currently,
|
//! The delta files are stored in `timelines/<timeline_id>` directory. Currently,
|
||||||
//! there are no subdirectories, and each delta file is named like this:
|
//! there are no subdirectories, and each delta file is named like this:
|
||||||
//!
|
//!
|
||||||
//! <key start>-<key end>__<start LSN>-<end LSN
|
//! ```text
|
||||||
|
//! <key start>-<key end>__<start LSN>-<end LSN>
|
||||||
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! For example:
|
//! For example:
|
||||||
//!
|
//!
|
||||||
|
//! ```text
|
||||||
//! 000000067F000032BE0000400000000020B6-000000067F000032BE0000400000000030B6__000000578C6B29-0000000057A50051
|
//! 000000067F000032BE0000400000000020B6-000000067F000032BE0000400000000030B6__000000578C6B29-0000000057A50051
|
||||||
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Every delta file consists of three parts: "summary", "index", and
|
//! Every delta file consists of three parts: "summary", "index", and
|
||||||
//! "values". The summary is a fixed size header at the beginning of the file,
|
//! "values". The summary is a fixed size header at the beginning of the file,
|
||||||
@@ -47,6 +51,7 @@ use std::io::{Seek, SeekFrom};
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::os::unix::fs::FileExt;
|
use std::os::unix::fs::FileExt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
use utils::{
|
use utils::{
|
||||||
@@ -56,8 +61,8 @@ use utils::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
DeltaFileName, Layer, LayerAccessStats, LayerAccessStatsReset, LayerIter, LayerKeyIter,
|
AsLayerDesc, DeltaFileName, Layer, LayerAccessStats, LayerAccessStatsReset, LayerIter,
|
||||||
PathOrConf, PersistentLayerDesc,
|
LayerKeyIter, PathOrConf, PersistentLayerDesc,
|
||||||
};
|
};
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -222,13 +227,14 @@ impl Layer for DeltaLayer {
|
|||||||
/// debugging function to print out the contents of the layer
|
/// debugging function to print out the contents of the layer
|
||||||
fn dump(&self, verbose: bool, ctx: &RequestContext) -> Result<()> {
|
fn dump(&self, verbose: bool, ctx: &RequestContext) -> Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"----- delta layer for ten {} tli {} keys {}-{} lsn {}-{} ----",
|
"----- delta layer for ten {} tli {} keys {}-{} lsn {}-{} size {} ----",
|
||||||
self.desc.tenant_id,
|
self.desc.tenant_id,
|
||||||
self.desc.timeline_id,
|
self.desc.timeline_id,
|
||||||
self.desc.key_range.start,
|
self.desc.key_range.start,
|
||||||
self.desc.key_range.end,
|
self.desc.key_range.end,
|
||||||
self.desc.lsn_range.start,
|
self.desc.lsn_range.start,
|
||||||
self.desc.lsn_range.end
|
self.desc.lsn_range.end,
|
||||||
|
self.desc.file_size,
|
||||||
);
|
);
|
||||||
|
|
||||||
if !verbose {
|
if !verbose {
|
||||||
@@ -394,16 +400,23 @@ impl Layer for DeltaLayer {
|
|||||||
fn is_incremental(&self) -> bool {
|
fn is_incremental(&self) -> bool {
|
||||||
self.layer_desc().is_incremental
|
self.layer_desc().is_incremental
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
impl std::fmt::Display for DeltaLayer {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.layer_desc().short_id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
impl AsLayerDesc for DeltaLayer {
|
||||||
fn short_id(&self) -> String {
|
fn layer_desc(&self) -> &PersistentLayerDesc {
|
||||||
self.layer_desc().short_id()
|
&self.desc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PersistentLayer for DeltaLayer {
|
impl PersistentLayer for DeltaLayer {
|
||||||
fn layer_desc(&self) -> &PersistentLayerDesc {
|
fn downcast_delta_layer(self: Arc<Self>) -> Option<std::sync::Arc<DeltaLayer>> {
|
||||||
&self.desc
|
Some(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_path(&self) -> Option<PathBuf> {
|
fn local_path(&self) -> Option<PathBuf> {
|
||||||
@@ -457,22 +470,22 @@ impl PersistentLayer for DeltaLayer {
|
|||||||
impl DeltaLayer {
|
impl DeltaLayer {
|
||||||
fn path_for(
|
fn path_for(
|
||||||
path_or_conf: &PathOrConf,
|
path_or_conf: &PathOrConf,
|
||||||
timeline_id: TimelineId,
|
tenant_id: &TenantId,
|
||||||
tenant_id: TenantId,
|
timeline_id: &TimelineId,
|
||||||
fname: &DeltaFileName,
|
fname: &DeltaFileName,
|
||||||
) -> PathBuf {
|
) -> PathBuf {
|
||||||
match path_or_conf {
|
match path_or_conf {
|
||||||
PathOrConf::Path(path) => path.clone(),
|
PathOrConf::Path(path) => path.clone(),
|
||||||
PathOrConf::Conf(conf) => conf
|
PathOrConf::Conf(conf) => conf
|
||||||
.timeline_path(&timeline_id, &tenant_id)
|
.timeline_path(tenant_id, timeline_id)
|
||||||
.join(fname.to_string()),
|
.join(fname.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn temp_path_for(
|
fn temp_path_for(
|
||||||
conf: &PageServerConf,
|
conf: &PageServerConf,
|
||||||
timeline_id: TimelineId,
|
tenant_id: &TenantId,
|
||||||
tenant_id: TenantId,
|
timeline_id: &TimelineId,
|
||||||
key_start: Key,
|
key_start: Key,
|
||||||
lsn_range: &Range<Lsn>,
|
lsn_range: &Range<Lsn>,
|
||||||
) -> PathBuf {
|
) -> PathBuf {
|
||||||
@@ -482,7 +495,7 @@ impl DeltaLayer {
|
|||||||
.map(char::from)
|
.map(char::from)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
conf.timeline_path(&timeline_id, &tenant_id).join(format!(
|
conf.timeline_path(tenant_id, timeline_id).join(format!(
|
||||||
"{}-XXX__{:016X}-{:016X}.{}.{}",
|
"{}-XXX__{:016X}-{:016X}.{}.{}",
|
||||||
key_start,
|
key_start,
|
||||||
u64::from(lsn_range.start),
|
u64::from(lsn_range.start),
|
||||||
@@ -604,8 +617,8 @@ impl DeltaLayer {
|
|||||||
pub fn path(&self) -> PathBuf {
|
pub fn path(&self) -> PathBuf {
|
||||||
Self::path_for(
|
Self::path_for(
|
||||||
&self.path_or_conf,
|
&self.path_or_conf,
|
||||||
self.desc.timeline_id,
|
&self.desc.tenant_id,
|
||||||
self.desc.tenant_id,
|
&self.desc.timeline_id,
|
||||||
&self.layer_name(),
|
&self.layer_name(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -653,7 +666,7 @@ impl DeltaLayerWriterInner {
|
|||||||
//
|
//
|
||||||
// Note: This overwrites any existing file. There shouldn't be any.
|
// Note: This overwrites any existing file. There shouldn't be any.
|
||||||
// FIXME: throw an error instead?
|
// FIXME: throw an error instead?
|
||||||
let path = DeltaLayer::temp_path_for(conf, timeline_id, tenant_id, key_start, &lsn_range);
|
let path = DeltaLayer::temp_path_for(conf, &tenant_id, &timeline_id, key_start, &lsn_range);
|
||||||
|
|
||||||
let mut file = VirtualFile::create(&path)?;
|
let mut file = VirtualFile::create(&path)?;
|
||||||
// make room for the header block
|
// make room for the header block
|
||||||
@@ -768,8 +781,8 @@ impl DeltaLayerWriterInner {
|
|||||||
// FIXME: throw an error instead?
|
// FIXME: throw an error instead?
|
||||||
let final_path = DeltaLayer::path_for(
|
let final_path = DeltaLayer::path_for(
|
||||||
&PathOrConf::Conf(self.conf),
|
&PathOrConf::Conf(self.conf),
|
||||||
self.timeline_id,
|
&self.tenant_id,
|
||||||
self.tenant_id,
|
&self.timeline_id,
|
||||||
&DeltaFileName {
|
&DeltaFileName {
|
||||||
key_range: self.key_start..key_end,
|
key_range: self.key_start..key_end,
|
||||||
lsn_range: self.lsn_range,
|
lsn_range: self.lsn_range,
|
||||||
@@ -796,7 +809,7 @@ impl DeltaLayerWriterInner {
|
|||||||
///
|
///
|
||||||
/// # Note
|
/// # Note
|
||||||
///
|
///
|
||||||
/// As described in https://github.com/neondatabase/neon/issues/2650, it's
|
/// As described in <https://github.com/neondatabase/neon/issues/2650>, it's
|
||||||
/// possible for the writer to drop before `finish` is actually called. So this
|
/// possible for the writer to drop before `finish` is actually called. So this
|
||||||
/// could lead to odd temporary files in the directory, exhausting file system.
|
/// could lead to odd temporary files in the directory, exhausting file system.
|
||||||
/// This structure wraps `DeltaLayerWriterInner` and also contains `Drop`
|
/// This structure wraps `DeltaLayerWriterInner` and also contains `Drop`
|
||||||
|
|||||||
@@ -57,8 +57,9 @@ impl Ord for DeltaFileName {
|
|||||||
|
|
||||||
/// Represents the filename of a DeltaLayer
|
/// Represents the filename of a DeltaLayer
|
||||||
///
|
///
|
||||||
|
/// ```text
|
||||||
/// <key start>-<key end>__<LSN start>-<LSN end>
|
/// <key start>-<key end>__<LSN start>-<LSN end>
|
||||||
///
|
/// ```
|
||||||
impl DeltaFileName {
|
impl DeltaFileName {
|
||||||
///
|
///
|
||||||
/// Parse a string as a delta file name. Returns None if the filename does not
|
/// Parse a string as a delta file name. Returns None if the filename does not
|
||||||
@@ -162,7 +163,9 @@ impl ImageFileName {
|
|||||||
///
|
///
|
||||||
/// Represents the filename of an ImageLayer
|
/// Represents the filename of an ImageLayer
|
||||||
///
|
///
|
||||||
|
/// ```text
|
||||||
/// <key start>-<key end>__<LSN>
|
/// <key start>-<key end>__<LSN>
|
||||||
|
/// ```
|
||||||
impl ImageFileName {
|
impl ImageFileName {
|
||||||
///
|
///
|
||||||
/// Parse a string as an image file name. Returns None if the filename does not
|
/// Parse a string as an image file name. Returns None if the filename does not
|
||||||
@@ -210,9 +213,15 @@ pub enum LayerFileName {
|
|||||||
|
|
||||||
impl LayerFileName {
|
impl LayerFileName {
|
||||||
pub fn file_name(&self) -> String {
|
pub fn file_name(&self) -> String {
|
||||||
|
self.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for LayerFileName {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Image(fname) => fname.to_string(),
|
Self::Image(fname) => write!(f, "{fname}"),
|
||||||
Self::Delta(fname) => fname.to_string(),
|
Self::Delta(fname) => write!(f, "{fname}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,15 @@
|
|||||||
//! timelines/<timeline_id> directory. Currently, there are no
|
//! timelines/<timeline_id> directory. Currently, there are no
|
||||||
//! subdirectories, and each image layer file is named like this:
|
//! subdirectories, and each image layer file is named like this:
|
||||||
//!
|
//!
|
||||||
|
//! ```text
|
||||||
//! <key start>-<key end>__<LSN>
|
//! <key start>-<key end>__<LSN>
|
||||||
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! For example:
|
//! For example:
|
||||||
//!
|
//!
|
||||||
|
//! ```text
|
||||||
//! 000000067F000032BE0000400000000070B6-000000067F000032BE0000400000000080B6__00000000346BC568
|
//! 000000067F000032BE0000400000000070B6-000000067F000032BE0000400000000080B6__00000000346BC568
|
||||||
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Every image layer file consists of three parts: "summary",
|
//! Every image layer file consists of three parts: "summary",
|
||||||
//! "index", and "values". The summary is a fixed size header at the
|
//! "index", and "values". The summary is a fixed size header at the
|
||||||
@@ -53,7 +57,9 @@ use utils::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::filename::ImageFileName;
|
use super::filename::ImageFileName;
|
||||||
use super::{Layer, LayerAccessStatsReset, LayerIter, PathOrConf, PersistentLayerDesc};
|
use super::{
|
||||||
|
AsLayerDesc, Layer, LayerAccessStatsReset, LayerIter, PathOrConf, PersistentLayerDesc,
|
||||||
|
};
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Header stored in the beginning of the file
|
/// Header stored in the beginning of the file
|
||||||
@@ -153,12 +159,14 @@ impl Layer for ImageLayer {
|
|||||||
/// debugging function to print out the contents of the layer
|
/// debugging function to print out the contents of the layer
|
||||||
fn dump(&self, verbose: bool, ctx: &RequestContext) -> Result<()> {
|
fn dump(&self, verbose: bool, ctx: &RequestContext) -> Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"----- image layer for ten {} tli {} key {}-{} at {} ----",
|
"----- image layer for ten {} tli {} key {}-{} at {} is_incremental {} size {} ----",
|
||||||
self.desc.tenant_id,
|
self.desc.tenant_id,
|
||||||
self.desc.timeline_id,
|
self.desc.timeline_id,
|
||||||
self.desc.key_range.start,
|
self.desc.key_range.start,
|
||||||
self.desc.key_range.end,
|
self.desc.key_range.end,
|
||||||
self.lsn
|
self.lsn,
|
||||||
|
self.desc.is_incremental,
|
||||||
|
self.desc.file_size
|
||||||
);
|
);
|
||||||
|
|
||||||
if !verbose {
|
if !verbose {
|
||||||
@@ -230,18 +238,22 @@ impl Layer for ImageLayer {
|
|||||||
fn is_incremental(&self) -> bool {
|
fn is_incremental(&self) -> bool {
|
||||||
self.layer_desc().is_incremental
|
self.layer_desc().is_incremental
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
fn short_id(&self) -> String {
|
impl std::fmt::Display for ImageLayer {
|
||||||
self.layer_desc().short_id()
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.layer_desc().short_id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsLayerDesc for ImageLayer {
|
||||||
|
fn layer_desc(&self) -> &PersistentLayerDesc {
|
||||||
|
&self.desc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PersistentLayer for ImageLayer {
|
impl PersistentLayer for ImageLayer {
|
||||||
fn layer_desc(&self) -> &PersistentLayerDesc {
|
|
||||||
&self.desc
|
|
||||||
}
|
|
||||||
|
|
||||||
fn local_path(&self) -> Option<PathBuf> {
|
fn local_path(&self) -> Option<PathBuf> {
|
||||||
Some(self.path())
|
Some(self.path())
|
||||||
}
|
}
|
||||||
@@ -284,7 +296,7 @@ impl ImageLayer {
|
|||||||
match path_or_conf {
|
match path_or_conf {
|
||||||
PathOrConf::Path(path) => path.to_path_buf(),
|
PathOrConf::Path(path) => path.to_path_buf(),
|
||||||
PathOrConf::Conf(conf) => conf
|
PathOrConf::Conf(conf) => conf
|
||||||
.timeline_path(&timeline_id, &tenant_id)
|
.timeline_path(&tenant_id, &timeline_id)
|
||||||
.join(fname.to_string()),
|
.join(fname.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,7 +313,7 @@ impl ImageLayer {
|
|||||||
.map(char::from)
|
.map(char::from)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
conf.timeline_path(&timeline_id, &tenant_id)
|
conf.timeline_path(&tenant_id, &timeline_id)
|
||||||
.join(format!("{fname}.{rand_string}.{TEMP_FILE_SUFFIX}"))
|
.join(format!("{fname}.{rand_string}.{TEMP_FILE_SUFFIX}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,7 +664,7 @@ impl ImageLayerWriterInner {
|
|||||||
///
|
///
|
||||||
/// # Note
|
/// # Note
|
||||||
///
|
///
|
||||||
/// As described in https://github.com/neondatabase/neon/issues/2650, it's
|
/// As described in <https://github.com/neondatabase/neon/issues/2650>, it's
|
||||||
/// possible for the writer to drop before `finish` is actually called. So this
|
/// possible for the writer to drop before `finish` is actually called. So this
|
||||||
/// could lead to odd temporary files in the directory, exhausting file system.
|
/// could lead to odd temporary files in the directory, exhausting file system.
|
||||||
/// This structure wraps `ImageLayerWriterInner` and also contains `Drop`
|
/// This structure wraps `ImageLayerWriterInner` and also contains `Drop`
|
||||||
|
|||||||
@@ -131,13 +131,6 @@ impl Layer for InMemoryLayer {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn short_id(&self) -> String {
|
|
||||||
let inner = self.inner.read().unwrap();
|
|
||||||
|
|
||||||
let end_lsn = inner.end_lsn.unwrap_or(Lsn(u64::MAX));
|
|
||||||
format!("inmem-{:016X}-{:016X}", self.start_lsn.0, end_lsn.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// debugging function to print out the contents of the layer
|
/// debugging function to print out the contents of the layer
|
||||||
fn dump(&self, verbose: bool, _ctx: &RequestContext) -> Result<()> {
|
fn dump(&self, verbose: bool, _ctx: &RequestContext) -> Result<()> {
|
||||||
let inner = self.inner.read().unwrap();
|
let inner = self.inner.read().unwrap();
|
||||||
@@ -240,6 +233,15 @@ impl Layer for InMemoryLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for InMemoryLayer {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let inner = self.inner.read().unwrap();
|
||||||
|
|
||||||
|
let end_lsn = inner.end_lsn.unwrap_or(Lsn(u64::MAX));
|
||||||
|
write!(f, "inmem-{:016X}-{:016X}", self.start_lsn.0, end_lsn.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl InMemoryLayer {
|
impl InMemoryLayer {
|
||||||
///
|
///
|
||||||
/// Get layer size on the disk
|
/// Get layer size on the disk
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use core::fmt::Display;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use utils::{
|
use utils::{
|
||||||
id::{TenantId, TimelineId},
|
id::{TenantId, TimelineId},
|
||||||
@@ -48,8 +49,8 @@ impl PersistentLayerDesc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn short_id(&self) -> String {
|
pub fn short_id(&self) -> impl Display {
|
||||||
self.filename().file_name()
|
self.filename()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -173,13 +174,16 @@ impl PersistentLayerDesc {
|
|||||||
|
|
||||||
pub fn dump(&self, _verbose: bool, _ctx: &RequestContext) -> Result<()> {
|
pub fn dump(&self, _verbose: bool, _ctx: &RequestContext) -> Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"----- layer for ten {} tli {} keys {}-{} lsn {}-{} ----",
|
"----- layer for ten {} tli {} keys {}-{} lsn {}-{} is_delta {} is_incremental {} size {} ----",
|
||||||
self.tenant_id,
|
self.tenant_id,
|
||||||
self.timeline_id,
|
self.timeline_id,
|
||||||
self.key_range.start,
|
self.key_range.start,
|
||||||
self.key_range.end,
|
self.key_range.end,
|
||||||
self.lsn_range.start,
|
self.lsn_range.start,
|
||||||
self.lsn_range.end
|
self.lsn_range.end,
|
||||||
|
self.is_delta,
|
||||||
|
self.is_incremental,
|
||||||
|
self.file_size,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
use crate::config::PageServerConf;
|
use crate::config::PageServerConf;
|
||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::repository::Key;
|
use crate::repository::Key;
|
||||||
use crate::tenant::layer_map::BatchedUpdates;
|
|
||||||
use crate::tenant::remote_timeline_client::index::LayerFileMetadata;
|
use crate::tenant::remote_timeline_client::index::LayerFileMetadata;
|
||||||
use crate::tenant::storage_layer::{Layer, ValueReconstructResult, ValueReconstructState};
|
use crate::tenant::storage_layer::{Layer, ValueReconstructResult, ValueReconstructState};
|
||||||
|
use crate::tenant::timeline::layer_manager::LayerManager;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use pageserver_api::models::HistoricLayerInfo;
|
use pageserver_api::models::HistoricLayerInfo;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
@@ -20,12 +20,12 @@ use utils::{
|
|||||||
|
|
||||||
use super::filename::{DeltaFileName, ImageFileName};
|
use super::filename::{DeltaFileName, ImageFileName};
|
||||||
use super::{
|
use super::{
|
||||||
DeltaLayer, ImageLayer, LayerAccessStats, LayerAccessStatsReset, LayerIter, LayerKeyIter,
|
AsLayerDesc, DeltaLayer, ImageLayer, LayerAccessStats, LayerAccessStatsReset, LayerIter,
|
||||||
LayerResidenceStatus, PersistentLayer, PersistentLayerDesc,
|
LayerKeyIter, LayerResidenceStatus, PersistentLayer, PersistentLayerDesc,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// RemoteLayer is a not yet downloaded [`ImageLayer`] or
|
/// RemoteLayer is a not yet downloaded [`ImageLayer`] or
|
||||||
/// [`crate::storage_layer::DeltaLayer`].
|
/// [`DeltaLayer`](super::DeltaLayer).
|
||||||
///
|
///
|
||||||
/// RemoteLayer might be downloaded on-demand during operations which are
|
/// RemoteLayer might be downloaded on-demand during operations which are
|
||||||
/// allowed download remote layers and during which, it gets replaced with a
|
/// allowed download remote layers and during which, it gets replaced with a
|
||||||
@@ -50,6 +50,8 @@ pub struct RemoteLayer {
|
|||||||
/// It is very unlikely to accumulate these in the Timeline's LayerMap, but having this avoids
|
/// It is very unlikely to accumulate these in the Timeline's LayerMap, but having this avoids
|
||||||
/// a possible fast loop between `Timeline::get_reconstruct_data` and
|
/// a possible fast loop between `Timeline::get_reconstruct_data` and
|
||||||
/// `Timeline::download_remote_layer`, which also logs.
|
/// `Timeline::download_remote_layer`, which also logs.
|
||||||
|
///
|
||||||
|
/// [`ongoing_download`]: Self::ongoing_download
|
||||||
pub(crate) download_replacement_failure: std::sync::atomic::AtomicBool,
|
pub(crate) download_replacement_failure: std::sync::atomic::AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,22 +73,22 @@ impl Layer for RemoteLayer {
|
|||||||
_reconstruct_state: &mut ValueReconstructState,
|
_reconstruct_state: &mut ValueReconstructState,
|
||||||
_ctx: &RequestContext,
|
_ctx: &RequestContext,
|
||||||
) -> Result<ValueReconstructResult> {
|
) -> Result<ValueReconstructResult> {
|
||||||
bail!(
|
bail!("layer {self} needs to be downloaded");
|
||||||
"layer {} needs to be downloaded",
|
|
||||||
self.filename().file_name()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// debugging function to print out the contents of the layer
|
/// debugging function to print out the contents of the layer
|
||||||
fn dump(&self, _verbose: bool, _ctx: &RequestContext) -> Result<()> {
|
fn dump(&self, _verbose: bool, _ctx: &RequestContext) -> Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"----- remote layer for ten {} tli {} keys {}-{} lsn {}-{} ----",
|
"----- remote layer for ten {} tli {} keys {}-{} lsn {}-{} is_delta {} is_incremental {} size {} ----",
|
||||||
self.desc.tenant_id,
|
self.desc.tenant_id,
|
||||||
self.desc.timeline_id,
|
self.desc.timeline_id,
|
||||||
self.desc.key_range.start,
|
self.desc.key_range.start,
|
||||||
self.desc.key_range.end,
|
self.desc.key_range.end,
|
||||||
self.desc.lsn_range.start,
|
self.desc.lsn_range.start,
|
||||||
self.desc.lsn_range.end
|
self.desc.lsn_range.end,
|
||||||
|
self.desc.is_delta,
|
||||||
|
self.desc.is_incremental,
|
||||||
|
self.desc.file_size,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -106,18 +108,22 @@ impl Layer for RemoteLayer {
|
|||||||
fn is_incremental(&self) -> bool {
|
fn is_incremental(&self) -> bool {
|
||||||
self.layer_desc().is_incremental
|
self.layer_desc().is_incremental
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
fn short_id(&self) -> String {
|
impl std::fmt::Display for RemoteLayer {
|
||||||
self.layer_desc().short_id()
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.layer_desc().short_id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsLayerDesc for RemoteLayer {
|
||||||
|
fn layer_desc(&self) -> &PersistentLayerDesc {
|
||||||
|
&self.desc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PersistentLayer for RemoteLayer {
|
impl PersistentLayer for RemoteLayer {
|
||||||
fn layer_desc(&self) -> &PersistentLayerDesc {
|
|
||||||
&self.desc
|
|
||||||
}
|
|
||||||
|
|
||||||
fn local_path(&self) -> Option<PathBuf> {
|
fn local_path(&self) -> Option<PathBuf> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -218,15 +224,12 @@ impl RemoteLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a Layer struct representing this layer, after it has been downloaded.
|
/// Create a Layer struct representing this layer, after it has been downloaded.
|
||||||
pub fn create_downloaded_layer<L>(
|
pub fn create_downloaded_layer(
|
||||||
&self,
|
&self,
|
||||||
layer_map_lock_held_witness: &BatchedUpdates<'_, L>,
|
layer_map_lock_held_witness: &LayerManager,
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
file_size: u64,
|
file_size: u64,
|
||||||
) -> Arc<dyn PersistentLayer>
|
) -> Arc<dyn PersistentLayer> {
|
||||||
where
|
|
||||||
L: ?Sized + Layer,
|
|
||||||
{
|
|
||||||
if self.desc.is_delta {
|
if self.desc.is_delta {
|
||||||
let fname = self.desc.delta_file_name();
|
let fname = self.desc.delta_file_name();
|
||||||
Arc::new(DeltaLayer::new(
|
Arc::new(DeltaLayer::new(
|
||||||
|
|||||||
@@ -122,12 +122,12 @@ async fn compaction_loop(tenant: Arc<Tenant>, cancel: CancellationToken) {
|
|||||||
warn_when_period_overrun(started_at.elapsed(), period, "compaction");
|
warn_when_period_overrun(started_at.elapsed(), period, "compaction");
|
||||||
|
|
||||||
// Sleep
|
// Sleep
|
||||||
tokio::select! {
|
if tokio::time::timeout(sleep_duration, cancel.cancelled())
|
||||||
_ = cancel.cancelled() => {
|
.await
|
||||||
info!("received cancellation request during idling");
|
.is_ok()
|
||||||
break;
|
{
|
||||||
},
|
info!("received cancellation request during idling");
|
||||||
_ = tokio::time::sleep(sleep_duration) => {},
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,12 +196,12 @@ async fn gc_loop(tenant: Arc<Tenant>, cancel: CancellationToken) {
|
|||||||
warn_when_period_overrun(started_at.elapsed(), period, "gc");
|
warn_when_period_overrun(started_at.elapsed(), period, "gc");
|
||||||
|
|
||||||
// Sleep
|
// Sleep
|
||||||
tokio::select! {
|
if tokio::time::timeout(sleep_duration, cancel.cancelled())
|
||||||
_ = cancel.cancelled() => {
|
.await
|
||||||
info!("received cancellation request during idling");
|
.is_ok()
|
||||||
break;
|
{
|
||||||
},
|
info!("received cancellation request during idling");
|
||||||
_ = tokio::time::sleep(sleep_duration) => {},
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,9 +263,9 @@ pub(crate) async fn random_init_delay(
|
|||||||
rng.gen_range(Duration::ZERO..=period)
|
rng.gen_range(Duration::ZERO..=period)
|
||||||
};
|
};
|
||||||
|
|
||||||
tokio::select! {
|
match tokio::time::timeout(d, cancel.cancelled()).await {
|
||||||
_ = cancel.cancelled() => Err(Cancelled),
|
Ok(_) => Err(Cancelled),
|
||||||
_ = tokio::time::sleep(d) => Ok(()),
|
Err(_) => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ use crate::{
|
|||||||
tenant::{
|
tenant::{
|
||||||
config::{EvictionPolicy, EvictionPolicyLayerAccessThreshold},
|
config::{EvictionPolicy, EvictionPolicyLayerAccessThreshold},
|
||||||
storage_layer::PersistentLayer,
|
storage_layer::PersistentLayer,
|
||||||
|
timeline::EvictionError,
|
||||||
LogicalSizeCalculationCause, Tenant,
|
LogicalSizeCalculationCause, Tenant,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -70,7 +71,6 @@ impl Timeline {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self_clone.eviction_task(cancel).await;
|
self_clone.eviction_task(cancel).await;
|
||||||
info!("eviction task finishing");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -78,6 +78,9 @@ impl Timeline {
|
|||||||
|
|
||||||
#[instrument(skip_all, fields(tenant_id = %self.tenant_id, timeline_id = %self.timeline_id))]
|
#[instrument(skip_all, fields(tenant_id = %self.tenant_id, timeline_id = %self.timeline_id))]
|
||||||
async fn eviction_task(self: Arc<Self>, cancel: CancellationToken) {
|
async fn eviction_task(self: Arc<Self>, cancel: CancellationToken) {
|
||||||
|
scopeguard::defer! {
|
||||||
|
info!("eviction task finishing");
|
||||||
|
}
|
||||||
use crate::tenant::tasks::random_init_delay;
|
use crate::tenant::tasks::random_init_delay;
|
||||||
{
|
{
|
||||||
let policy = self.get_eviction_policy();
|
let policy = self.get_eviction_policy();
|
||||||
@@ -86,7 +89,6 @@ impl Timeline {
|
|||||||
EvictionPolicy::NoEviction => Duration::from_secs(10),
|
EvictionPolicy::NoEviction => Duration::from_secs(10),
|
||||||
};
|
};
|
||||||
if random_init_delay(period, &cancel).await.is_err() {
|
if random_init_delay(period, &cancel).await.is_err() {
|
||||||
info!("shutting down");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,12 +101,11 @@ impl Timeline {
|
|||||||
match cf {
|
match cf {
|
||||||
ControlFlow::Break(()) => break,
|
ControlFlow::Break(()) => break,
|
||||||
ControlFlow::Continue(sleep_until) => {
|
ControlFlow::Continue(sleep_until) => {
|
||||||
tokio::select! {
|
if tokio::time::timeout_at(sleep_until, cancel.cancelled())
|
||||||
_ = cancel.cancelled() => {
|
.await
|
||||||
info!("shutting down");
|
.is_ok()
|
||||||
break;
|
{
|
||||||
}
|
break;
|
||||||
_ = tokio::time::sleep_until(sleep_until) => { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,9 +198,11 @@ impl Timeline {
|
|||||||
// We don't want to hold the layer map lock during eviction.
|
// We don't want to hold the layer map lock during eviction.
|
||||||
// So, we just need to deal with this.
|
// So, we just need to deal with this.
|
||||||
let candidates: Vec<Arc<dyn PersistentLayer>> = {
|
let candidates: Vec<Arc<dyn PersistentLayer>> = {
|
||||||
let layers = self.layers.read().await;
|
let guard = self.layers.read().await;
|
||||||
|
let layers = guard.layer_map();
|
||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
for hist_layer in layers.iter_historic_layers() {
|
for hist_layer in layers.iter_historic_layers() {
|
||||||
|
let hist_layer = guard.get_from_desc(&hist_layer);
|
||||||
if hist_layer.is_remote_layer() {
|
if hist_layer.is_remote_layer() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -207,7 +210,7 @@ impl Timeline {
|
|||||||
let last_activity_ts = hist_layer.access_stats().latest_activity().unwrap_or_else(|| {
|
let last_activity_ts = hist_layer.access_stats().latest_activity().unwrap_or_else(|| {
|
||||||
// We only use this fallback if there's an implementation error.
|
// We only use this fallback if there's an implementation error.
|
||||||
// `latest_activity` already does rate-limited warn!() log.
|
// `latest_activity` already does rate-limited warn!() log.
|
||||||
debug!(layer=%hist_layer.filename().file_name(), "last_activity returns None, using SystemTime::now");
|
debug!(layer=%hist_layer, "last_activity returns None, using SystemTime::now");
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -268,20 +271,22 @@ impl Timeline {
|
|||||||
None => {
|
None => {
|
||||||
stats.skipped_for_shutdown += 1;
|
stats.skipped_for_shutdown += 1;
|
||||||
}
|
}
|
||||||
Some(Ok(true)) => {
|
Some(Ok(())) => {
|
||||||
debug!("evicted layer {l:?}");
|
|
||||||
stats.evicted += 1;
|
stats.evicted += 1;
|
||||||
}
|
}
|
||||||
Some(Ok(false)) => {
|
Some(Err(EvictionError::CannotEvictRemoteLayer)) => {
|
||||||
debug!("layer is not evictable: {l:?}");
|
|
||||||
stats.not_evictable += 1;
|
stats.not_evictable += 1;
|
||||||
}
|
}
|
||||||
Some(Err(e)) => {
|
Some(Err(EvictionError::FileNotFound)) => {
|
||||||
// This variant is the case where an unexpected error happened during eviction.
|
// compaction/gc removed the file while we were waiting on layer_removal_cs
|
||||||
// Expected errors that result in non-eviction are `Some(Ok(false))`.
|
stats.not_evictable += 1;
|
||||||
// So, dump Debug here to gather as much info as possible in this rare case.
|
}
|
||||||
warn!("failed to evict layer {l:?}: {e:?}");
|
Some(Err(
|
||||||
stats.errors += 1;
|
e @ EvictionError::LayerNotFound(_) | e @ EvictionError::StatFailed(_),
|
||||||
|
)) => {
|
||||||
|
let e = utils::error::report_compact_sources(&e);
|
||||||
|
warn!(layer = %l, "failed to evict layer: {e}");
|
||||||
|
stats.not_evictable += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
391
pageserver/src/tenant/timeline/layer_manager.rs
Normal file
391
pageserver/src/tenant/timeline/layer_manager.rs
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
use anyhow::{bail, ensure, Context, Result};
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
use tracing::trace;
|
||||||
|
use utils::{
|
||||||
|
id::{TenantId, TimelineId},
|
||||||
|
lsn::{AtomicLsn, Lsn},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::PageServerConf,
|
||||||
|
metrics::TimelineMetrics,
|
||||||
|
tenant::{
|
||||||
|
layer_map::{BatchedUpdates, LayerMap},
|
||||||
|
storage_layer::{
|
||||||
|
AsLayerDesc, DeltaLayer, ImageLayer, InMemoryLayer, Layer, PersistentLayer,
|
||||||
|
PersistentLayerDesc, PersistentLayerKey, RemoteLayer,
|
||||||
|
},
|
||||||
|
timeline::compare_arced_layers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Provides semantic APIs to manipulate the layer map.
|
||||||
|
pub struct LayerManager {
|
||||||
|
layer_map: LayerMap,
|
||||||
|
layer_fmgr: LayerFileManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// After GC, the layer map changes will not be applied immediately. Users should manually apply the changes after
|
||||||
|
/// scheduling deletes in remote client.
|
||||||
|
pub struct ApplyGcResultGuard<'a>(BatchedUpdates<'a>);
|
||||||
|
|
||||||
|
impl ApplyGcResultGuard<'_> {
|
||||||
|
pub fn flush(self) {
|
||||||
|
self.0.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayerManager {
|
||||||
|
pub fn create() -> Self {
|
||||||
|
Self {
|
||||||
|
layer_map: LayerMap::default(),
|
||||||
|
layer_fmgr: LayerFileManager::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_from_desc(&self, desc: &PersistentLayerDesc) -> Arc<dyn PersistentLayer> {
|
||||||
|
self.layer_fmgr.get_from_desc(desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an immutable reference to the layer map.
|
||||||
|
///
|
||||||
|
/// We expect users only to be able to get an immutable layer map. If users want to make modifications,
|
||||||
|
/// they should use the below semantic APIs. This design makes us step closer to immutable storage state.
|
||||||
|
pub fn layer_map(&self) -> &LayerMap {
|
||||||
|
&self.layer_map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a mutable reference to the layer map. This function will be removed once `flush_frozen_layer`
|
||||||
|
/// gets a refactor.
|
||||||
|
pub fn layer_map_mut(&mut self) -> &mut LayerMap {
|
||||||
|
&mut self.layer_map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace layers in the layer file manager, used in evictions and layer downloads.
|
||||||
|
pub fn replace_and_verify(
|
||||||
|
&mut self,
|
||||||
|
expected: Arc<dyn PersistentLayer>,
|
||||||
|
new: Arc<dyn PersistentLayer>,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.layer_fmgr.replace_and_verify(expected, new)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called from `load_layer_map`. Initialize the layer manager with:
|
||||||
|
/// 1. all on-disk layers
|
||||||
|
/// 2. next open layer (with disk disk_consistent_lsn LSN)
|
||||||
|
pub fn initialize_local_layers(
|
||||||
|
&mut self,
|
||||||
|
on_disk_layers: Vec<Arc<dyn PersistentLayer>>,
|
||||||
|
next_open_layer_at: Lsn,
|
||||||
|
) {
|
||||||
|
let mut updates = self.layer_map.batch_update();
|
||||||
|
for layer in on_disk_layers {
|
||||||
|
Self::insert_historic_layer(layer, &mut updates, &mut self.layer_fmgr);
|
||||||
|
}
|
||||||
|
updates.flush();
|
||||||
|
self.layer_map.next_open_layer_at = Some(next_open_layer_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize when creating a new timeline, called in `init_empty_layer_map`.
|
||||||
|
pub fn initialize_empty(&mut self, next_open_layer_at: Lsn) {
|
||||||
|
self.layer_map.next_open_layer_at = Some(next_open_layer_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initialize_remote_layers(
|
||||||
|
&mut self,
|
||||||
|
corrupted_local_layers: Vec<Arc<dyn PersistentLayer>>,
|
||||||
|
remote_layers: Vec<Arc<RemoteLayer>>,
|
||||||
|
) {
|
||||||
|
let mut updates = self.layer_map.batch_update();
|
||||||
|
for layer in corrupted_local_layers {
|
||||||
|
Self::remove_historic_layer(layer, &mut updates, &mut self.layer_fmgr);
|
||||||
|
}
|
||||||
|
for layer in remote_layers {
|
||||||
|
Self::insert_historic_layer(layer, &mut updates, &mut self.layer_fmgr);
|
||||||
|
}
|
||||||
|
updates.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a new writable layer to append data if there is no open layer, otherwise return the current open layer,
|
||||||
|
/// called within `get_layer_for_write`.
|
||||||
|
pub fn get_layer_for_write(
|
||||||
|
&mut self,
|
||||||
|
lsn: Lsn,
|
||||||
|
last_record_lsn: Lsn,
|
||||||
|
conf: &'static PageServerConf,
|
||||||
|
timeline_id: TimelineId,
|
||||||
|
tenant_id: TenantId,
|
||||||
|
) -> Result<Arc<InMemoryLayer>> {
|
||||||
|
ensure!(lsn.is_aligned());
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
lsn > last_record_lsn,
|
||||||
|
"cannot modify relation after advancing last_record_lsn (incoming_lsn={}, last_record_lsn={})\n{}",
|
||||||
|
lsn,
|
||||||
|
last_record_lsn,
|
||||||
|
std::backtrace::Backtrace::force_capture(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Do we have a layer open for writing already?
|
||||||
|
let layer = if let Some(open_layer) = &self.layer_map.open_layer {
|
||||||
|
if open_layer.get_lsn_range().start > lsn {
|
||||||
|
bail!(
|
||||||
|
"unexpected open layer in the future: open layers starts at {}, write lsn {}",
|
||||||
|
open_layer.get_lsn_range().start,
|
||||||
|
lsn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Arc::clone(open_layer)
|
||||||
|
} else {
|
||||||
|
// No writeable layer yet. Create one.
|
||||||
|
let start_lsn = self
|
||||||
|
.layer_map
|
||||||
|
.next_open_layer_at
|
||||||
|
.context("No next open layer found")?;
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"creating in-memory layer at {}/{} for record at {}",
|
||||||
|
timeline_id,
|
||||||
|
start_lsn,
|
||||||
|
lsn
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_layer = InMemoryLayer::create(conf, timeline_id, tenant_id, start_lsn)?;
|
||||||
|
let layer = Arc::new(new_layer);
|
||||||
|
|
||||||
|
self.layer_map.open_layer = Some(layer.clone());
|
||||||
|
self.layer_map.next_open_layer_at = None;
|
||||||
|
|
||||||
|
layer
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called from `freeze_inmem_layer`, returns true if successfully frozen.
|
||||||
|
pub fn try_freeze_in_memory_layer(
|
||||||
|
&mut self,
|
||||||
|
Lsn(last_record_lsn): Lsn,
|
||||||
|
last_freeze_at: &AtomicLsn,
|
||||||
|
) {
|
||||||
|
let end_lsn = Lsn(last_record_lsn + 1);
|
||||||
|
|
||||||
|
if let Some(open_layer) = &self.layer_map.open_layer {
|
||||||
|
let open_layer_rc = Arc::clone(open_layer);
|
||||||
|
// Does this layer need freezing?
|
||||||
|
open_layer.freeze(end_lsn);
|
||||||
|
|
||||||
|
// The layer is no longer open, update the layer map to reflect this.
|
||||||
|
// We will replace it with on-disk historics below.
|
||||||
|
self.layer_map.frozen_layers.push_back(open_layer_rc);
|
||||||
|
self.layer_map.open_layer = None;
|
||||||
|
self.layer_map.next_open_layer_at = Some(end_lsn);
|
||||||
|
last_freeze_at.store(end_lsn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add image layers to the layer map, called from `create_image_layers`.
|
||||||
|
pub fn track_new_image_layers(&mut self, image_layers: Vec<ImageLayer>) {
|
||||||
|
let mut updates = self.layer_map.batch_update();
|
||||||
|
for layer in image_layers {
|
||||||
|
Self::insert_historic_layer(Arc::new(layer), &mut updates, &mut self.layer_fmgr);
|
||||||
|
}
|
||||||
|
updates.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flush a frozen layer and add the written delta layer to the layer map.
|
||||||
|
pub fn finish_flush_l0_layer(
|
||||||
|
&mut self,
|
||||||
|
delta_layer: Option<DeltaLayer>,
|
||||||
|
frozen_layer_for_check: &Arc<InMemoryLayer>,
|
||||||
|
) {
|
||||||
|
let l = self.layer_map.frozen_layers.pop_front();
|
||||||
|
let mut updates = self.layer_map.batch_update();
|
||||||
|
|
||||||
|
// Only one thread may call this function at a time (for this
|
||||||
|
// timeline). If two threads tried to flush the same frozen
|
||||||
|
// layer to disk at the same time, that would not work.
|
||||||
|
assert!(compare_arced_layers(&l.unwrap(), frozen_layer_for_check));
|
||||||
|
|
||||||
|
if let Some(delta_layer) = delta_layer {
|
||||||
|
Self::insert_historic_layer(Arc::new(delta_layer), &mut updates, &mut self.layer_fmgr);
|
||||||
|
}
|
||||||
|
updates.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when compaction is completed.
|
||||||
|
pub fn finish_compact_l0(
|
||||||
|
&mut self,
|
||||||
|
layer_removal_cs: Arc<tokio::sync::OwnedMutexGuard<()>>,
|
||||||
|
compact_from: Vec<Arc<dyn PersistentLayer>>,
|
||||||
|
compact_to: Vec<Arc<dyn PersistentLayer>>,
|
||||||
|
metrics: &TimelineMetrics,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut updates = self.layer_map.batch_update();
|
||||||
|
for l in compact_to {
|
||||||
|
Self::insert_historic_layer(l, &mut updates, &mut self.layer_fmgr);
|
||||||
|
}
|
||||||
|
for l in compact_from {
|
||||||
|
// NB: the layer file identified by descriptor `l` is guaranteed to be present
|
||||||
|
// in the LayerFileManager because compaction kept holding `layer_removal_cs` the entire
|
||||||
|
// time, even though we dropped `Timeline::layers` inbetween.
|
||||||
|
Self::delete_historic_layer(
|
||||||
|
layer_removal_cs.clone(),
|
||||||
|
l,
|
||||||
|
&mut updates,
|
||||||
|
metrics,
|
||||||
|
&mut self.layer_fmgr,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
updates.flush();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when garbage collect the timeline. Returns a guard that will apply the updates to the layer map.
|
||||||
|
pub fn finish_gc_timeline(
|
||||||
|
&mut self,
|
||||||
|
layer_removal_cs: Arc<tokio::sync::OwnedMutexGuard<()>>,
|
||||||
|
gc_layers: Vec<Arc<dyn PersistentLayer>>,
|
||||||
|
metrics: &TimelineMetrics,
|
||||||
|
) -> Result<ApplyGcResultGuard> {
|
||||||
|
let mut updates = self.layer_map.batch_update();
|
||||||
|
for doomed_layer in gc_layers {
|
||||||
|
Self::delete_historic_layer(
|
||||||
|
layer_removal_cs.clone(),
|
||||||
|
doomed_layer,
|
||||||
|
&mut updates,
|
||||||
|
metrics,
|
||||||
|
&mut self.layer_fmgr,
|
||||||
|
)?; // FIXME: schedule succeeded deletions in timeline.rs `gc_timeline` instead of in batch?
|
||||||
|
}
|
||||||
|
Ok(ApplyGcResultGuard(updates))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to insert a layer into the layer map and file manager.
|
||||||
|
fn insert_historic_layer(
|
||||||
|
layer: Arc<dyn PersistentLayer>,
|
||||||
|
updates: &mut BatchedUpdates<'_>,
|
||||||
|
mapping: &mut LayerFileManager,
|
||||||
|
) {
|
||||||
|
updates.insert_historic(layer.layer_desc().clone());
|
||||||
|
mapping.insert(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to remove a layer into the layer map and file manager
|
||||||
|
fn remove_historic_layer(
|
||||||
|
layer: Arc<dyn PersistentLayer>,
|
||||||
|
updates: &mut BatchedUpdates<'_>,
|
||||||
|
mapping: &mut LayerFileManager,
|
||||||
|
) {
|
||||||
|
updates.remove_historic(layer.layer_desc().clone());
|
||||||
|
mapping.remove(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the layer from local FS (if present) and from memory.
|
||||||
|
/// Remote storage is not affected by this operation.
|
||||||
|
fn delete_historic_layer(
|
||||||
|
// we cannot remove layers otherwise, since gc and compaction will race
|
||||||
|
_layer_removal_cs: Arc<tokio::sync::OwnedMutexGuard<()>>,
|
||||||
|
layer: Arc<dyn PersistentLayer>,
|
||||||
|
updates: &mut BatchedUpdates<'_>,
|
||||||
|
metrics: &TimelineMetrics,
|
||||||
|
mapping: &mut LayerFileManager,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if !layer.is_remote_layer() {
|
||||||
|
layer.delete_resident_layer_file()?;
|
||||||
|
let layer_file_size = layer.file_size();
|
||||||
|
metrics.resident_physical_size_gauge.sub(layer_file_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Removing from the bottom of the layer map is expensive.
|
||||||
|
// Maybe instead discard all layer map historic versions that
|
||||||
|
// won't be needed for page reconstruction for this timeline,
|
||||||
|
// and mark what we can't delete yet as deleted from the layer
|
||||||
|
// map index without actually rebuilding the index.
|
||||||
|
updates.remove_historic(layer.layer_desc().clone());
|
||||||
|
mapping.remove(layer);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn contains(&self, layer: &Arc<dyn PersistentLayer>) -> bool {
|
||||||
|
self.layer_fmgr.contains(layer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LayerFileManager<T: AsLayerDesc + ?Sized = dyn PersistentLayer>(
|
||||||
|
HashMap<PersistentLayerKey, Arc<T>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl<T: AsLayerDesc + ?Sized> LayerFileManager<T> {
|
||||||
|
fn get_from_desc(&self, desc: &PersistentLayerDesc) -> Arc<T> {
|
||||||
|
// The assumption for the `expect()` is that all code maintains the following invariant:
|
||||||
|
// A layer's descriptor is present in the LayerMap => the LayerFileManager contains a layer for the descriptor.
|
||||||
|
self.0
|
||||||
|
.get(&desc.key())
|
||||||
|
.with_context(|| format!("get layer from desc: {}", desc.filename()))
|
||||||
|
.expect("not found")
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn insert(&mut self, layer: Arc<T>) {
|
||||||
|
let present = self.0.insert(layer.layer_desc().key(), layer.clone());
|
||||||
|
if present.is_some() && cfg!(debug_assertions) {
|
||||||
|
panic!("overwriting a layer: {:?}", layer.layer_desc())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn contains(&self, layer: &Arc<T>) -> bool {
|
||||||
|
self.0.contains_key(&layer.layer_desc().key())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self(HashMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove(&mut self, layer: Arc<T>) {
|
||||||
|
let present = self.0.remove(&layer.layer_desc().key());
|
||||||
|
if present.is_none() && cfg!(debug_assertions) {
|
||||||
|
panic!(
|
||||||
|
"removing layer that is not present in layer mapping: {:?}",
|
||||||
|
layer.layer_desc()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn replace_and_verify(&mut self, expected: Arc<T>, new: Arc<T>) -> Result<()> {
|
||||||
|
let key = expected.layer_desc().key();
|
||||||
|
let other = new.layer_desc().key();
|
||||||
|
|
||||||
|
let expected_l0 = LayerMap::is_l0(expected.layer_desc());
|
||||||
|
let new_l0 = LayerMap::is_l0(new.layer_desc());
|
||||||
|
|
||||||
|
fail::fail_point!("layermap-replace-notfound", |_| anyhow::bail!(
|
||||||
|
"layermap-replace-notfound"
|
||||||
|
));
|
||||||
|
|
||||||
|
anyhow::ensure!(
|
||||||
|
key == other,
|
||||||
|
"expected and new layer have different keys: {key:?} != {other:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
anyhow::ensure!(
|
||||||
|
expected_l0 == new_l0,
|
||||||
|
"one layer is l0 while the other is not: {expected_l0} != {new_l0}"
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(layer) = self.0.get_mut(&key) {
|
||||||
|
anyhow::ensure!(
|
||||||
|
compare_arced_layers(&expected, layer),
|
||||||
|
"another layer was found instead of expected, expected={expected:?}, new={new:?}",
|
||||||
|
expected = Arc::as_ptr(&expected),
|
||||||
|
new = Arc::as_ptr(layer),
|
||||||
|
);
|
||||||
|
*layer = new;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("layer was not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
128
pageserver/src/tenant/timeline/logical_size.rs
Normal file
128
pageserver/src/tenant/timeline/logical_size.rs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
|
use tokio::sync::Semaphore;
|
||||||
|
use utils::lsn::Lsn;
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicI64, Ordering as AtomicOrdering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Internal structure to hold all data needed for logical size calculation.
|
||||||
|
///
|
||||||
|
/// Calculation consists of two stages:
|
||||||
|
///
|
||||||
|
/// 1. Initial size calculation. That might take a long time, because it requires
|
||||||
|
/// reading all layers containing relation sizes at `initial_part_end`.
|
||||||
|
///
|
||||||
|
/// 2. Collecting an incremental part and adding that to the initial size.
|
||||||
|
/// Increments are appended on walreceiver writing new timeline data,
|
||||||
|
/// which result in increase or decrease of the logical size.
|
||||||
|
pub(super) struct LogicalSize {
|
||||||
|
/// Size, potentially slow to compute. Calculating this might require reading multiple
|
||||||
|
/// layers, and even ancestor's layers.
|
||||||
|
///
|
||||||
|
/// NOTE: size at a given LSN is constant, but after a restart we will calculate
|
||||||
|
/// the initial size at a different LSN.
|
||||||
|
pub initial_logical_size: OnceCell<u64>,
|
||||||
|
|
||||||
|
/// Semaphore to track ongoing calculation of `initial_logical_size`.
|
||||||
|
pub initial_size_computation: Arc<tokio::sync::Semaphore>,
|
||||||
|
|
||||||
|
/// Latest Lsn that has its size uncalculated, could be absent for freshly created timelines.
|
||||||
|
pub initial_part_end: Option<Lsn>,
|
||||||
|
|
||||||
|
/// All other size changes after startup, combined together.
|
||||||
|
///
|
||||||
|
/// Size shouldn't ever be negative, but this is signed for two reasons:
|
||||||
|
///
|
||||||
|
/// 1. If we initialized the "baseline" size lazily, while we already
|
||||||
|
/// process incoming WAL, the incoming WAL records could decrement the
|
||||||
|
/// variable and temporarily make it negative. (This is just future-proofing;
|
||||||
|
/// the initialization is currently not done lazily.)
|
||||||
|
///
|
||||||
|
/// 2. If there is a bug and we e.g. forget to increment it in some cases
|
||||||
|
/// when size grows, but remember to decrement it when it shrinks again, the
|
||||||
|
/// variable could go negative. In that case, it seems better to at least
|
||||||
|
/// try to keep tracking it, rather than clamp or overflow it. Note that
|
||||||
|
/// get_current_logical_size() will clamp the returned value to zero if it's
|
||||||
|
/// negative, and log an error. Could set it permanently to zero or some
|
||||||
|
/// special value to indicate "broken" instead, but this will do for now.
|
||||||
|
///
|
||||||
|
/// Note that we also expose a copy of this value as a prometheus metric,
|
||||||
|
/// see `current_logical_size_gauge`. Use the `update_current_logical_size`
|
||||||
|
/// to modify this, it will also keep the prometheus metric in sync.
|
||||||
|
pub size_added_after_initial: AtomicI64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normalized current size, that the data in pageserver occupies.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(super) enum CurrentLogicalSize {
|
||||||
|
/// The size is not yet calculated to the end, this is an intermediate result,
|
||||||
|
/// constructed from walreceiver increments and normalized: logical data could delete some objects, hence be negative,
|
||||||
|
/// yet total logical size cannot be below 0.
|
||||||
|
Approximate(u64),
|
||||||
|
// Fully calculated logical size, only other future walreceiver increments are changing it, and those changes are
|
||||||
|
// available for observation without any calculations.
|
||||||
|
Exact(u64),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CurrentLogicalSize {
|
||||||
|
pub(super) fn size(&self) -> u64 {
|
||||||
|
*match self {
|
||||||
|
Self::Approximate(size) => size,
|
||||||
|
Self::Exact(size) => size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogicalSize {
|
||||||
|
pub(super) fn empty_initial() -> Self {
|
||||||
|
Self {
|
||||||
|
initial_logical_size: OnceCell::with_value(0),
|
||||||
|
// initial_logical_size already computed, so, don't admit any calculations
|
||||||
|
initial_size_computation: Arc::new(Semaphore::new(0)),
|
||||||
|
initial_part_end: None,
|
||||||
|
size_added_after_initial: AtomicI64::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn deferred_initial(compute_to: Lsn) -> Self {
|
||||||
|
Self {
|
||||||
|
initial_logical_size: OnceCell::new(),
|
||||||
|
initial_size_computation: Arc::new(Semaphore::new(1)),
|
||||||
|
initial_part_end: Some(compute_to),
|
||||||
|
size_added_after_initial: AtomicI64::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn current_size(&self) -> anyhow::Result<CurrentLogicalSize> {
|
||||||
|
let size_increment: i64 = self.size_added_after_initial.load(AtomicOrdering::Acquire);
|
||||||
|
// ^^^ keep this type explicit so that the casts in this function break if
|
||||||
|
// we change the type.
|
||||||
|
match self.initial_logical_size.get() {
|
||||||
|
Some(initial_size) => {
|
||||||
|
initial_size.checked_add_signed(size_increment)
|
||||||
|
.with_context(|| format!("Overflow during logical size calculation, initial_size: {initial_size}, size_increment: {size_increment}"))
|
||||||
|
.map(CurrentLogicalSize::Exact)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let non_negative_size_increment = u64::try_from(size_increment).unwrap_or(0);
|
||||||
|
Ok(CurrentLogicalSize::Approximate(non_negative_size_increment))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn increment_size(&self, delta: i64) {
|
||||||
|
self.size_added_after_initial
|
||||||
|
.fetch_add(delta, AtomicOrdering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make the value computed by initial logical size computation
|
||||||
|
/// available for re-use. This doesn't contain the incremental part.
|
||||||
|
pub(super) fn initialized_size(&self, lsn: Lsn) -> Option<u64> {
|
||||||
|
match self.initial_part_end {
|
||||||
|
Some(v) if v == lsn => self.initial_logical_size.get().copied(),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
pageserver/src/tenant/timeline/span.rs
Normal file
20
pageserver/src/tenant/timeline/span.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#[cfg(debug_assertions)]
|
||||||
|
use utils::tracing_span_assert::{check_fields_present, Extractor, MultiNameExtractor};
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
pub(crate) fn debug_assert_current_span_has_tenant_and_timeline_id() {}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
#[track_caller]
|
||||||
|
pub(crate) fn debug_assert_current_span_has_tenant_and_timeline_id() {
|
||||||
|
static TIMELINE_ID_EXTRACTOR: once_cell::sync::Lazy<MultiNameExtractor<1>> =
|
||||||
|
once_cell::sync::Lazy::new(|| MultiNameExtractor::new("TimelineId", ["timeline_id"]));
|
||||||
|
|
||||||
|
let fields: [&dyn Extractor; 2] = [
|
||||||
|
&*crate::tenant::span::TENANT_ID_EXTRACTOR,
|
||||||
|
&*TIMELINE_ID_EXTRACTOR,
|
||||||
|
];
|
||||||
|
if let Err(missing) = check_fields_present!(fields) {
|
||||||
|
panic!("missing extractors: {missing:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user