mirror of
https://github.com/neondatabase/neon.git
synced 2026-08-16 19:18:22 +00:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c78a5067f | |||
| 108f08f982 | |||
| 5700233a47 | |||
| 1d66ca79a9 | |||
| 23827c6b0d | |||
| 66b0bf41a1 | |||
| 89cf8df93b | |||
| 54a06de4b5 | |||
| 6f20a18e8e | |||
| d557002675 | |||
| 32b75e7c73 | |||
| d2753719e3 | |||
| 04b2ac3fed | |||
| c39d5b03e8 | |||
| 76fc3d4aa1 | |||
| dd3adc3693 | |||
| 5b871802fd | |||
| 24ce73ffaf | |||
| 3118c24521 | |||
| 5af9660b9e | |||
| d7e349d33c | |||
| 47e5bf3bbb | |||
| 5d2f9ffa89 | |||
| fdadd6a152 | |||
| 9b623d3a2c | |||
| 9b98823d61 | |||
| 76864e6a2a | |||
| 6c5d3b5263 | |||
| cd9a550d97 | |||
| 07f21dd6b6 | |||
| 64a4461191 | |||
| 961fc0ba8f | |||
| 9b2f9419d9 | |||
| 947f6da75e | |||
| 7026dde9eb | |||
| d502313841 | |||
| 219e78f885 | |||
| 1ea5d8b132 | |||
| 3d760938e1 | |||
| 9211de0df7 | |||
| d8ffe662a9 | |||
| a4db2af1f0 | |||
| 47fdf93cf0 | |||
| de05f90735 | |||
| 188797f048 | |||
| 5446e08891 | |||
| 78d9059fc7 |
@@ -183,7 +183,7 @@ runs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
path: ~/.cache/pypoetry/virtualenvs
|
||||||
key: v2-${{ runner.os }}-python-deps-${{ hashFiles('poetry.lock') }}
|
key: v2-${{ runner.os }}-${{ runner.arch }}-python-deps-${{ hashFiles('poetry.lock') }}
|
||||||
|
|
||||||
- name: Store Allure test stat in the DB (new)
|
- name: Store Allure test stat in the DB (new)
|
||||||
if: ${{ !cancelled() && inputs.store-test-results-into-db == 'true' }}
|
if: ${{ !cancelled() && inputs.store-test-results-into-db == 'true' }}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ runs:
|
|||||||
TARGET: ${{ inputs.path }}
|
TARGET: ${{ inputs.path }}
|
||||||
ARCHIVE: /tmp/downloads/${{ inputs.name }}.tar.zst
|
ARCHIVE: /tmp/downloads/${{ inputs.name }}.tar.zst
|
||||||
SKIP_IF_DOES_NOT_EXIST: ${{ inputs.skip-if-does-not-exist }}
|
SKIP_IF_DOES_NOT_EXIST: ${{ inputs.skip-if-does-not-exist }}
|
||||||
PREFIX: artifacts/${{ inputs.prefix || format('{0}/{1}', github.run_id, github.run_attempt) }}
|
PREFIX: artifacts/${{ inputs.prefix || format('{0}/{1}/{2}', github.event.pull_request.head.sha || github.sha, github.run_id, github.run_attempt) }}
|
||||||
run: |
|
run: |
|
||||||
BUCKET=neon-github-public-dev
|
BUCKET=neon-github-public-dev
|
||||||
FILENAME=$(basename $ARCHIVE)
|
FILENAME=$(basename $ARCHIVE)
|
||||||
|
|||||||
@@ -56,14 +56,14 @@ runs:
|
|||||||
if: inputs.build_type != 'remote'
|
if: inputs.build_type != 'remote'
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-${{ inputs.build_type }}-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-${{ inputs.build_type }}-artifact
|
||||||
path: /tmp/neon
|
path: /tmp/neon
|
||||||
|
|
||||||
- name: Download Neon binaries for the previous release
|
- name: Download Neon binaries for the previous release
|
||||||
if: inputs.build_type != 'remote'
|
if: inputs.build_type != 'remote'
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-${{ inputs.build_type }}-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-${{ inputs.build_type }}-artifact
|
||||||
path: /tmp/neon-previous
|
path: /tmp/neon-previous
|
||||||
prefix: latest
|
prefix: latest
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ runs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
path: ~/.cache/pypoetry/virtualenvs
|
||||||
key: v2-${{ runner.os }}-python-deps-${{ hashFiles('poetry.lock') }}
|
key: v2-${{ runner.os }}-${{ runner.arch }}-python-deps-${{ hashFiles('poetry.lock') }}
|
||||||
|
|
||||||
- name: Install Python deps
|
- name: Install Python deps
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ inputs:
|
|||||||
description: "A directory or file to upload"
|
description: "A directory or file to upload"
|
||||||
required: true
|
required: true
|
||||||
prefix:
|
prefix:
|
||||||
description: "S3 prefix. Default is '${GITHUB_RUN_ID}/${GITHUB_RUN_ATTEMPT}'"
|
description: "S3 prefix. Default is '${GITHUB_SHA}/${GITHUB_RUN_ID}/${GITHUB_RUN_ATTEMPT}'"
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
@@ -45,7 +45,7 @@ runs:
|
|||||||
env:
|
env:
|
||||||
SOURCE: ${{ inputs.path }}
|
SOURCE: ${{ inputs.path }}
|
||||||
ARCHIVE: /tmp/uploads/${{ inputs.name }}.tar.zst
|
ARCHIVE: /tmp/uploads/${{ inputs.name }}.tar.zst
|
||||||
PREFIX: artifacts/${{ inputs.prefix || format('{0}/{1}', github.run_id, github.run_attempt) }}
|
PREFIX: artifacts/${{ inputs.prefix || format('{0}/{1}/{2}', github.event.pull_request.head.sha || github.sha, github.run_id , github.run_attempt) }}
|
||||||
run: |
|
run: |
|
||||||
BUCKET=neon-github-public-dev
|
BUCKET=neon-github-public-dev
|
||||||
FILENAME=$(basename $ARCHIVE)
|
FILENAME=$(basename $ARCHIVE)
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
- name: Download Neon artifact
|
- name: Download Neon artifact
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-release-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-release-artifact
|
||||||
path: /tmp/neon/
|
path: /tmp/neon/
|
||||||
prefix: latest
|
prefix: latest
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ jobs:
|
|||||||
- name: Download Neon artifact
|
- name: Download Neon artifact
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-release-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-release-artifact
|
||||||
path: /tmp/neon/
|
path: /tmp/neon/
|
||||||
prefix: latest
|
prefix: latest
|
||||||
|
|
||||||
@@ -373,7 +373,7 @@ jobs:
|
|||||||
- name: Download Neon artifact
|
- name: Download Neon artifact
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-release-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-release-artifact
|
||||||
path: /tmp/neon/
|
path: /tmp/neon/
|
||||||
prefix: latest
|
prefix: latest
|
||||||
|
|
||||||
@@ -473,7 +473,7 @@ jobs:
|
|||||||
- name: Download Neon artifact
|
- name: Download Neon artifact
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-release-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-release-artifact
|
||||||
path: /tmp/neon/
|
path: /tmp/neon/
|
||||||
prefix: latest
|
prefix: latest
|
||||||
|
|
||||||
@@ -576,7 +576,7 @@ jobs:
|
|||||||
- name: Download Neon artifact
|
- name: Download Neon artifact
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-release-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-release-artifact
|
||||||
path: /tmp/neon/
|
path: /tmp/neon/
|
||||||
prefix: latest
|
prefix: latest
|
||||||
|
|
||||||
@@ -677,7 +677,7 @@ jobs:
|
|||||||
- name: Download Neon artifact
|
- name: Download Neon artifact
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-release-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-release-artifact
|
||||||
path: /tmp/neon/
|
path: /tmp/neon/
|
||||||
prefix: latest
|
prefix: latest
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
file: Dockerfile.build-tools
|
file: Dockerfile.build-tools
|
||||||
cache-from: type=registry,ref=neondatabase/build-tools:cache-${{ matrix.arch }}
|
cache-from: type=registry,ref=neondatabase/build-tools:cache-${{ matrix.arch }}
|
||||||
cache-to: type=registry,ref=neondatabase/build-tools:cache-${{ matrix.arch }},mode=max
|
cache-to: ${{ github.ref_name == 'main' && format('type=registry,ref=neondatabase/build-tools:cache-{0},mode=max', matrix.arch) || '' }}
|
||||||
tags: neondatabase/build-tools:${{ inputs.image-tag }}-${{ matrix.arch }}
|
tags: neondatabase/build-tools:${{ inputs.image-tag }}-${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Remove custom docker config directory
|
- name: Remove custom docker config directory
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
path: ~/.cache/pypoetry/virtualenvs
|
||||||
key: v2-${{ runner.os }}-python-deps-${{ hashFiles('poetry.lock') }}
|
key: v2-${{ runner.os }}-${{ runner.arch }}-python-deps-${{ hashFiles('poetry.lock') }}
|
||||||
|
|
||||||
- name: Install Python deps
|
- name: Install Python deps
|
||||||
run: ./scripts/pysync
|
run: ./scripts/pysync
|
||||||
@@ -149,7 +149,7 @@ jobs:
|
|||||||
# !~/.cargo/registry/src
|
# !~/.cargo/registry/src
|
||||||
# ~/.cargo/git/
|
# ~/.cargo/git/
|
||||||
# target/
|
# target/
|
||||||
# key: v1-${{ runner.os }}-cargo-clippy-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('Cargo.lock') }}
|
# key: v1-${{ runner.os }}-${{ runner.arch }}-cargo-clippy-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
# Some of our rust modules use FFI and need those to be checked
|
# Some of our rust modules use FFI and need those to be checked
|
||||||
- name: Get postgres headers
|
- name: Get postgres headers
|
||||||
@@ -291,29 +291,29 @@ jobs:
|
|||||||
# target/
|
# target/
|
||||||
# # Fall back to older versions of the key, if no cache for current Cargo.lock was found
|
# # Fall back to older versions of the key, if no cache for current Cargo.lock was found
|
||||||
# key: |
|
# key: |
|
||||||
# v1-${{ runner.os }}-${{ matrix.build_type }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('Cargo.lock') }}
|
# v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.build_type }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('Cargo.lock') }}
|
||||||
# v1-${{ runner.os }}-${{ matrix.build_type }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-
|
# v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.build_type }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-
|
||||||
|
|
||||||
- name: Cache postgres v14 build
|
- name: Cache postgres v14 build
|
||||||
id: cache_pg_14
|
id: cache_pg_14
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: pg_install/v14
|
path: pg_install/v14
|
||||||
key: v1-${{ runner.os }}-${{ matrix.build_type }}-pg-${{ steps.pg_v14_rev.outputs.pg_rev }}-${{ hashFiles('Makefile', 'Dockerfile.build-tools') }}
|
key: v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.build_type }}-pg-${{ steps.pg_v14_rev.outputs.pg_rev }}-${{ hashFiles('Makefile', 'Dockerfile.build-tools') }}
|
||||||
|
|
||||||
- name: Cache postgres v15 build
|
- name: Cache postgres v15 build
|
||||||
id: cache_pg_15
|
id: cache_pg_15
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: pg_install/v15
|
path: pg_install/v15
|
||||||
key: v1-${{ runner.os }}-${{ matrix.build_type }}-pg-${{ steps.pg_v15_rev.outputs.pg_rev }}-${{ hashFiles('Makefile', 'Dockerfile.build-tools') }}
|
key: v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.build_type }}-pg-${{ steps.pg_v15_rev.outputs.pg_rev }}-${{ hashFiles('Makefile', 'Dockerfile.build-tools') }}
|
||||||
|
|
||||||
- name: Cache postgres v16 build
|
- name: Cache postgres v16 build
|
||||||
id: cache_pg_16
|
id: cache_pg_16
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: pg_install/v16
|
path: pg_install/v16
|
||||||
key: v1-${{ runner.os }}-${{ matrix.build_type }}-pg-${{ steps.pg_v16_rev.outputs.pg_rev }}-${{ hashFiles('Makefile', 'Dockerfile.build-tools') }}
|
key: v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.build_type }}-pg-${{ steps.pg_v16_rev.outputs.pg_rev }}-${{ hashFiles('Makefile', 'Dockerfile.build-tools') }}
|
||||||
|
|
||||||
- name: Build postgres v14
|
- name: Build postgres v14
|
||||||
if: steps.cache_pg_14.outputs.cache-hit != 'true'
|
if: steps.cache_pg_14.outputs.cache-hit != 'true'
|
||||||
@@ -411,7 +411,7 @@ jobs:
|
|||||||
- name: Upload Neon artifact
|
- name: Upload Neon artifact
|
||||||
uses: ./.github/actions/upload
|
uses: ./.github/actions/upload
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-${{ matrix.build_type }}-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-${{ matrix.build_type }}-artifact
|
||||||
path: /tmp/neon
|
path: /tmp/neon
|
||||||
|
|
||||||
# XXX: keep this after the binaries.list is formed, so the coverage can properly work later
|
# XXX: keep this after the binaries.list is formed, so the coverage can properly work later
|
||||||
@@ -490,7 +490,7 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
path: ~/.cache/pypoetry/virtualenvs
|
||||||
key: v1-${{ runner.os }}-python-deps-${{ hashFiles('poetry.lock') }}
|
key: v1-${{ runner.os }}-${{ runner.arch }}-python-deps-${{ hashFiles('poetry.lock') }}
|
||||||
|
|
||||||
- name: Install Python deps
|
- name: Install Python deps
|
||||||
run: ./scripts/pysync
|
run: ./scripts/pysync
|
||||||
@@ -639,7 +639,7 @@ jobs:
|
|||||||
- name: Get Neon artifact
|
- name: Get Neon artifact
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-${{ matrix.build_type }}-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-${{ matrix.build_type }}-artifact
|
||||||
path: /tmp/neon
|
path: /tmp/neon
|
||||||
|
|
||||||
- name: Get coverage artifact
|
- name: Get coverage artifact
|
||||||
@@ -763,7 +763,7 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
cache-from: type=registry,ref=neondatabase/neon:cache-${{ matrix.arch }}
|
cache-from: type=registry,ref=neondatabase/neon:cache-${{ matrix.arch }}
|
||||||
cache-to: type=registry,ref=neondatabase/neon:cache-${{ matrix.arch }},mode=max
|
cache-to: ${{ github.ref_name == 'main' && format('type=registry,ref=neondatabase/neon:cache-{0},mode=max', matrix.arch) || '' }}
|
||||||
tags: |
|
tags: |
|
||||||
neondatabase/neon:${{ needs.tag.outputs.build-tag }}-${{ matrix.arch }}
|
neondatabase/neon:${{ needs.tag.outputs.build-tag }}-${{ matrix.arch }}
|
||||||
|
|
||||||
@@ -855,7 +855,7 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
file: Dockerfile.compute-node
|
file: Dockerfile.compute-node
|
||||||
cache-from: type=registry,ref=neondatabase/compute-node-${{ matrix.version }}:cache-${{ matrix.arch }}
|
cache-from: type=registry,ref=neondatabase/compute-node-${{ matrix.version }}:cache-${{ matrix.arch }}
|
||||||
cache-to: type=registry,ref=neondatabase/compute-node-${{ matrix.version }}:cache-${{ matrix.arch }},mode=max
|
cache-to: ${{ github.ref_name == 'main' && format('type=registry,ref=neondatabase/compute-node-{0}:cache-{1},mode=max', matrix.version, matrix.arch) || '' }}
|
||||||
tags: |
|
tags: |
|
||||||
neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}-${{ matrix.arch }}
|
neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}-${{ matrix.arch }}
|
||||||
|
|
||||||
@@ -875,7 +875,7 @@ jobs:
|
|||||||
file: Dockerfile.compute-node
|
file: Dockerfile.compute-node
|
||||||
target: neon-pg-ext-test
|
target: neon-pg-ext-test
|
||||||
cache-from: type=registry,ref=neondatabase/neon-test-extensions-${{ matrix.version }}:cache-${{ matrix.arch }}
|
cache-from: type=registry,ref=neondatabase/neon-test-extensions-${{ matrix.version }}:cache-${{ matrix.arch }}
|
||||||
cache-to: type=registry,ref=neondatabase/neon-test-extensions-${{ matrix.version }}:cache-${{ matrix.arch }},mode=max
|
cache-to: ${{ github.ref_name == 'main' && format('type=registry,ref=neondatabase/neon-test-extensions-{0}:cache-{1},mode=max', matrix.version, matrix.arch) || '' }}
|
||||||
tags: |
|
tags: |
|
||||||
neondatabase/neon-test-extensions-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}-${{ matrix.arch }}
|
neondatabase/neon-test-extensions-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}-${{ matrix.arch }}
|
||||||
|
|
||||||
@@ -1245,6 +1245,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
if [[ "$GITHUB_REF_NAME" == "main" ]]; then
|
if [[ "$GITHUB_REF_NAME" == "main" ]]; then
|
||||||
gh workflow --repo neondatabase/aws run deploy-dev.yml --ref main -f branch=main -f dockerTag=${{needs.tag.outputs.build-tag}} -f deployPreprodRegion=false
|
gh workflow --repo neondatabase/aws run deploy-dev.yml --ref main -f branch=main -f dockerTag=${{needs.tag.outputs.build-tag}} -f deployPreprodRegion=false
|
||||||
|
gh workflow --repo neondatabase/azure run deploy.yml -f dockerTag=${{needs.tag.outputs.build-tag}}
|
||||||
elif [[ "$GITHUB_REF_NAME" == "release" ]]; then
|
elif [[ "$GITHUB_REF_NAME" == "release" ]]; then
|
||||||
gh workflow --repo neondatabase/aws run deploy-dev.yml --ref main \
|
gh workflow --repo neondatabase/aws run deploy-dev.yml --ref main \
|
||||||
-f deployPgSniRouter=false \
|
-f deployPgSniRouter=false \
|
||||||
@@ -1339,7 +1340,7 @@ jobs:
|
|||||||
# Update Neon artifact for the release (reuse already uploaded artifact)
|
# Update Neon artifact for the release (reuse already uploaded artifact)
|
||||||
for build_type in debug release; do
|
for build_type in debug release; do
|
||||||
OLD_PREFIX=artifacts/${GITHUB_RUN_ID}
|
OLD_PREFIX=artifacts/${GITHUB_RUN_ID}
|
||||||
FILENAME=neon-${{ runner.os }}-${build_type}-artifact.tar.zst
|
FILENAME=neon-${{ runner.os }}-${{ runner.arch }}-${build_type}-artifact.tar.zst
|
||||||
|
|
||||||
S3_KEY=$(aws s3api list-objects-v2 --bucket ${BUCKET} --prefix ${OLD_PREFIX} | jq -r '.Contents[]?.Key' | grep ${FILENAME} | sort --version-sort | tail -1 || true)
|
S3_KEY=$(aws s3api list-objects-v2 --bucket ${BUCKET} --prefix ${OLD_PREFIX} | jq -r '.Contents[]?.Key' | grep ${FILENAME} | sort --version-sort | tail -1 || true)
|
||||||
if [ -z "${S3_KEY}" ]; then
|
if [ -z "${S3_KEY}" ]; then
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
path: ~/.cache/pypoetry/virtualenvs
|
||||||
key: v2-${{ runner.os }}-python-deps-ubunutu-latest-${{ hashFiles('poetry.lock') }}
|
key: v2-${{ runner.os }}-${{ runner.arch }}-python-deps-ubunutu-latest-${{ hashFiles('poetry.lock') }}
|
||||||
|
|
||||||
- name: Install Python deps
|
- name: Install Python deps
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
name: python-test-pg_clients-${{ runner.os }}-stage-logs
|
name: python-test-pg_clients-${{ runner.os }}-${{ runner.arch }}-stage-logs
|
||||||
path: ${{ env.TEST_OUTPUT }}
|
path: ${{ env.TEST_OUTPUT }}
|
||||||
|
|
||||||
- name: Post to a Slack channel
|
- name: Post to a Slack channel
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
# * `-A unknown_lints` – do not warn about unknown lint suppressions
|
# * `-A unknown_lints` – do not warn about unknown lint suppressions
|
||||||
# that people with newer toolchains might use
|
# that people with newer toolchains might use
|
||||||
# * `-D warnings` - fail on any warnings (`cargo` returns non-zero exit status)
|
# * `-D warnings` - fail on any warnings (`cargo` returns non-zero exit status)
|
||||||
export CLIPPY_COMMON_ARGS="--locked --workspace --all-targets -- -A unknown_lints -D warnings"
|
# * `-D clippy::todo` - don't let `todo!()` slip into `main`
|
||||||
|
export CLIPPY_COMMON_ARGS="--locked --workspace --all-targets -- -A unknown_lints -D warnings -D clippy::todo"
|
||||||
|
|||||||
Generated
+80
-31
@@ -1246,7 +1246,7 @@ dependencies = [
|
|||||||
"tokio-postgres",
|
"tokio-postgres",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml_edit",
|
"toml_edit 0.19.10",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-opentelemetry",
|
"tracing-opentelemetry",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -1362,8 +1362,8 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-postgres",
|
"tokio-postgres",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml",
|
"toml 0.7.4",
|
||||||
"toml_edit",
|
"toml_edit 0.19.10",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"utils",
|
"utils",
|
||||||
@@ -1669,9 +1669,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diesel"
|
name = "diesel"
|
||||||
version = "2.1.4"
|
version = "2.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62c6fcf842f17f8c78ecf7c81d75c5ce84436b41ee07e03f490fbb5f5a8731d8"
|
checksum = "62d6dcd069e7b5fe49a302411f759d4cf1cf2c27fe798ef46fb8baefc053dd2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.4.1",
|
"bitflags 2.4.1",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
@@ -1684,11 +1684,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diesel_derives"
|
name = "diesel_derives"
|
||||||
version = "2.1.2"
|
version = "2.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef8337737574f55a468005a83499da720f20c65586241ffea339db9ecdfd2b44"
|
checksum = "59de76a222c2b8059f789cbe07afbfd8deb8c31dd0bc2a21f85e256c1def8259"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"diesel_table_macro_syntax",
|
"diesel_table_macro_syntax",
|
||||||
|
"dsl_auto_type",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.52",
|
"syn 2.0.52",
|
||||||
@@ -1696,9 +1697,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diesel_migrations"
|
name = "diesel_migrations"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac"
|
checksum = "8a73ce704bad4231f001bff3314d91dce4aba0770cee8b233991859abc15c1f6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"diesel",
|
"diesel",
|
||||||
"migrations_internals",
|
"migrations_internals",
|
||||||
@@ -1707,9 +1708,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diesel_table_macro_syntax"
|
name = "diesel_table_macro_syntax"
|
||||||
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 = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5"
|
checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"syn 2.0.52",
|
"syn 2.0.52",
|
||||||
]
|
]
|
||||||
@@ -1745,6 +1746,20 @@ dependencies = [
|
|||||||
"const-random",
|
"const-random",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dsl_auto_type"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0892a17df262a24294c382f0d5997571006e7a4348b4327557c4ff1cd4a8bccc"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"either",
|
||||||
|
"heck 0.5.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.52",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dyn-clone"
|
name = "dyn-clone"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
@@ -3084,19 +3099,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "migrations_internals"
|
name = "migrations_internals"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada"
|
checksum = "fd01039851e82f8799046eabbb354056283fb265c8ec0996af940f4e85a380ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"toml",
|
"toml 0.8.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "migrations_macros"
|
name = "migrations_macros"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08"
|
checksum = "ffb161cc72176cb37aa47f1fc520d3ef02263d67d661f44f05d05a079e1237fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"migrations_internals",
|
"migrations_internals",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -3576,7 +3591,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml_edit",
|
"toml_edit 0.19.10",
|
||||||
"utils",
|
"utils",
|
||||||
"workspace_hack",
|
"workspace_hack",
|
||||||
]
|
]
|
||||||
@@ -3659,7 +3674,7 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-tar",
|
"tokio-tar",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml_edit",
|
"toml_edit 0.19.10",
|
||||||
"tracing",
|
"tracing",
|
||||||
"twox-hash",
|
"twox-hash",
|
||||||
"url",
|
"url",
|
||||||
@@ -4005,7 +4020,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "postgres"
|
name = "postgres"
|
||||||
version = "0.19.4"
|
version = "0.19.4"
|
||||||
source = "git+https://github.com/neondatabase/rust-postgres.git?branch=neon#20031d7a9ee1addeae6e0968e3899ae6bf01cee2"
|
source = "git+https://github.com/neondatabase/rust-postgres.git?branch=neon#cff6927e4f58b1af6ecc2ee7279df1f2ff537295"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
@@ -4018,7 +4033,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "postgres-protocol"
|
name = "postgres-protocol"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
source = "git+https://github.com/neondatabase/rust-postgres.git?branch=neon#20031d7a9ee1addeae6e0968e3899ae6bf01cee2"
|
source = "git+https://github.com/neondatabase/rust-postgres.git?branch=neon#cff6927e4f58b1af6ecc2ee7279df1f2ff537295"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.20.0",
|
"base64 0.20.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
@@ -4037,7 +4052,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "postgres-types"
|
name = "postgres-types"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
source = "git+https://github.com/neondatabase/rust-postgres.git?branch=neon#20031d7a9ee1addeae6e0968e3899ae6bf01cee2"
|
source = "git+https://github.com/neondatabase/rust-postgres.git?branch=neon#cff6927e4f58b1af6ecc2ee7279df1f2ff537295"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
@@ -4665,7 +4680,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml_edit",
|
"toml_edit 0.19.10",
|
||||||
"tracing",
|
"tracing",
|
||||||
"utils",
|
"utils",
|
||||||
"workspace_hack",
|
"workspace_hack",
|
||||||
@@ -5164,7 +5179,7 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-tar",
|
"tokio-tar",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml_edit",
|
"toml_edit 0.19.10",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"url",
|
"url",
|
||||||
@@ -5443,9 +5458,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "0.6.2"
|
version = "0.6.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
|
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -6210,7 +6225,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-postgres"
|
name = "tokio-postgres"
|
||||||
version = "0.7.7"
|
version = "0.7.7"
|
||||||
source = "git+https://github.com/neondatabase/rust-postgres.git?branch=neon#20031d7a9ee1addeae6e0968e3899ae6bf01cee2"
|
source = "git+https://github.com/neondatabase/rust-postgres.git?branch=neon#cff6927e4f58b1af6ecc2ee7279df1f2ff537295"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
@@ -6330,14 +6345,26 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"toml_edit",
|
"toml_edit 0.19.10",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit 0.22.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.6.2"
|
version = "0.6.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
|
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -6352,7 +6379,20 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"winnow",
|
"winnow 0.4.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap 2.0.1",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"winnow 0.6.13",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7335,6 +7375,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.6.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winreg"
|
name = "winreg"
|
||||||
version = "0.50.0"
|
version = "0.50.0"
|
||||||
@@ -7424,7 +7473,7 @@ dependencies = [
|
|||||||
"tokio-rustls 0.24.0",
|
"tokio-rustls 0.24.0",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"toml_edit",
|
"toml_edit 0.19.10",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tower",
|
"tower",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|||||||
@@ -73,13 +73,6 @@ RUN curl -fsSL 'https://apt.llvm.org/llvm-snapshot.gpg.key' | apt-key add - \
|
|||||||
&& bash -c 'for f in /usr/bin/clang*-${LLVM_VERSION} /usr/bin/llvm*-${LLVM_VERSION}; do ln -s "${f}" "${f%-${LLVM_VERSION}}"; done' \
|
&& bash -c 'for f in /usr/bin/clang*-${LLVM_VERSION} /usr/bin/llvm*-${LLVM_VERSION}; do ln -s "${f}" "${f%-${LLVM_VERSION}}"; done' \
|
||||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
# PostgreSQL 14
|
|
||||||
RUN curl -fsSL 'https://www.postgresql.org/media/keys/ACCC4CF8.asc' | apt-key add - \
|
|
||||||
&& echo 'deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main' > /etc/apt/sources.list.d/pgdg.list \
|
|
||||||
&& apt update \
|
|
||||||
&& apt install -y postgresql-client-14 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
||||||
|
|
||||||
# AWS CLI
|
# AWS CLI
|
||||||
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o "awscliv2.zip" \
|
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o "awscliv2.zip" \
|
||||||
&& unzip -q awscliv2.zip \
|
&& unzip -q awscliv2.zip \
|
||||||
@@ -113,10 +106,10 @@ RUN for package in Capture::Tiny DateTime Devel::Cover Digest::MD5 File::Spec JS
|
|||||||
&& rm -rf ../lcov.tar.gz
|
&& rm -rf ../lcov.tar.gz
|
||||||
|
|
||||||
# Compile and install the static OpenSSL library
|
# Compile and install the static OpenSSL library
|
||||||
ENV OPENSSL_VERSION=3.2.2
|
ENV OPENSSL_VERSION=1.1.1w
|
||||||
ENV OPENSSL_PREFIX=/usr/local/openssl
|
ENV OPENSSL_PREFIX=/usr/local/openssl
|
||||||
RUN wget -O /tmp/openssl-${OPENSSL_VERSION}.tar.gz https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz && \
|
RUN wget -O /tmp/openssl-${OPENSSL_VERSION}.tar.gz https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz && \
|
||||||
echo "197149c18d9e9f292c43f0400acaba12e5f52cacfe050f3d199277ea738ec2e7 /tmp/openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum --check && \
|
echo "cf3098950cb4d853ad95c0841f1f9c6d3dc102dccfcacd521d93925208b76ac8 /tmp/openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum --check && \
|
||||||
cd /tmp && \
|
cd /tmp && \
|
||||||
tar xzvf /tmp/openssl-${OPENSSL_VERSION}.tar.gz && \
|
tar xzvf /tmp/openssl-${OPENSSL_VERSION}.tar.gz && \
|
||||||
rm /tmp/openssl-${OPENSSL_VERSION}.tar.gz && \
|
rm /tmp/openssl-${OPENSSL_VERSION}.tar.gz && \
|
||||||
|
|||||||
@@ -83,12 +83,6 @@ pub fn write_postgres_conf(
|
|||||||
ComputeMode::Replica => {
|
ComputeMode::Replica => {
|
||||||
// hot_standby is 'on' by default, but let's be explicit
|
// hot_standby is 'on' by default, but let's be explicit
|
||||||
writeln!(file, "hot_standby=on")?;
|
writeln!(file, "hot_standby=on")?;
|
||||||
|
|
||||||
// Inform the replica about the primary state
|
|
||||||
// Default is 'false'
|
|
||||||
if let Some(primary_is_running) = spec.primary_is_running {
|
|
||||||
writeln!(file, "neon.primary_is_running={}", primary_is_running)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ const MONITOR_CHECK_INTERVAL: Duration = Duration::from_millis(500);
|
|||||||
// should be handled gracefully.
|
// should be handled gracefully.
|
||||||
fn watch_compute_activity(compute: &ComputeNode) {
|
fn watch_compute_activity(compute: &ComputeNode) {
|
||||||
// Suppose that `connstr` doesn't change
|
// Suppose that `connstr` doesn't change
|
||||||
let connstr = compute.connstr.as_str();
|
let mut connstr = compute.connstr.clone();
|
||||||
|
connstr
|
||||||
|
.query_pairs_mut()
|
||||||
|
.append_pair("application_name", "compute_activity_monitor");
|
||||||
|
let connstr = connstr.as_str();
|
||||||
|
|
||||||
// During startup and configuration we connect to every Postgres database,
|
// During startup and configuration we connect to every Postgres database,
|
||||||
// but we don't want to count this as some user activity. So wait until
|
// but we don't want to count this as some user activity. So wait until
|
||||||
|
|||||||
@@ -600,13 +600,9 @@ async fn handle_timeline(timeline_match: &ArgMatches, env: &mut local_env::Local
|
|||||||
Some(("import", import_match)) => {
|
Some(("import", import_match)) => {
|
||||||
let tenant_id = get_tenant_id(import_match, env)?;
|
let tenant_id = get_tenant_id(import_match, env)?;
|
||||||
let timeline_id = parse_timeline_id(import_match)?.expect("No timeline id provided");
|
let timeline_id = parse_timeline_id(import_match)?.expect("No timeline id provided");
|
||||||
let name = import_match
|
let branch_name = import_match
|
||||||
.get_one::<String>("node-name")
|
.get_one::<String>("branch-name")
|
||||||
.ok_or_else(|| anyhow!("No node name provided"))?;
|
.ok_or_else(|| anyhow!("No branch name provided"))?;
|
||||||
let update_catalog = import_match
|
|
||||||
.get_one::<bool>("update-catalog")
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// Parse base inputs
|
// Parse base inputs
|
||||||
let base_tarfile = import_match
|
let base_tarfile = import_match
|
||||||
@@ -633,24 +629,11 @@ async fn handle_timeline(timeline_match: &ArgMatches, env: &mut local_env::Local
|
|||||||
.copied()
|
.copied()
|
||||||
.context("Failed to parse postgres version from the argument string")?;
|
.context("Failed to parse postgres version from the argument string")?;
|
||||||
|
|
||||||
let mut cplane = ComputeControlPlane::load(env.clone())?;
|
|
||||||
println!("Importing timeline into pageserver ...");
|
println!("Importing timeline into pageserver ...");
|
||||||
pageserver
|
pageserver
|
||||||
.timeline_import(tenant_id, timeline_id, base, pg_wal, pg_version)
|
.timeline_import(tenant_id, timeline_id, base, pg_wal, pg_version)
|
||||||
.await?;
|
.await?;
|
||||||
env.register_branch_mapping(name.to_string(), tenant_id, timeline_id)?;
|
env.register_branch_mapping(branch_name.to_string(), tenant_id, timeline_id)?;
|
||||||
|
|
||||||
println!("Creating endpoint for imported timeline ...");
|
|
||||||
cplane.new_endpoint(
|
|
||||||
name,
|
|
||||||
tenant_id,
|
|
||||||
timeline_id,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
pg_version,
|
|
||||||
ComputeMode::Primary,
|
|
||||||
!update_catalog,
|
|
||||||
)?;
|
|
||||||
println!("Done");
|
println!("Done");
|
||||||
}
|
}
|
||||||
Some(("branch", branch_match)) => {
|
Some(("branch", branch_match)) => {
|
||||||
@@ -865,20 +848,13 @@ async fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Re
|
|||||||
|
|
||||||
let allow_multiple = sub_args.get_flag("allow-multiple");
|
let allow_multiple = sub_args.get_flag("allow-multiple");
|
||||||
|
|
||||||
// If --safekeepers argument is given, use only the listed safekeeper nodes.
|
// If --safekeepers argument is given, use only the listed
|
||||||
let safekeepers =
|
// safekeeper nodes; otherwise all from the env.
|
||||||
if let Some(safekeepers_str) = sub_args.get_one::<String>("safekeepers") {
|
let safekeepers = if let Some(safekeepers) = parse_safekeepers(sub_args)? {
|
||||||
let mut safekeepers: Vec<NodeId> = Vec::new();
|
safekeepers
|
||||||
for sk_id in safekeepers_str.split(',').map(str::trim) {
|
} else {
|
||||||
let sk_id = NodeId(u64::from_str(sk_id).map_err(|_| {
|
env.safekeepers.iter().map(|sk| sk.id).collect()
|
||||||
anyhow!("invalid node ID \"{sk_id}\" in --safekeepers list")
|
};
|
||||||
})?);
|
|
||||||
safekeepers.push(sk_id);
|
|
||||||
}
|
|
||||||
safekeepers
|
|
||||||
} else {
|
|
||||||
env.safekeepers.iter().map(|sk| sk.id).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
let endpoint = cplane
|
let endpoint = cplane
|
||||||
.endpoints
|
.endpoints
|
||||||
@@ -982,7 +958,10 @@ async fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Re
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
};
|
};
|
||||||
endpoint.reconfigure(pageservers, None).await?;
|
// If --safekeepers argument is given, use only the listed
|
||||||
|
// safekeeper nodes; otherwise all from the env.
|
||||||
|
let safekeepers = parse_safekeepers(sub_args)?;
|
||||||
|
endpoint.reconfigure(pageservers, None, safekeepers).await?;
|
||||||
}
|
}
|
||||||
"stop" => {
|
"stop" => {
|
||||||
let endpoint_id = sub_args
|
let endpoint_id = sub_args
|
||||||
@@ -1004,6 +983,23 @@ async fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Re
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse --safekeepers as list of safekeeper ids.
|
||||||
|
fn parse_safekeepers(sub_args: &ArgMatches) -> Result<Option<Vec<NodeId>>> {
|
||||||
|
if let Some(safekeepers_str) = sub_args.get_one::<String>("safekeepers") {
|
||||||
|
let mut safekeepers: Vec<NodeId> = Vec::new();
|
||||||
|
for sk_id in safekeepers_str.split(',').map(str::trim) {
|
||||||
|
let sk_id = NodeId(
|
||||||
|
u64::from_str(sk_id)
|
||||||
|
.map_err(|_| anyhow!("invalid node ID \"{sk_id}\" in --safekeepers list"))?,
|
||||||
|
);
|
||||||
|
safekeepers.push(sk_id);
|
||||||
|
}
|
||||||
|
Ok(Some(safekeepers))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_mappings(sub_match: &ArgMatches, env: &mut local_env::LocalEnv) -> Result<()> {
|
fn handle_mappings(sub_match: &ArgMatches, env: &mut local_env::LocalEnv) -> Result<()> {
|
||||||
let (sub_name, sub_args) = match sub_match.subcommand() {
|
let (sub_name, sub_args) = match sub_match.subcommand() {
|
||||||
Some(ep_subcommand_data) => ep_subcommand_data,
|
Some(ep_subcommand_data) => ep_subcommand_data,
|
||||||
@@ -1487,8 +1483,7 @@ fn cli() -> Command {
|
|||||||
.about("Import timeline from basebackup directory")
|
.about("Import timeline from basebackup directory")
|
||||||
.arg(tenant_id_arg.clone())
|
.arg(tenant_id_arg.clone())
|
||||||
.arg(timeline_id_arg.clone())
|
.arg(timeline_id_arg.clone())
|
||||||
.arg(Arg::new("node-name").long("node-name")
|
.arg(branch_name_arg.clone())
|
||||||
.help("Name to assign to the imported timeline"))
|
|
||||||
.arg(Arg::new("base-tarfile")
|
.arg(Arg::new("base-tarfile")
|
||||||
.long("base-tarfile")
|
.long("base-tarfile")
|
||||||
.value_parser(value_parser!(PathBuf))
|
.value_parser(value_parser!(PathBuf))
|
||||||
@@ -1504,7 +1499,6 @@ fn cli() -> Command {
|
|||||||
.arg(Arg::new("end-lsn").long("end-lsn")
|
.arg(Arg::new("end-lsn").long("end-lsn")
|
||||||
.help("Lsn the basebackup ends at"))
|
.help("Lsn the basebackup ends at"))
|
||||||
.arg(pg_version_arg.clone())
|
.arg(pg_version_arg.clone())
|
||||||
.arg(update_catalog.clone())
|
|
||||||
)
|
)
|
||||||
).subcommand(
|
).subcommand(
|
||||||
Command::new("tenant")
|
Command::new("tenant")
|
||||||
@@ -1609,7 +1603,7 @@ fn cli() -> Command {
|
|||||||
.about("Start postgres.\n If the endpoint doesn't exist yet, it is created.")
|
.about("Start postgres.\n If the endpoint doesn't exist yet, it is created.")
|
||||||
.arg(endpoint_id_arg.clone())
|
.arg(endpoint_id_arg.clone())
|
||||||
.arg(endpoint_pageserver_id_arg.clone())
|
.arg(endpoint_pageserver_id_arg.clone())
|
||||||
.arg(safekeepers_arg)
|
.arg(safekeepers_arg.clone())
|
||||||
.arg(remote_ext_config_args)
|
.arg(remote_ext_config_args)
|
||||||
.arg(create_test_user)
|
.arg(create_test_user)
|
||||||
.arg(allow_multiple.clone())
|
.arg(allow_multiple.clone())
|
||||||
@@ -1618,6 +1612,7 @@ fn cli() -> Command {
|
|||||||
.subcommand(Command::new("reconfigure")
|
.subcommand(Command::new("reconfigure")
|
||||||
.about("Reconfigure the endpoint")
|
.about("Reconfigure the endpoint")
|
||||||
.arg(endpoint_pageserver_id_arg)
|
.arg(endpoint_pageserver_id_arg)
|
||||||
|
.arg(safekeepers_arg)
|
||||||
.arg(endpoint_id_arg.clone())
|
.arg(endpoint_id_arg.clone())
|
||||||
.arg(tenant_id_arg.clone())
|
.arg(tenant_id_arg.clone())
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -499,6 +499,23 @@ impl Endpoint {
|
|||||||
.join(",")
|
.join(",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Map safekeepers ids to the actual connection strings.
|
||||||
|
fn build_safekeepers_connstrs(&self, sk_ids: Vec<NodeId>) -> Result<Vec<String>> {
|
||||||
|
let mut safekeeper_connstrings = Vec::new();
|
||||||
|
if self.mode == ComputeMode::Primary {
|
||||||
|
for sk_id in sk_ids {
|
||||||
|
let sk = self
|
||||||
|
.env
|
||||||
|
.safekeepers
|
||||||
|
.iter()
|
||||||
|
.find(|node| node.id == sk_id)
|
||||||
|
.ok_or_else(|| anyhow!("safekeeper {sk_id} does not exist"))?;
|
||||||
|
safekeeper_connstrings.push(format!("127.0.0.1:{}", sk.get_compute_port()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(safekeeper_connstrings)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn start(
|
pub async fn start(
|
||||||
&self,
|
&self,
|
||||||
auth_token: &Option<String>,
|
auth_token: &Option<String>,
|
||||||
@@ -523,18 +540,7 @@ impl Endpoint {
|
|||||||
let pageserver_connstring = Self::build_pageserver_connstr(&pageservers);
|
let pageserver_connstring = Self::build_pageserver_connstr(&pageservers);
|
||||||
assert!(!pageserver_connstring.is_empty());
|
assert!(!pageserver_connstring.is_empty());
|
||||||
|
|
||||||
let mut safekeeper_connstrings = Vec::new();
|
let safekeeper_connstrings = self.build_safekeepers_connstrs(safekeepers)?;
|
||||||
if self.mode == ComputeMode::Primary {
|
|
||||||
for sk_id in safekeepers {
|
|
||||||
let sk = self
|
|
||||||
.env
|
|
||||||
.safekeepers
|
|
||||||
.iter()
|
|
||||||
.find(|node| node.id == sk_id)
|
|
||||||
.ok_or_else(|| anyhow!("safekeeper {sk_id} does not exist"))?;
|
|
||||||
safekeeper_connstrings.push(format!("127.0.0.1:{}", sk.get_compute_port()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for file remote_extensions_spec.json
|
// check for file remote_extensions_spec.json
|
||||||
// if it is present, read it and pass to compute_ctl
|
// if it is present, read it and pass to compute_ctl
|
||||||
@@ -592,7 +598,6 @@ impl Endpoint {
|
|||||||
remote_extensions,
|
remote_extensions,
|
||||||
pgbouncer_settings: None,
|
pgbouncer_settings: None,
|
||||||
shard_stripe_size: Some(shard_stripe_size),
|
shard_stripe_size: Some(shard_stripe_size),
|
||||||
primary_is_running: None,
|
|
||||||
};
|
};
|
||||||
let spec_path = self.endpoint_path().join("spec.json");
|
let spec_path = self.endpoint_path().join("spec.json");
|
||||||
std::fs::write(spec_path, serde_json::to_string_pretty(&spec)?)?;
|
std::fs::write(spec_path, serde_json::to_string_pretty(&spec)?)?;
|
||||||
@@ -741,6 +746,7 @@ impl Endpoint {
|
|||||||
&self,
|
&self,
|
||||||
mut pageservers: Vec<(Host, u16)>,
|
mut pageservers: Vec<(Host, u16)>,
|
||||||
stripe_size: Option<ShardStripeSize>,
|
stripe_size: Option<ShardStripeSize>,
|
||||||
|
safekeepers: Option<Vec<NodeId>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut spec: ComputeSpec = {
|
let mut spec: ComputeSpec = {
|
||||||
let spec_path = self.endpoint_path().join("spec.json");
|
let spec_path = self.endpoint_path().join("spec.json");
|
||||||
@@ -775,6 +781,12 @@ impl Endpoint {
|
|||||||
spec.shard_stripe_size = stripe_size.map(|s| s.0 as usize);
|
spec.shard_stripe_size = stripe_size.map(|s| s.0 as usize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If safekeepers are not specified, don't change them.
|
||||||
|
if let Some(safekeepers) = safekeepers {
|
||||||
|
let safekeeper_connstrings = self.build_safekeepers_connstrs(safekeepers)?;
|
||||||
|
spec.safekeeper_connstrings = safekeeper_connstrings;
|
||||||
|
}
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(Duration::from_secs(30))
|
.timeout(Duration::from_secs(30))
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use std::{collections::HashMap, str::FromStr, time::Duration};
|
use std::{str::FromStr, time::Duration};
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use pageserver_api::{
|
use pageserver_api::{
|
||||||
@@ -21,7 +21,7 @@ use utils::id::{NodeId, TenantId};
|
|||||||
|
|
||||||
use pageserver_api::controller_api::{
|
use pageserver_api::controller_api::{
|
||||||
NodeConfigureRequest, NodeRegisterRequest, NodeSchedulingPolicy, PlacementPolicy,
|
NodeConfigureRequest, NodeRegisterRequest, NodeSchedulingPolicy, PlacementPolicy,
|
||||||
TenantLocateResponse, TenantShardMigrateRequest, TenantShardMigrateResponse,
|
TenantShardMigrateRequest, TenantShardMigrateResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
@@ -110,12 +110,6 @@ enum Command {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: String,
|
config: String,
|
||||||
},
|
},
|
||||||
/// Attempt to balance the locations for a tenant across pageservers. This is a client-side
|
|
||||||
/// alternative to the storage controller's scheduling optimization behavior.
|
|
||||||
TenantScatter {
|
|
||||||
#[arg(long)]
|
|
||||||
tenant_id: TenantId,
|
|
||||||
},
|
|
||||||
/// Print details about a particular tenant, including all its shards' states.
|
/// Print details about a particular tenant, including all its shards' states.
|
||||||
TenantDescribe {
|
TenantDescribe {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -498,88 +492,6 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
Command::TenantScatter { tenant_id } => {
|
|
||||||
// Find the shards
|
|
||||||
let locate_response = storcon_client
|
|
||||||
.dispatch::<(), TenantLocateResponse>(
|
|
||||||
Method::GET,
|
|
||||||
format!("control/v1/tenant/{tenant_id}/locate"),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let shards = locate_response.shards;
|
|
||||||
|
|
||||||
let mut node_to_shards: HashMap<NodeId, Vec<TenantShardId>> = HashMap::new();
|
|
||||||
let shard_count = shards.len();
|
|
||||||
for s in shards {
|
|
||||||
let entry = node_to_shards.entry(s.node_id).or_default();
|
|
||||||
entry.push(s.shard_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load list of available nodes
|
|
||||||
let nodes_resp = storcon_client
|
|
||||||
.dispatch::<(), Vec<NodeDescribeResponse>>(
|
|
||||||
Method::GET,
|
|
||||||
"control/v1/node".to_string(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for node in nodes_resp {
|
|
||||||
if matches!(node.availability, NodeAvailabilityWrapper::Active) {
|
|
||||||
node_to_shards.entry(node.id).or_default();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let max_shard_per_node = shard_count / node_to_shards.len();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let mut migrate_shard = None;
|
|
||||||
for shards in node_to_shards.values_mut() {
|
|
||||||
if shards.len() > max_shard_per_node {
|
|
||||||
// Pick the emptiest
|
|
||||||
migrate_shard = Some(shards.pop().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let Some(migrate_shard) = migrate_shard else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pick the emptiest node to migrate to
|
|
||||||
let mut destinations = node_to_shards
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| (k, v.len()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
destinations.sort_by_key(|i| i.1);
|
|
||||||
let (destination_node, destination_count) = *destinations.first().unwrap();
|
|
||||||
if destination_count + 1 > max_shard_per_node {
|
|
||||||
// Even the emptiest destination doesn't have space: we're done
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let destination_node = *destination_node;
|
|
||||||
|
|
||||||
node_to_shards
|
|
||||||
.get_mut(&destination_node)
|
|
||||||
.unwrap()
|
|
||||||
.push(migrate_shard);
|
|
||||||
|
|
||||||
println!("Migrate {} -> {} ...", migrate_shard, destination_node);
|
|
||||||
|
|
||||||
storcon_client
|
|
||||||
.dispatch::<TenantShardMigrateRequest, TenantShardMigrateResponse>(
|
|
||||||
Method::PUT,
|
|
||||||
format!("control/v1/tenant/{migrate_shard}/migrate"),
|
|
||||||
Some(TenantShardMigrateRequest {
|
|
||||||
tenant_shard_id: migrate_shard,
|
|
||||||
node_id: destination_node,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
println!("Migrate {} -> {} OK", migrate_shard, destination_node);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spread the shards across the nodes
|
|
||||||
}
|
|
||||||
Command::TenantDescribe { tenant_id } => {
|
Command::TenantDescribe { tenant_id } => {
|
||||||
let describe_response = storcon_client
|
let describe_response = storcon_client
|
||||||
.dispatch::<(), TenantDescribeResponse>(
|
.dispatch::<(), TenantDescribeResponse>(
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
# Example docker compose configuration
|
||||||
|
|
||||||
|
The configuration in this directory is used for testing Neon docker images: it is
|
||||||
|
not intended for deploying a usable system. To run a development environment where
|
||||||
|
you can experiment with a minature Neon system, use `cargo neon` rather than container images.
|
||||||
|
|
||||||
|
This configuration does not start the storage controller, because the controller
|
||||||
|
needs a way to reconfigure running computes, and no such thing exists in this setup.
|
||||||
|
|
||||||
@@ -23,11 +23,10 @@ echo "Page server is ready."
|
|||||||
echo "Create a tenant and timeline"
|
echo "Create a tenant and timeline"
|
||||||
generate_id tenant_id
|
generate_id tenant_id
|
||||||
PARAMS=(
|
PARAMS=(
|
||||||
-sb
|
-X PUT
|
||||||
-X POST
|
|
||||||
-H "Content-Type: application/json"
|
-H "Content-Type: application/json"
|
||||||
-d "{\"new_tenant_id\": \"${tenant_id}\"}"
|
-d "{\"mode\": \"AttachedSingle\", \"generation\": 1, \"tenant_conf\": {}}"
|
||||||
http://pageserver:9898/v1/tenant/
|
"http://pageserver:9898/v1/tenant/${tenant_id}/location_config"
|
||||||
)
|
)
|
||||||
result=$(curl "${PARAMS[@]}")
|
result=$(curl "${PARAMS[@]}")
|
||||||
echo $result | jq .
|
echo $result | jq .
|
||||||
|
|||||||
@@ -96,12 +96,6 @@ pub struct ComputeSpec {
|
|||||||
// Stripe size for pageserver sharding, in pages
|
// Stripe size for pageserver sharding, in pages
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub shard_stripe_size: Option<usize>,
|
pub shard_stripe_size: Option<usize>,
|
||||||
|
|
||||||
// When we are starting a new replica in hot standby mode,
|
|
||||||
// we need to know if the primary is running.
|
|
||||||
// This is used to determine if replica should wait for
|
|
||||||
// RUNNING_XACTS from primary or not.
|
|
||||||
pub primary_is_running: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Feature flag to signal `compute_ctl` to enable certain experimental functionality.
|
/// Feature flag to signal `compute_ctl` to enable certain experimental functionality.
|
||||||
|
|||||||
@@ -103,9 +103,10 @@ static MAXRSS_KB: Lazy<IntGauge> = Lazy::new(|| {
|
|||||||
.expect("Failed to register maxrss_kb int gauge")
|
.expect("Failed to register maxrss_kb int gauge")
|
||||||
});
|
});
|
||||||
|
|
||||||
pub const DISK_WRITE_SECONDS_BUCKETS: &[f64] = &[
|
/// Most common fsync latency is 50 µs - 100 µs, but it can be much higher,
|
||||||
0.000_050, 0.000_100, 0.000_500, 0.001, 0.003, 0.005, 0.01, 0.05, 0.1, 0.3, 0.5,
|
/// especially during many concurrent disk operations.
|
||||||
];
|
pub const DISK_FSYNC_SECONDS_BUCKETS: &[f64] =
|
||||||
|
&[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 30.0];
|
||||||
|
|
||||||
pub struct BuildInfo {
|
pub struct BuildInfo {
|
||||||
pub revision: &'static str,
|
pub revision: &'static str,
|
||||||
|
|||||||
@@ -160,8 +160,9 @@ impl Key {
|
|||||||
key
|
key
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a 18B slice to a key. This function should not be used for metadata keys because field2 is handled differently.
|
/// Convert a 18B slice to a key. This function should not be used for 16B metadata keys because `field2` is handled differently.
|
||||||
/// Use [`Key::from_i128`] instead if you want to handle 16B keys (i.e., metadata keys).
|
/// Use [`Key::from_i128`] instead if you want to handle 16B keys (i.e., metadata keys). There are some restrictions on `field2`,
|
||||||
|
/// and therefore not all 18B slices are valid page server keys.
|
||||||
pub fn from_slice(b: &[u8]) -> Self {
|
pub fn from_slice(b: &[u8]) -> Self {
|
||||||
Key {
|
Key {
|
||||||
field1: b[0],
|
field1: b[0],
|
||||||
@@ -173,7 +174,7 @@ impl Key {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a key to a 18B slice. This function should not be used for metadata keys because field2 is handled differently.
|
/// Convert a key to a 18B slice. This function should not be used for getting a 16B metadata key because `field2` is handled differently.
|
||||||
/// Use [`Key::to_i128`] instead if you want to get a 16B key (i.e., metadata keys).
|
/// Use [`Key::to_i128`] instead if you want to get a 16B key (i.e., metadata keys).
|
||||||
pub fn write_to_byte_slice(&self, buf: &mut [u8]) {
|
pub fn write_to_byte_slice(&self, buf: &mut [u8]) {
|
||||||
buf[0] = self.field1;
|
buf[0] = self.field1;
|
||||||
|
|||||||
@@ -607,31 +607,6 @@ impl TenantConfigRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct TenantAttachRequest {
|
|
||||||
#[serde(default)]
|
|
||||||
pub config: TenantAttachConfig,
|
|
||||||
#[serde(default)]
|
|
||||||
pub generation: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Newtype to enforce deny_unknown_fields on TenantConfig for
|
|
||||||
/// its usage inside `TenantAttachRequest`.
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
pub struct TenantAttachConfig {
|
|
||||||
#[serde(flatten)]
|
|
||||||
allowing_unknown_fields: TenantConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for TenantAttachConfig {
|
|
||||||
type Target = TenantConfig;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.allowing_unknown_fields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See [`TenantState::attachment_status`] and the OpenAPI docs for context.
|
/// See [`TenantState::attachment_status`] and the OpenAPI docs for context.
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
#[serde(tag = "slug", content = "data", rename_all = "snake_case")]
|
#[serde(tag = "slug", content = "data", rename_all = "snake_case")]
|
||||||
@@ -650,8 +625,7 @@ pub struct TenantInfo {
|
|||||||
/// If a layer is present in both local FS and S3, it counts only once.
|
/// If a layer is present in both local FS and S3, it counts only once.
|
||||||
pub current_physical_size: Option<u64>, // physical size is only included in `tenant_status` endpoint
|
pub current_physical_size: Option<u64>, // physical size is only included in `tenant_status` endpoint
|
||||||
pub attachment_status: TenantAttachmentStatus,
|
pub attachment_status: TenantAttachmentStatus,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
pub generation: u32,
|
||||||
pub generation: Option<u32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
@@ -1478,7 +1452,7 @@ mod tests {
|
|||||||
state: TenantState::Active,
|
state: TenantState::Active,
|
||||||
current_physical_size: Some(42),
|
current_physical_size: Some(42),
|
||||||
attachment_status: TenantAttachmentStatus::Attached,
|
attachment_status: TenantAttachmentStatus::Attached,
|
||||||
generation: None,
|
generation: 1,
|
||||||
};
|
};
|
||||||
let expected_active = json!({
|
let expected_active = json!({
|
||||||
"id": original_active.id.to_string(),
|
"id": original_active.id.to_string(),
|
||||||
@@ -1488,7 +1462,8 @@ mod tests {
|
|||||||
"current_physical_size": 42,
|
"current_physical_size": 42,
|
||||||
"attachment_status": {
|
"attachment_status": {
|
||||||
"slug":"attached",
|
"slug":"attached",
|
||||||
}
|
},
|
||||||
|
"generation" : 1
|
||||||
});
|
});
|
||||||
|
|
||||||
let original_broken = TenantInfo {
|
let original_broken = TenantInfo {
|
||||||
@@ -1499,7 +1474,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
current_physical_size: Some(42),
|
current_physical_size: Some(42),
|
||||||
attachment_status: TenantAttachmentStatus::Attached,
|
attachment_status: TenantAttachmentStatus::Attached,
|
||||||
generation: None,
|
generation: 1,
|
||||||
};
|
};
|
||||||
let expected_broken = json!({
|
let expected_broken = json!({
|
||||||
"id": original_broken.id.to_string(),
|
"id": original_broken.id.to_string(),
|
||||||
@@ -1513,7 +1488,8 @@ mod tests {
|
|||||||
"current_physical_size": 42,
|
"current_physical_size": 42,
|
||||||
"attachment_status": {
|
"attachment_status": {
|
||||||
"slug":"attached",
|
"slug":"attached",
|
||||||
}
|
},
|
||||||
|
"generation" : 1
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1554,18 +1530,6 @@ mod tests {
|
|||||||
"expect unknown field `unknown_field` error, got: {}",
|
"expect unknown field `unknown_field` error, got: {}",
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
|
|
||||||
let attach_request = json!({
|
|
||||||
"config": {
|
|
||||||
"unknown_field": "unknown_value".to_string(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
let err = serde_json::from_value::<TenantAttachRequest>(attach_request).unwrap_err();
|
|
||||||
assert!(
|
|
||||||
err.to_string().contains("unknown field `unknown_field`"),
|
|
||||||
"expect unknown field `unknown_field` error, got: {}",
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -144,20 +144,7 @@ impl PgConnectionConfig {
|
|||||||
// implement and this function is hardly a bottleneck. The function is only called around
|
// implement and this function is hardly a bottleneck. The function is only called around
|
||||||
// establishing a new connection.
|
// establishing a new connection.
|
||||||
#[allow(unstable_name_collisions)]
|
#[allow(unstable_name_collisions)]
|
||||||
config.options(
|
config.options(&encode_options(&self.options));
|
||||||
&self
|
|
||||||
.options
|
|
||||||
.iter()
|
|
||||||
.map(|s| {
|
|
||||||
if s.contains(['\\', ' ']) {
|
|
||||||
Cow::Owned(s.replace('\\', "\\\\").replace(' ', "\\ "))
|
|
||||||
} else {
|
|
||||||
Cow::Borrowed(s.as_str())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.intersperse(Cow::Borrowed(" ")) // TODO: use impl from std once it's stabilized
|
|
||||||
.collect::<String>(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
@@ -178,6 +165,21 @@ impl PgConnectionConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unstable_name_collisions)]
|
||||||
|
fn encode_options(options: &[String]) -> String {
|
||||||
|
options
|
||||||
|
.iter()
|
||||||
|
.map(|s| {
|
||||||
|
if s.contains(['\\', ' ']) {
|
||||||
|
Cow::Owned(s.replace('\\', "\\\\").replace(' ', "\\ "))
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed(s.as_str())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.intersperse(Cow::Borrowed(" ")) // TODO: use impl from std once it's stabilized
|
||||||
|
.collect::<String>()
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for PgConnectionConfig {
|
impl fmt::Display for PgConnectionConfig {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
// The password is intentionally hidden and not part of this display string.
|
// The password is intentionally hidden and not part of this display string.
|
||||||
@@ -206,7 +208,7 @@ impl fmt::Debug for PgConnectionConfig {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests_pg_connection_config {
|
mod tests_pg_connection_config {
|
||||||
use crate::PgConnectionConfig;
|
use crate::{encode_options, PgConnectionConfig};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use url::Host;
|
use url::Host;
|
||||||
|
|
||||||
@@ -255,18 +257,12 @@ mod tests_pg_connection_config {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_with_options() {
|
fn test_with_options() {
|
||||||
let cfg = PgConnectionConfig::new_host_port(STUB_HOST.clone(), 123).extend_options([
|
let options = encode_options(&[
|
||||||
"hello",
|
"hello".to_owned(),
|
||||||
"world",
|
"world".to_owned(),
|
||||||
"with space",
|
"with space".to_owned(),
|
||||||
"and \\ backslashes",
|
"and \\ backslashes".to_owned(),
|
||||||
]);
|
]);
|
||||||
assert_eq!(cfg.host(), &*STUB_HOST);
|
assert_eq!(options, "hello world with\\ space and\\ \\\\\\ backslashes");
|
||||||
assert_eq!(cfg.port(), 123);
|
|
||||||
assert_eq!(cfg.raw_address(), "stub.host.example:123");
|
|
||||||
assert_eq!(
|
|
||||||
cfg.to_tokio_postgres_config().get_options(),
|
|
||||||
Some("hello world with\\ space and\\ \\\\\\ backslashes")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ use utils::backoff;
|
|||||||
|
|
||||||
use crate::metrics::{start_measuring_requests, AttemptOutcome, RequestKind};
|
use crate::metrics::{start_measuring_requests, AttemptOutcome, RequestKind};
|
||||||
use crate::{
|
use crate::{
|
||||||
error::Cancelled, AzureConfig, ConcurrencyLimiter, Download, DownloadError, Listing,
|
config::AzureConfig, error::Cancelled, ConcurrencyLimiter, Download, DownloadError, Listing,
|
||||||
ListingMode, RemotePath, RemoteStorage, StorageMetadata, TimeTravelError, TimeoutOrCancel,
|
ListingMode, RemotePath, RemoteStorage, StorageMetadata, TimeTravelError, TimeoutOrCancel,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
use std::{fmt::Debug, num::NonZeroUsize, str::FromStr, time::Duration};
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use aws_sdk_s3::types::StorageClass;
|
||||||
|
use camino::Utf8PathBuf;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
DEFAULT_MAX_KEYS_PER_LIST_RESPONSE, DEFAULT_REMOTE_STORAGE_AZURE_CONCURRENCY_LIMIT,
|
||||||
|
DEFAULT_REMOTE_STORAGE_S3_CONCURRENCY_LIMIT,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// External backup storage configuration, enough for creating a client for that storage.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
pub struct RemoteStorageConfig {
|
||||||
|
/// The storage connection configuration.
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub storage: RemoteStorageKind,
|
||||||
|
/// A common timeout enforced for all requests after concurrency limiter permit has been
|
||||||
|
/// acquired.
|
||||||
|
#[serde(
|
||||||
|
with = "humantime_serde",
|
||||||
|
default = "default_timeout",
|
||||||
|
skip_serializing_if = "is_default_timeout"
|
||||||
|
)]
|
||||||
|
pub timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_timeout() -> Duration {
|
||||||
|
RemoteStorageConfig::DEFAULT_TIMEOUT
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_default_timeout(d: &Duration) -> bool {
|
||||||
|
*d == RemoteStorageConfig::DEFAULT_TIMEOUT
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A kind of a remote storage to connect to, with its connection configuration.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum RemoteStorageKind {
|
||||||
|
/// Storage based on local file system.
|
||||||
|
/// Specify a root folder to place all stored files into.
|
||||||
|
LocalFs { local_path: Utf8PathBuf },
|
||||||
|
/// AWS S3 based storage, storing all files in the S3 bucket
|
||||||
|
/// specified by the config
|
||||||
|
AwsS3(S3Config),
|
||||||
|
/// Azure Blob based storage, storing all files in the container
|
||||||
|
/// specified by the config
|
||||||
|
AzureContainer(AzureConfig),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AWS S3 bucket coordinates and access credentials to manage the bucket contents (read and write).
|
||||||
|
#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
pub struct S3Config {
|
||||||
|
/// Name of the bucket to connect to.
|
||||||
|
pub bucket_name: String,
|
||||||
|
/// The region where the bucket is located at.
|
||||||
|
pub bucket_region: String,
|
||||||
|
/// A "subfolder" in the bucket, to use the same bucket separately by multiple remote storage users at once.
|
||||||
|
pub prefix_in_bucket: Option<String>,
|
||||||
|
/// A base URL to send S3 requests to.
|
||||||
|
/// By default, the endpoint is derived from a region name, assuming it's
|
||||||
|
/// an AWS S3 region name, erroring on wrong region name.
|
||||||
|
/// Endpoint provides a way to support other S3 flavors and their regions.
|
||||||
|
///
|
||||||
|
/// Example: `http://127.0.0.1:5000`
|
||||||
|
pub endpoint: Option<String>,
|
||||||
|
/// AWS S3 has various limits on its API calls, we need not to exceed those.
|
||||||
|
/// See [`DEFAULT_REMOTE_STORAGE_S3_CONCURRENCY_LIMIT`] for more details.
|
||||||
|
#[serde(default = "default_remote_storage_s3_concurrency_limit")]
|
||||||
|
pub concurrency_limit: NonZeroUsize,
|
||||||
|
#[serde(default = "default_max_keys_per_list_response")]
|
||||||
|
pub max_keys_per_list_response: Option<i32>,
|
||||||
|
#[serde(
|
||||||
|
deserialize_with = "deserialize_storage_class",
|
||||||
|
serialize_with = "serialize_storage_class",
|
||||||
|
default
|
||||||
|
)]
|
||||||
|
pub upload_storage_class: Option<StorageClass>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_remote_storage_s3_concurrency_limit() -> NonZeroUsize {
|
||||||
|
DEFAULT_REMOTE_STORAGE_S3_CONCURRENCY_LIMIT
|
||||||
|
.try_into()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_max_keys_per_list_response() -> Option<i32> {
|
||||||
|
DEFAULT_MAX_KEYS_PER_LIST_RESPONSE
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for S3Config {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("S3Config")
|
||||||
|
.field("bucket_name", &self.bucket_name)
|
||||||
|
.field("bucket_region", &self.bucket_region)
|
||||||
|
.field("prefix_in_bucket", &self.prefix_in_bucket)
|
||||||
|
.field("concurrency_limit", &self.concurrency_limit)
|
||||||
|
.field(
|
||||||
|
"max_keys_per_list_response",
|
||||||
|
&self.max_keys_per_list_response,
|
||||||
|
)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Azure bucket coordinates and access credentials to manage the bucket contents (read and write).
|
||||||
|
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct AzureConfig {
|
||||||
|
/// Name of the container to connect to.
|
||||||
|
pub container_name: String,
|
||||||
|
/// Name of the storage account the container is inside of
|
||||||
|
pub storage_account: Option<String>,
|
||||||
|
/// The region where the bucket is located at.
|
||||||
|
pub container_region: String,
|
||||||
|
/// A "subfolder" in the container, to use the same container separately by multiple remote storage users at once.
|
||||||
|
pub prefix_in_container: Option<String>,
|
||||||
|
/// Azure has various limits on its API calls, we need not to exceed those.
|
||||||
|
/// See [`DEFAULT_REMOTE_STORAGE_AZURE_CONCURRENCY_LIMIT`] for more details.
|
||||||
|
#[serde(default = "default_remote_storage_azure_concurrency_limit")]
|
||||||
|
pub concurrency_limit: NonZeroUsize,
|
||||||
|
#[serde(default = "default_max_keys_per_list_response")]
|
||||||
|
pub max_keys_per_list_response: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_remote_storage_azure_concurrency_limit() -> NonZeroUsize {
|
||||||
|
NonZeroUsize::new(DEFAULT_REMOTE_STORAGE_AZURE_CONCURRENCY_LIMIT).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for AzureConfig {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("AzureConfig")
|
||||||
|
.field("bucket_name", &self.container_name)
|
||||||
|
.field("storage_account", &self.storage_account)
|
||||||
|
.field("bucket_region", &self.container_region)
|
||||||
|
.field("prefix_in_container", &self.prefix_in_container)
|
||||||
|
.field("concurrency_limit", &self.concurrency_limit)
|
||||||
|
.field(
|
||||||
|
"max_keys_per_list_response",
|
||||||
|
&self.max_keys_per_list_response,
|
||||||
|
)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_storage_class<'de, D: serde::Deserializer<'de>>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<Option<StorageClass>, D::Error> {
|
||||||
|
Option::<String>::deserialize(deserializer).and_then(|s| {
|
||||||
|
if let Some(s) = s {
|
||||||
|
use serde::de::Error;
|
||||||
|
let storage_class = StorageClass::from_str(&s).expect("infallible");
|
||||||
|
#[allow(deprecated)]
|
||||||
|
if matches!(storage_class, StorageClass::Unknown(_)) {
|
||||||
|
return Err(D::Error::custom(format!(
|
||||||
|
"Specified storage class unknown to SDK: '{s}'. Allowed values: {:?}",
|
||||||
|
StorageClass::values()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(Some(storage_class))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_storage_class<S: serde::Serializer>(
|
||||||
|
val: &Option<StorageClass>,
|
||||||
|
serializer: S,
|
||||||
|
) -> Result<S::Ok, S::Error> {
|
||||||
|
let val = val.as_ref().map(StorageClass::as_str);
|
||||||
|
Option::<&str>::serialize(&val, serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RemoteStorageConfig {
|
||||||
|
pub const DEFAULT_TIMEOUT: Duration = std::time::Duration::from_secs(120);
|
||||||
|
|
||||||
|
pub fn from_toml(toml: &toml_edit::Item) -> anyhow::Result<Option<RemoteStorageConfig>> {
|
||||||
|
let document: toml_edit::Document = match toml {
|
||||||
|
toml_edit::Item::Table(toml) => toml.clone().into(),
|
||||||
|
toml_edit::Item::Value(toml_edit::Value::InlineTable(toml)) => {
|
||||||
|
toml.clone().into_table().into()
|
||||||
|
}
|
||||||
|
_ => bail!("toml not a table or inline table"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if document.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(toml_edit::de::from_document(document)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn parse(input: &str) -> anyhow::Result<Option<RemoteStorageConfig>> {
|
||||||
|
let toml = input.parse::<toml_edit::Document>().unwrap();
|
||||||
|
RemoteStorageConfig::from_toml(toml.as_item())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_localfs_config_with_timeout() {
|
||||||
|
let input = "local_path = '.'
|
||||||
|
timeout = '5s'";
|
||||||
|
|
||||||
|
let config = parse(input).unwrap().expect("it exists");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config,
|
||||||
|
RemoteStorageConfig {
|
||||||
|
storage: RemoteStorageKind::LocalFs {
|
||||||
|
local_path: Utf8PathBuf::from(".")
|
||||||
|
},
|
||||||
|
timeout: Duration::from_secs(5)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_s3_parsing() {
|
||||||
|
let toml = "\
|
||||||
|
bucket_name = 'foo-bar'
|
||||||
|
bucket_region = 'eu-central-1'
|
||||||
|
upload_storage_class = 'INTELLIGENT_TIERING'
|
||||||
|
timeout = '7s'
|
||||||
|
";
|
||||||
|
|
||||||
|
let config = parse(toml).unwrap().expect("it exists");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config,
|
||||||
|
RemoteStorageConfig {
|
||||||
|
storage: RemoteStorageKind::AwsS3(S3Config {
|
||||||
|
bucket_name: "foo-bar".into(),
|
||||||
|
bucket_region: "eu-central-1".into(),
|
||||||
|
prefix_in_bucket: None,
|
||||||
|
endpoint: None,
|
||||||
|
concurrency_limit: default_remote_storage_s3_concurrency_limit(),
|
||||||
|
max_keys_per_list_response: DEFAULT_MAX_KEYS_PER_LIST_RESPONSE,
|
||||||
|
upload_storage_class: Some(StorageClass::IntelligentTiering),
|
||||||
|
}),
|
||||||
|
timeout: Duration::from_secs(7)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_azure_parsing() {
|
||||||
|
let toml = "\
|
||||||
|
container_name = 'foo-bar'
|
||||||
|
container_region = 'westeurope'
|
||||||
|
upload_storage_class = 'INTELLIGENT_TIERING'
|
||||||
|
timeout = '7s'
|
||||||
|
";
|
||||||
|
|
||||||
|
let config = parse(toml).unwrap().expect("it exists");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config,
|
||||||
|
RemoteStorageConfig {
|
||||||
|
storage: RemoteStorageKind::AzureContainer(AzureConfig {
|
||||||
|
container_name: "foo-bar".into(),
|
||||||
|
storage_account: None,
|
||||||
|
container_region: "westeurope".into(),
|
||||||
|
prefix_in_container: None,
|
||||||
|
concurrency_limit: default_remote_storage_azure_concurrency_limit(),
|
||||||
|
max_keys_per_list_response: DEFAULT_MAX_KEYS_PER_LIST_RESPONSE,
|
||||||
|
}),
|
||||||
|
timeout: Duration::from_secs(7)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
mod azure_blob;
|
mod azure_blob;
|
||||||
|
mod config;
|
||||||
mod error;
|
mod error;
|
||||||
mod local_fs;
|
mod local_fs;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
@@ -18,17 +19,10 @@ mod simulate_failures;
|
|||||||
mod support;
|
mod support;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap, fmt::Debug, num::NonZeroU32, pin::Pin, sync::Arc, time::SystemTime,
|
||||||
fmt::Debug,
|
|
||||||
num::{NonZeroU32, NonZeroUsize},
|
|
||||||
pin::Pin,
|
|
||||||
str::FromStr,
|
|
||||||
sync::Arc,
|
|
||||||
time::{Duration, SystemTime},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{bail, Context};
|
use anyhow::Context;
|
||||||
use aws_sdk_s3::types::StorageClass;
|
|
||||||
use camino::{Utf8Path, Utf8PathBuf};
|
use camino::{Utf8Path, Utf8PathBuf};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
@@ -44,6 +38,8 @@ pub use self::{
|
|||||||
};
|
};
|
||||||
use s3_bucket::RequestKind;
|
use s3_bucket::RequestKind;
|
||||||
|
|
||||||
|
pub use crate::config::{AzureConfig, RemoteStorageConfig, RemoteStorageKind, S3Config};
|
||||||
|
|
||||||
/// Azure SDK's ETag type is a simple String wrapper: we use this internally instead of repeating it here.
|
/// Azure SDK's ETag type is a simple String wrapper: we use this internally instead of repeating it here.
|
||||||
pub use azure_core::Etag;
|
pub use azure_core::Etag;
|
||||||
|
|
||||||
@@ -525,168 +521,6 @@ impl<const N: usize> From<[(&str, &str); N]> for StorageMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// External backup storage configuration, enough for creating a client for that storage.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
|
||||||
pub struct RemoteStorageConfig {
|
|
||||||
/// The storage connection configuration.
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub storage: RemoteStorageKind,
|
|
||||||
/// A common timeout enforced for all requests after concurrency limiter permit has been
|
|
||||||
/// acquired.
|
|
||||||
#[serde(with = "humantime_serde", default = "default_timeout")]
|
|
||||||
pub timeout: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_timeout() -> Duration {
|
|
||||||
RemoteStorageConfig::DEFAULT_TIMEOUT
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A kind of a remote storage to connect to, with its connection configuration.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum RemoteStorageKind {
|
|
||||||
/// Storage based on local file system.
|
|
||||||
/// Specify a root folder to place all stored files into.
|
|
||||||
LocalFs { local_path: Utf8PathBuf },
|
|
||||||
/// AWS S3 based storage, storing all files in the S3 bucket
|
|
||||||
/// specified by the config
|
|
||||||
AwsS3(S3Config),
|
|
||||||
/// Azure Blob based storage, storing all files in the container
|
|
||||||
/// specified by the config
|
|
||||||
AzureContainer(AzureConfig),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// AWS S3 bucket coordinates and access credentials to manage the bucket contents (read and write).
|
|
||||||
#[derive(Clone, PartialEq, Eq, serde::Deserialize)]
|
|
||||||
pub struct S3Config {
|
|
||||||
/// Name of the bucket to connect to.
|
|
||||||
pub bucket_name: String,
|
|
||||||
/// The region where the bucket is located at.
|
|
||||||
pub bucket_region: String,
|
|
||||||
/// A "subfolder" in the bucket, to use the same bucket separately by multiple remote storage users at once.
|
|
||||||
pub prefix_in_bucket: Option<String>,
|
|
||||||
/// A base URL to send S3 requests to.
|
|
||||||
/// By default, the endpoint is derived from a region name, assuming it's
|
|
||||||
/// an AWS S3 region name, erroring on wrong region name.
|
|
||||||
/// Endpoint provides a way to support other S3 flavors and their regions.
|
|
||||||
///
|
|
||||||
/// Example: `http://127.0.0.1:5000`
|
|
||||||
pub endpoint: Option<String>,
|
|
||||||
/// AWS S3 has various limits on its API calls, we need not to exceed those.
|
|
||||||
/// See [`DEFAULT_REMOTE_STORAGE_S3_CONCURRENCY_LIMIT`] for more details.
|
|
||||||
#[serde(default = "default_remote_storage_s3_concurrency_limit")]
|
|
||||||
pub concurrency_limit: NonZeroUsize,
|
|
||||||
#[serde(default = "default_max_keys_per_list_response")]
|
|
||||||
pub max_keys_per_list_response: Option<i32>,
|
|
||||||
#[serde(deserialize_with = "deserialize_storage_class", default)]
|
|
||||||
pub upload_storage_class: Option<StorageClass>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_remote_storage_s3_concurrency_limit() -> NonZeroUsize {
|
|
||||||
DEFAULT_REMOTE_STORAGE_S3_CONCURRENCY_LIMIT
|
|
||||||
.try_into()
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_max_keys_per_list_response() -> Option<i32> {
|
|
||||||
DEFAULT_MAX_KEYS_PER_LIST_RESPONSE
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for S3Config {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("S3Config")
|
|
||||||
.field("bucket_name", &self.bucket_name)
|
|
||||||
.field("bucket_region", &self.bucket_region)
|
|
||||||
.field("prefix_in_bucket", &self.prefix_in_bucket)
|
|
||||||
.field("concurrency_limit", &self.concurrency_limit)
|
|
||||||
.field(
|
|
||||||
"max_keys_per_list_response",
|
|
||||||
&self.max_keys_per_list_response,
|
|
||||||
)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Azure bucket coordinates and access credentials to manage the bucket contents (read and write).
|
|
||||||
#[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct AzureConfig {
|
|
||||||
/// Name of the container to connect to.
|
|
||||||
pub container_name: String,
|
|
||||||
/// Name of the storage account the container is inside of
|
|
||||||
pub storage_account: Option<String>,
|
|
||||||
/// The region where the bucket is located at.
|
|
||||||
pub container_region: String,
|
|
||||||
/// A "subfolder" in the container, to use the same container separately by multiple remote storage users at once.
|
|
||||||
pub prefix_in_container: Option<String>,
|
|
||||||
/// Azure has various limits on its API calls, we need not to exceed those.
|
|
||||||
/// See [`DEFAULT_REMOTE_STORAGE_AZURE_CONCURRENCY_LIMIT`] for more details.
|
|
||||||
#[serde(default = "default_remote_storage_azure_concurrency_limit")]
|
|
||||||
pub concurrency_limit: NonZeroUsize,
|
|
||||||
#[serde(default = "default_max_keys_per_list_response")]
|
|
||||||
pub max_keys_per_list_response: Option<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_remote_storage_azure_concurrency_limit() -> NonZeroUsize {
|
|
||||||
NonZeroUsize::new(DEFAULT_REMOTE_STORAGE_AZURE_CONCURRENCY_LIMIT).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for AzureConfig {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("AzureConfig")
|
|
||||||
.field("bucket_name", &self.container_name)
|
|
||||||
.field("storage_account", &self.storage_account)
|
|
||||||
.field("bucket_region", &self.container_region)
|
|
||||||
.field("prefix_in_container", &self.prefix_in_container)
|
|
||||||
.field("concurrency_limit", &self.concurrency_limit)
|
|
||||||
.field(
|
|
||||||
"max_keys_per_list_response",
|
|
||||||
&self.max_keys_per_list_response,
|
|
||||||
)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_storage_class<'de, D: serde::Deserializer<'de>>(
|
|
||||||
deserializer: D,
|
|
||||||
) -> Result<Option<StorageClass>, D::Error> {
|
|
||||||
Option::<String>::deserialize(deserializer).and_then(|s| {
|
|
||||||
if let Some(s) = s {
|
|
||||||
use serde::de::Error;
|
|
||||||
let storage_class = StorageClass::from_str(&s).expect("infallible");
|
|
||||||
#[allow(deprecated)]
|
|
||||||
if matches!(storage_class, StorageClass::Unknown(_)) {
|
|
||||||
return Err(D::Error::custom(format!(
|
|
||||||
"Specified storage class unknown to SDK: '{s}'. Allowed values: {:?}",
|
|
||||||
StorageClass::values()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Ok(Some(storage_class))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RemoteStorageConfig {
|
|
||||||
pub const DEFAULT_TIMEOUT: Duration = std::time::Duration::from_secs(120);
|
|
||||||
|
|
||||||
pub fn from_toml(toml: &toml_edit::Item) -> anyhow::Result<Option<RemoteStorageConfig>> {
|
|
||||||
let document: toml_edit::Document = match toml {
|
|
||||||
toml_edit::Item::Table(toml) => toml.clone().into(),
|
|
||||||
toml_edit::Item::Value(toml_edit::Value::InlineTable(toml)) => {
|
|
||||||
toml.clone().into_table().into()
|
|
||||||
}
|
|
||||||
_ => bail!("toml not a table or inline table"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if document.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(toml_edit::de::from_document(document)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ConcurrencyLimiter {
|
struct ConcurrencyLimiter {
|
||||||
// Every request to S3 can be throttled or cancelled, if a certain number of requests per second is exceeded.
|
// Every request to S3 can be throttled or cancelled, if a certain number of requests per second is exceeded.
|
||||||
// Same goes to IAM, which is queried before every S3 request, if enabled. IAM has even lower RPS threshold.
|
// Same goes to IAM, which is queried before every S3 request, if enabled. IAM has even lower RPS threshold.
|
||||||
@@ -733,11 +567,6 @@ impl ConcurrencyLimiter {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn parse(input: &str) -> anyhow::Result<Option<RemoteStorageConfig>> {
|
|
||||||
let toml = input.parse::<toml_edit::Document>().unwrap();
|
|
||||||
RemoteStorageConfig::from_toml(toml.as_item())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_object_name() {
|
fn test_object_name() {
|
||||||
let k = RemotePath::new(Utf8Path::new("a/b/c")).unwrap();
|
let k = RemotePath::new(Utf8Path::new("a/b/c")).unwrap();
|
||||||
@@ -759,77 +588,4 @@ mod tests {
|
|||||||
let err = RemotePath::new(Utf8Path::new("/")).expect_err("Should fail on absolute paths");
|
let err = RemotePath::new(Utf8Path::new("/")).expect_err("Should fail on absolute paths");
|
||||||
assert_eq!(err.to_string(), "Path \"/\" is not relative");
|
assert_eq!(err.to_string(), "Path \"/\" is not relative");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_localfs_config_with_timeout() {
|
|
||||||
let input = "local_path = '.'
|
|
||||||
timeout = '5s'";
|
|
||||||
|
|
||||||
let config = parse(input).unwrap().expect("it exists");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config,
|
|
||||||
RemoteStorageConfig {
|
|
||||||
storage: RemoteStorageKind::LocalFs {
|
|
||||||
local_path: Utf8PathBuf::from(".")
|
|
||||||
},
|
|
||||||
timeout: Duration::from_secs(5)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_s3_parsing() {
|
|
||||||
let toml = "\
|
|
||||||
bucket_name = 'foo-bar'
|
|
||||||
bucket_region = 'eu-central-1'
|
|
||||||
upload_storage_class = 'INTELLIGENT_TIERING'
|
|
||||||
timeout = '7s'
|
|
||||||
";
|
|
||||||
|
|
||||||
let config = parse(toml).unwrap().expect("it exists");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config,
|
|
||||||
RemoteStorageConfig {
|
|
||||||
storage: RemoteStorageKind::AwsS3(S3Config {
|
|
||||||
bucket_name: "foo-bar".into(),
|
|
||||||
bucket_region: "eu-central-1".into(),
|
|
||||||
prefix_in_bucket: None,
|
|
||||||
endpoint: None,
|
|
||||||
concurrency_limit: default_remote_storage_s3_concurrency_limit(),
|
|
||||||
max_keys_per_list_response: DEFAULT_MAX_KEYS_PER_LIST_RESPONSE,
|
|
||||||
upload_storage_class: Some(StorageClass::IntelligentTiering),
|
|
||||||
}),
|
|
||||||
timeout: Duration::from_secs(7)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_azure_parsing() {
|
|
||||||
let toml = "\
|
|
||||||
container_name = 'foo-bar'
|
|
||||||
container_region = 'westeurope'
|
|
||||||
upload_storage_class = 'INTELLIGENT_TIERING'
|
|
||||||
timeout = '7s'
|
|
||||||
";
|
|
||||||
|
|
||||||
let config = parse(toml).unwrap().expect("it exists");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config,
|
|
||||||
RemoteStorageConfig {
|
|
||||||
storage: RemoteStorageKind::AzureContainer(AzureConfig {
|
|
||||||
container_name: "foo-bar".into(),
|
|
||||||
storage_account: None,
|
|
||||||
container_region: "westeurope".into(),
|
|
||||||
prefix_in_container: None,
|
|
||||||
concurrency_limit: default_remote_storage_azure_concurrency_limit(),
|
|
||||||
max_keys_per_list_response: DEFAULT_MAX_KEYS_PER_LIST_RESPONSE,
|
|
||||||
}),
|
|
||||||
timeout: Duration::from_secs(7)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ use utils::backoff;
|
|||||||
|
|
||||||
use super::StorageMetadata;
|
use super::StorageMetadata;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::S3Config,
|
||||||
error::Cancelled,
|
error::Cancelled,
|
||||||
metrics::{start_counting_cancelled_wait, start_measuring_requests},
|
metrics::{start_counting_cancelled_wait, start_measuring_requests},
|
||||||
support::PermitCarrying,
|
support::PermitCarrying,
|
||||||
ConcurrencyLimiter, Download, DownloadError, Listing, ListingMode, RemotePath, RemoteStorage,
|
ConcurrencyLimiter, Download, DownloadError, Listing, ListingMode, RemotePath, RemoteStorage,
|
||||||
S3Config, TimeTravelError, TimeoutOrCancel, MAX_KEYS_PER_DELETE,
|
TimeTravelError, TimeoutOrCancel, MAX_KEYS_PER_DELETE, REMOTE_STORAGE_PREFIX_SEPARATOR,
|
||||||
REMOTE_STORAGE_PREFIX_SEPARATOR,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::metrics::AttemptOutcome;
|
use crate::metrics::AttemptOutcome;
|
||||||
|
|||||||
@@ -9,20 +9,11 @@ use serde::{Deserialize, Serialize};
|
|||||||
/// numbers are used.
|
/// numbers are used.
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
||||||
pub enum Generation {
|
pub enum Generation {
|
||||||
// Generations with this magic value will not add a suffix to S3 keys, and will not
|
// The None Generation is used in the metadata of layers written before generations were
|
||||||
// be included in persisted index_part.json. This value is only to be used
|
// introduced. A running Tenant always has a valid generation, but the layer metadata may
|
||||||
// during migration from pre-generation metadata to generation-aware metadata,
|
// include None generations.
|
||||||
// and should eventually go away.
|
|
||||||
//
|
|
||||||
// A special Generation is used rather than always wrapping Generation in an Option,
|
|
||||||
// so that code handling generations doesn't have to be aware of the legacy
|
|
||||||
// case everywhere it touches a generation.
|
|
||||||
None,
|
None,
|
||||||
// Generations with this magic value may never be used to construct S3 keys:
|
|
||||||
// we will panic if someone tries to. This is for Tenants in the "Broken" state,
|
|
||||||
// so that we can satisfy their constructor with a Generation without risking
|
|
||||||
// a code bug using it in an S3 write (broken tenants should never write)
|
|
||||||
Broken,
|
|
||||||
Valid(u32),
|
Valid(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,11 +33,6 @@ impl Generation {
|
|||||||
Self::None
|
Self::None
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new generation that will panic if you try to use get_suffix
|
|
||||||
pub fn broken() -> Self {
|
|
||||||
Self::Broken
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn new(v: u32) -> Self {
|
pub const fn new(v: u32) -> Self {
|
||||||
Self::Valid(v)
|
Self::Valid(v)
|
||||||
}
|
}
|
||||||
@@ -60,9 +46,6 @@ impl Generation {
|
|||||||
match self {
|
match self {
|
||||||
Self::Valid(v) => GenerationFileSuffix(Some(*v)),
|
Self::Valid(v) => GenerationFileSuffix(Some(*v)),
|
||||||
Self::None => GenerationFileSuffix(None),
|
Self::None => GenerationFileSuffix(None),
|
||||||
Self::Broken => {
|
|
||||||
panic!("Tried to use a broken generation");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +69,6 @@ impl Generation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::None => Self::None,
|
Self::None => Self::None,
|
||||||
Self::Broken => panic!("Attempted to use a broken generation"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +77,6 @@ impl Generation {
|
|||||||
match self {
|
match self {
|
||||||
Self::Valid(n) => Self::Valid(*n + 1),
|
Self::Valid(n) => Self::Valid(*n + 1),
|
||||||
Self::None => Self::Valid(1),
|
Self::None => Self::Valid(1),
|
||||||
Self::Broken => panic!("Attempted to use a broken generation"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +109,7 @@ impl Serialize for Generation {
|
|||||||
if let Self::Valid(v) = self {
|
if let Self::Valid(v) = self {
|
||||||
v.serialize(serializer)
|
v.serialize(serializer)
|
||||||
} else {
|
} else {
|
||||||
// We should never be asked to serialize a None or Broken. Structures
|
// We should never be asked to serialize a None. Structures
|
||||||
// that include an optional generation should convert None to an
|
// that include an optional generation should convert None to an
|
||||||
// Option<Generation>::None
|
// Option<Generation>::None
|
||||||
Err(serde::ser::Error::custom(
|
Err(serde::ser::Error::custom(
|
||||||
@@ -159,9 +140,6 @@ impl Debug for Generation {
|
|||||||
Self::None => {
|
Self::None => {
|
||||||
write!(f, "<none>")
|
write!(f, "<none>")
|
||||||
}
|
}
|
||||||
Self::Broken => {
|
|
||||||
write!(f, "<broken>")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,22 +8,15 @@ use super::error::ApiError;
|
|||||||
pub async fn json_request<T: for<'de> Deserialize<'de>>(
|
pub async fn json_request<T: for<'de> Deserialize<'de>>(
|
||||||
request: &mut Request<Body>,
|
request: &mut Request<Body>,
|
||||||
) -> Result<T, ApiError> {
|
) -> Result<T, ApiError> {
|
||||||
json_request_or_empty_body(request)
|
|
||||||
.await?
|
|
||||||
.context("missing request body")
|
|
||||||
.map_err(ApiError::BadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Will be removed as part of <https://github.com/neondatabase/neon/issues/4282>
|
|
||||||
pub async fn json_request_or_empty_body<T: for<'de> Deserialize<'de>>(
|
|
||||||
request: &mut Request<Body>,
|
|
||||||
) -> Result<Option<T>, ApiError> {
|
|
||||||
let body = hyper::body::aggregate(request.body_mut())
|
let body = hyper::body::aggregate(request.body_mut())
|
||||||
.await
|
.await
|
||||||
.context("Failed to read request body")
|
.context("Failed to read request body")
|
||||||
.map_err(ApiError::BadRequest)?;
|
.map_err(ApiError::BadRequest)?;
|
||||||
|
|
||||||
if body.remaining() == 0 {
|
if body.remaining() == 0 {
|
||||||
return Ok(None);
|
return Err(ApiError::BadRequest(anyhow::anyhow!(
|
||||||
|
"missing request body"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut deser = serde_json::de::Deserializer::from_reader(body.reader());
|
let mut deser = serde_json::de::Deserializer::from_reader(body.reader());
|
||||||
@@ -31,7 +24,6 @@ pub async fn json_request_or_empty_body<T: for<'de> Deserialize<'de>>(
|
|||||||
serde_path_to_error::deserialize(&mut deser)
|
serde_path_to_error::deserialize(&mut deser)
|
||||||
// intentionally stringify because the debug version is not helpful in python logs
|
// intentionally stringify because the debug version is not helpful in python logs
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to parse json request: {e}"))
|
.map_err(|e| anyhow::anyhow!("Failed to parse json request: {e}"))
|
||||||
.map(Some)
|
|
||||||
.map_err(ApiError::BadRequest)
|
.map_err(ApiError::BadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(clippy::todo)]
|
||||||
|
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
//! medium/128 time: [8.8311 ms 8.9849 ms 9.1263 ms]
|
//! medium/128 time: [8.8311 ms 8.9849 ms 9.1263 ms]
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
use bytes::{Buf, Bytes};
|
use bytes::{Buf, Bytes};
|
||||||
use criterion::{BenchmarkId, Criterion};
|
use criterion::{BenchmarkId, Criterion};
|
||||||
use pageserver::{config::PageServerConf, walrecord::NeonWalRecord, walredo::PostgresRedoManager};
|
use pageserver::{config::PageServerConf, walrecord::NeonWalRecord, walredo::PostgresRedoManager};
|
||||||
@@ -188,6 +189,7 @@ impl Request {
|
|||||||
manager
|
manager
|
||||||
.request_redo(*key, *lsn, base_img.clone(), records.clone(), *pg_version)
|
.request_redo(*key, *lsn, base_img.clone(), records.clone(), *pg_version)
|
||||||
.await
|
.await
|
||||||
|
.context("request_redo")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pg_record(will_init: bool, bytes: &'static [u8]) -> NeonWalRecord {
|
fn pg_record(will_init: bool, bytes: &'static [u8]) -> NeonWalRecord {
|
||||||
|
|||||||
@@ -83,10 +83,18 @@ fn parse_filename(name: &str) -> (Range<Key>, Range<Lsn>) {
|
|||||||
let keys: Vec<&str> = split[0].split('-').collect();
|
let keys: Vec<&str> = split[0].split('-').collect();
|
||||||
let mut lsns: Vec<&str> = split[1].split('-').collect();
|
let mut lsns: Vec<&str> = split[1].split('-').collect();
|
||||||
|
|
||||||
|
// The current format of the layer file name: 000000067F0000000400000B150100000000-000000067F0000000400000D350100000000__00000000014B7AC8-v1-00000001
|
||||||
|
|
||||||
|
// Handle generation number `-00000001` part
|
||||||
if lsns.last().expect("should").len() == 8 {
|
if lsns.last().expect("should").len() == 8 {
|
||||||
lsns.pop();
|
lsns.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle version number `-v1` part
|
||||||
|
if lsns.last().expect("should").starts_with('v') {
|
||||||
|
lsns.pop();
|
||||||
|
}
|
||||||
|
|
||||||
if lsns.len() == 1 {
|
if lsns.len() == 1 {
|
||||||
lsns.push(lsns[0]);
|
lsns.push(lsns[0]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,15 +33,10 @@ use utils::{
|
|||||||
use crate::tenant::timeline::GetVectoredImpl;
|
use crate::tenant::timeline::GetVectoredImpl;
|
||||||
use crate::tenant::vectored_blob_io::MaxVectoredReadBytes;
|
use crate::tenant::vectored_blob_io::MaxVectoredReadBytes;
|
||||||
use crate::tenant::{config::TenantConfOpt, timeline::GetImpl};
|
use crate::tenant::{config::TenantConfOpt, timeline::GetImpl};
|
||||||
use crate::tenant::{
|
use crate::tenant::{TENANTS_SEGMENT_NAME, TIMELINES_SEGMENT_NAME};
|
||||||
TENANTS_SEGMENT_NAME, TENANT_DELETED_MARKER_FILE_NAME, TIMELINES_SEGMENT_NAME,
|
|
||||||
};
|
|
||||||
use crate::{disk_usage_eviction_task::DiskUsageEvictionTaskConfig, virtual_file::io_engine};
|
use crate::{disk_usage_eviction_task::DiskUsageEvictionTaskConfig, virtual_file::io_engine};
|
||||||
use crate::{tenant::config::TenantConf, virtual_file};
|
use crate::{tenant::config::TenantConf, virtual_file};
|
||||||
use crate::{
|
use crate::{TENANT_HEATMAP_BASENAME, TENANT_LOCATION_CONFIG_NAME, TIMELINE_DELETE_MARK_SUFFIX};
|
||||||
TENANT_CONFIG_NAME, TENANT_HEATMAP_BASENAME, TENANT_LOCATION_CONFIG_NAME,
|
|
||||||
TIMELINE_DELETE_MARK_SUFFIX,
|
|
||||||
};
|
|
||||||
|
|
||||||
use self::defaults::DEFAULT_CONCURRENT_TENANT_WARMUP;
|
use self::defaults::DEFAULT_CONCURRENT_TENANT_WARMUP;
|
||||||
|
|
||||||
@@ -812,15 +807,11 @@ impl PageServerConf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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 LocationConf be stored.
|
||||||
///
|
pub(crate) fn tenant_location_config_path(
|
||||||
/// Legacy: superseded by tenant_location_config_path. Eventually
|
&self,
|
||||||
/// remove this function.
|
tenant_shard_id: &TenantShardId,
|
||||||
pub fn tenant_config_path(&self, tenant_shard_id: &TenantShardId) -> Utf8PathBuf {
|
) -> Utf8PathBuf {
|
||||||
self.tenant_path(tenant_shard_id).join(TENANT_CONFIG_NAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tenant_location_config_path(&self, tenant_shard_id: &TenantShardId) -> Utf8PathBuf {
|
|
||||||
self.tenant_path(tenant_shard_id)
|
self.tenant_path(tenant_shard_id)
|
||||||
.join(TENANT_LOCATION_CONFIG_NAME)
|
.join(TENANT_LOCATION_CONFIG_NAME)
|
||||||
}
|
}
|
||||||
@@ -855,14 +846,6 @@ impl PageServerConf {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn tenant_deleted_mark_file_path(
|
|
||||||
&self,
|
|
||||||
tenant_shard_id: &TenantShardId,
|
|
||||||
) -> Utf8PathBuf {
|
|
||||||
self.tenant_path(tenant_shard_id)
|
|
||||||
.join(TENANT_DELETED_MARKER_FILE_NAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn traces_path(&self) -> Utf8PathBuf {
|
pub fn traces_path(&self) -> Utf8PathBuf {
|
||||||
self.workdir.join("traces")
|
self.workdir.join("traces")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -382,17 +382,6 @@ pub enum DeletionQueueError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DeletionQueueClient {
|
impl DeletionQueueClient {
|
||||||
pub(crate) fn broken() -> Self {
|
|
||||||
// Channels whose receivers are immediately dropped.
|
|
||||||
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
|
|
||||||
let (executor_tx, _executor_rx) = tokio::sync::mpsc::channel(1);
|
|
||||||
Self {
|
|
||||||
tx,
|
|
||||||
executor_tx,
|
|
||||||
lsn_table: Arc::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This is cancel-safe. If you drop the future before it completes, the message
|
/// This is cancel-safe. If you drop the future before it completes, the message
|
||||||
/// is not pushed, although in the context of the deletion queue it doesn't matter: once
|
/// is not pushed, although in the context of the deletion queue it doesn't matter: once
|
||||||
/// we decide to do a deletion the decision is always final.
|
/// we decide to do a deletion the decision is always final.
|
||||||
|
|||||||
@@ -236,6 +236,13 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
description: A timestamp to get the LSN
|
description: A timestamp to get the LSN
|
||||||
|
- name: with_lease
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
description: Whether to grant a lease to the corresponding LSN. Default to false.
|
||||||
|
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
@@ -360,16 +367,7 @@ paths:
|
|||||||
$ref: "#/components/schemas/TenantLocationConfigResponse"
|
$ref: "#/components/schemas/TenantLocationConfigResponse"
|
||||||
"409":
|
"409":
|
||||||
description: |
|
description: |
|
||||||
The tenant is already known to Pageserver in some way,
|
The tenant is already being modified, perhaps by a concurrent call to this API
|
||||||
and hence this `/attach` call has been rejected.
|
|
||||||
|
|
||||||
Some examples of how this can happen:
|
|
||||||
- tenant was created on this pageserver
|
|
||||||
- tenant attachment was started by an earlier call to `/attach`.
|
|
||||||
|
|
||||||
Callers should poll the tenant status's `attachment_status` field,
|
|
||||||
like for status 202. See the longer description for `POST /attach`
|
|
||||||
for details.
|
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@@ -755,8 +753,6 @@ components:
|
|||||||
For example this can be caused by s3 being unreachable. The retry may be implemented
|
For example this can be caused by s3 being unreachable. The retry may be implemented
|
||||||
with call to detach, though it would be better to not automate it and inspec failed state
|
with call to detach, though it would be better to not automate it and inspec failed state
|
||||||
manually before proceeding with a retry.
|
manually before proceeding with a retry.
|
||||||
|
|
||||||
See the tenant `/attach` endpoint for more information.
|
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- slug
|
- slug
|
||||||
@@ -1029,6 +1025,10 @@ components:
|
|||||||
kind:
|
kind:
|
||||||
type: string
|
type: string
|
||||||
enum: [past, present, future, nodata]
|
enum: [past, present, future, nodata]
|
||||||
|
valid_until:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: The expiration time of the granted lease.
|
||||||
|
|
||||||
LsnLease:
|
LsnLease:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use pageserver_api::models::IngestAuxFilesRequest;
|
|||||||
use pageserver_api::models::ListAuxFilesRequest;
|
use pageserver_api::models::ListAuxFilesRequest;
|
||||||
use pageserver_api::models::LocationConfig;
|
use pageserver_api::models::LocationConfig;
|
||||||
use pageserver_api::models::LocationConfigListResponse;
|
use pageserver_api::models::LocationConfigListResponse;
|
||||||
|
use pageserver_api::models::LsnLease;
|
||||||
use pageserver_api::models::ShardParameters;
|
use pageserver_api::models::ShardParameters;
|
||||||
use pageserver_api::models::TenantDetails;
|
use pageserver_api::models::TenantDetails;
|
||||||
use pageserver_api::models::TenantLocationConfigResponse;
|
use pageserver_api::models::TenantLocationConfigResponse;
|
||||||
@@ -30,13 +31,11 @@ use pageserver_api::models::TenantShardLocation;
|
|||||||
use pageserver_api::models::TenantShardSplitRequest;
|
use pageserver_api::models::TenantShardSplitRequest;
|
||||||
use pageserver_api::models::TenantShardSplitResponse;
|
use pageserver_api::models::TenantShardSplitResponse;
|
||||||
use pageserver_api::models::TenantSorting;
|
use pageserver_api::models::TenantSorting;
|
||||||
use pageserver_api::models::TenantState;
|
|
||||||
use pageserver_api::models::TopTenantShardItem;
|
use pageserver_api::models::TopTenantShardItem;
|
||||||
use pageserver_api::models::TopTenantShardsRequest;
|
use pageserver_api::models::TopTenantShardsRequest;
|
||||||
use pageserver_api::models::TopTenantShardsResponse;
|
use pageserver_api::models::TopTenantShardsResponse;
|
||||||
use pageserver_api::models::{
|
use pageserver_api::models::{
|
||||||
DownloadRemoteLayersTaskSpawnRequest, LocationConfigMode, TenantAttachRequest,
|
DownloadRemoteLayersTaskSpawnRequest, LocationConfigMode, TenantLocationConfigRequest,
|
||||||
TenantLocationConfigRequest,
|
|
||||||
};
|
};
|
||||||
use pageserver_api::shard::ShardCount;
|
use pageserver_api::shard::ShardCount;
|
||||||
use pageserver_api::shard::TenantShardId;
|
use pageserver_api::shard::TenantShardId;
|
||||||
@@ -50,7 +49,6 @@ use utils::auth::JwtAuth;
|
|||||||
use utils::failpoint_support::failpoints_handler;
|
use utils::failpoint_support::failpoints_handler;
|
||||||
use utils::http::endpoint::prometheus_metrics_handler;
|
use utils::http::endpoint::prometheus_metrics_handler;
|
||||||
use utils::http::endpoint::request_span;
|
use utils::http::endpoint::request_span;
|
||||||
use utils::http::json::json_request_or_empty_body;
|
|
||||||
use utils::http::request::{get_request_param, must_get_query_param, parse_query_param};
|
use utils::http::request::{get_request_param, must_get_query_param, parse_query_param};
|
||||||
|
|
||||||
use crate::context::{DownloadBehavior, RequestContext};
|
use crate::context::{DownloadBehavior, RequestContext};
|
||||||
@@ -329,14 +327,11 @@ impl From<crate::tenant::mgr::DeleteTimelineError> for ApiError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<crate::tenant::delete::DeleteTenantError> for ApiError {
|
impl From<crate::tenant::mgr::DeleteTenantError> for ApiError {
|
||||||
fn from(value: crate::tenant::delete::DeleteTenantError) -> Self {
|
fn from(value: crate::tenant::mgr::DeleteTenantError) -> Self {
|
||||||
use crate::tenant::delete::DeleteTenantError::*;
|
use crate::tenant::mgr::DeleteTenantError::*;
|
||||||
match value {
|
match value {
|
||||||
Get(g) => ApiError::from(g),
|
|
||||||
Timeline(t) => ApiError::from(t),
|
|
||||||
SlotError(e) => e.into(),
|
SlotError(e) => e.into(),
|
||||||
SlotUpsertError(e) => e.into(),
|
|
||||||
Other(o) => ApiError::InternalServerError(o),
|
Other(o) => ApiError::InternalServerError(o),
|
||||||
Cancelled => ApiError::ShuttingDown,
|
Cancelled => ApiError::ShuttingDown,
|
||||||
}
|
}
|
||||||
@@ -731,6 +726,8 @@ async fn get_lsn_by_timestamp_handler(
|
|||||||
.map_err(ApiError::BadRequest)?;
|
.map_err(ApiError::BadRequest)?;
|
||||||
let timestamp_pg = postgres_ffi::to_pg_timestamp(timestamp);
|
let timestamp_pg = postgres_ffi::to_pg_timestamp(timestamp);
|
||||||
|
|
||||||
|
let with_lease = parse_query_param(&request, "with_lease")?.unwrap_or(false);
|
||||||
|
|
||||||
let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Download);
|
let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Download);
|
||||||
|
|
||||||
let timeline =
|
let timeline =
|
||||||
@@ -739,10 +736,15 @@ async fn get_lsn_by_timestamp_handler(
|
|||||||
let result = timeline
|
let result = timeline
|
||||||
.find_lsn_for_timestamp(timestamp_pg, &cancel, &ctx)
|
.find_lsn_for_timestamp(timestamp_pg, &cancel, &ctx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
#[derive(serde::Serialize, Debug)]
|
#[derive(serde::Serialize, Debug)]
|
||||||
struct Result {
|
struct Result {
|
||||||
lsn: Lsn,
|
lsn: Lsn,
|
||||||
kind: &'static str,
|
kind: &'static str,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(flatten)]
|
||||||
|
lease: Option<LsnLease>,
|
||||||
}
|
}
|
||||||
let (lsn, kind) = match result {
|
let (lsn, kind) = match result {
|
||||||
LsnForTimestamp::Present(lsn) => (lsn, "present"),
|
LsnForTimestamp::Present(lsn) => (lsn, "present"),
|
||||||
@@ -750,11 +752,28 @@ async fn get_lsn_by_timestamp_handler(
|
|||||||
LsnForTimestamp::Past(lsn) => (lsn, "past"),
|
LsnForTimestamp::Past(lsn) => (lsn, "past"),
|
||||||
LsnForTimestamp::NoData(lsn) => (lsn, "nodata"),
|
LsnForTimestamp::NoData(lsn) => (lsn, "nodata"),
|
||||||
};
|
};
|
||||||
let result = Result { lsn, kind };
|
|
||||||
|
let lease = if with_lease {
|
||||||
|
timeline
|
||||||
|
.make_lsn_lease(lsn, timeline.get_lsn_lease_length_for_ts(), &ctx)
|
||||||
|
.inspect_err(|_| {
|
||||||
|
warn!("fail to grant a lease to {}", lsn);
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Result { lsn, kind, lease };
|
||||||
|
let valid_until = result
|
||||||
|
.lease
|
||||||
|
.as_ref()
|
||||||
|
.map(|l| humantime::format_rfc3339_millis(l.valid_until).to_string());
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
lsn=?result.lsn,
|
lsn=?result.lsn,
|
||||||
kind=%result.kind,
|
kind=%result.kind,
|
||||||
timestamp=%timestamp_raw,
|
timestamp=%timestamp_raw,
|
||||||
|
valid_until=?valid_until,
|
||||||
"lsn_by_timestamp finished"
|
"lsn_by_timestamp finished"
|
||||||
);
|
);
|
||||||
json_response(StatusCode::OK, result)
|
json_response(StatusCode::OK, result)
|
||||||
@@ -799,58 +818,6 @@ async fn get_timestamp_of_lsn_handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn tenant_attach_handler(
|
|
||||||
mut request: Request<Body>,
|
|
||||||
_cancel: CancellationToken,
|
|
||||||
) -> Result<Response<Body>, ApiError> {
|
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
|
||||||
check_permission(&request, Some(tenant_id))?;
|
|
||||||
|
|
||||||
let maybe_body: Option<TenantAttachRequest> = json_request_or_empty_body(&mut request).await?;
|
|
||||||
let tenant_conf = match &maybe_body {
|
|
||||||
Some(request) => TenantConfOpt::try_from(&*request.config).map_err(ApiError::BadRequest)?,
|
|
||||||
None => TenantConfOpt::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Warn);
|
|
||||||
|
|
||||||
info!("Handling tenant attach {tenant_id}");
|
|
||||||
|
|
||||||
let state = get_state(&request);
|
|
||||||
|
|
||||||
let generation = get_request_generation(state, maybe_body.as_ref().and_then(|r| r.generation))?;
|
|
||||||
|
|
||||||
let tenant_shard_id = TenantShardId::unsharded(tenant_id);
|
|
||||||
let shard_params = ShardParameters::default();
|
|
||||||
let location_conf = LocationConf::attached_single(tenant_conf, generation, &shard_params);
|
|
||||||
|
|
||||||
let tenant = state
|
|
||||||
.tenant_manager
|
|
||||||
.upsert_location(tenant_shard_id, location_conf, None, SpawnMode::Eager, &ctx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let Some(tenant) = tenant else {
|
|
||||||
// This should never happen: indicates a bug in upsert_location
|
|
||||||
return Err(ApiError::InternalServerError(anyhow::anyhow!(
|
|
||||||
"Upsert succeeded but didn't return tenant!"
|
|
||||||
)));
|
|
||||||
};
|
|
||||||
|
|
||||||
// We might have successfully constructed a Tenant, but it could still
|
|
||||||
// end up in a broken state:
|
|
||||||
if let TenantState::Broken {
|
|
||||||
reason,
|
|
||||||
backtrace: _,
|
|
||||||
} = tenant.current_state()
|
|
||||||
{
|
|
||||||
return Err(ApiError::InternalServerError(anyhow::anyhow!(
|
|
||||||
"Tenant state is Broken: {reason}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
json_response(StatusCode::ACCEPTED, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn timeline_delete_handler(
|
async fn timeline_delete_handler(
|
||||||
request: Request<Body>,
|
request: Request<Body>,
|
||||||
_cancel: CancellationToken,
|
_cancel: CancellationToken,
|
||||||
@@ -881,26 +848,6 @@ async fn timeline_delete_handler(
|
|||||||
json_response(StatusCode::ACCEPTED, ())
|
json_response(StatusCode::ACCEPTED, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn tenant_detach_handler(
|
|
||||||
request: Request<Body>,
|
|
||||||
_cancel: CancellationToken,
|
|
||||||
) -> Result<Response<Body>, ApiError> {
|
|
||||||
let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?;
|
|
||||||
check_permission(&request, Some(tenant_id))?;
|
|
||||||
// This is a legacy API (`/location_conf` is the replacement). It only supports unsharded tenants
|
|
||||||
let tenant_shard_id = TenantShardId::unsharded(tenant_id);
|
|
||||||
|
|
||||||
let state = get_state(&request);
|
|
||||||
let conf = state.conf;
|
|
||||||
state
|
|
||||||
.tenant_manager
|
|
||||||
.detach_tenant(conf, tenant_shard_id, &state.deletion_queue_client)
|
|
||||||
.instrument(info_span!("tenant_detach", %tenant_id, shard_id=%tenant_shard_id.shard_slug()))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
json_response(StatusCode::OK, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn tenant_reset_handler(
|
async fn tenant_reset_handler(
|
||||||
request: Request<Body>,
|
request: Request<Body>,
|
||||||
_cancel: CancellationToken,
|
_cancel: CancellationToken,
|
||||||
@@ -940,7 +887,9 @@ async fn tenant_list_handler(
|
|||||||
state: state.clone(),
|
state: state.clone(),
|
||||||
current_physical_size: None,
|
current_physical_size: None,
|
||||||
attachment_status: state.attachment_status(),
|
attachment_status: state.attachment_status(),
|
||||||
generation: (*gen).into(),
|
generation: (*gen)
|
||||||
|
.into()
|
||||||
|
.expect("Tenants are always attached with a generation"),
|
||||||
})
|
})
|
||||||
.collect::<Vec<TenantInfo>>();
|
.collect::<Vec<TenantInfo>>();
|
||||||
|
|
||||||
@@ -988,7 +937,10 @@ async fn tenant_status(
|
|||||||
state: state.clone(),
|
state: state.clone(),
|
||||||
current_physical_size: Some(current_physical_size),
|
current_physical_size: Some(current_physical_size),
|
||||||
attachment_status: state.attachment_status(),
|
attachment_status: state.attachment_status(),
|
||||||
generation: tenant.generation().into(),
|
generation: tenant
|
||||||
|
.generation()
|
||||||
|
.into()
|
||||||
|
.expect("Tenants are always attached with a generation"),
|
||||||
},
|
},
|
||||||
walredo: tenant.wal_redo_manager_status(),
|
walredo: tenant.wal_redo_manager_status(),
|
||||||
timelines: tenant.list_timeline_ids(),
|
timelines: tenant.list_timeline_ids(),
|
||||||
@@ -1705,6 +1657,14 @@ async fn timeline_compact_handler(
|
|||||||
if Some(true) == parse_query_param::<_, bool>(&request, "force_image_layer_creation")? {
|
if Some(true) == parse_query_param::<_, bool>(&request, "force_image_layer_creation")? {
|
||||||
flags |= CompactFlags::ForceImageLayerCreation;
|
flags |= CompactFlags::ForceImageLayerCreation;
|
||||||
}
|
}
|
||||||
|
if Some(true) == parse_query_param::<_, bool>(&request, "enhanced_gc_bottom_most_compaction")? {
|
||||||
|
if !cfg!(feature = "testing") {
|
||||||
|
return Err(ApiError::InternalServerError(anyhow!(
|
||||||
|
"enhanced_gc_bottom_most_compaction is only available in testing mode"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
flags |= CompactFlags::EnhancedGcBottomMostCompaction;
|
||||||
|
}
|
||||||
let wait_until_uploaded =
|
let wait_until_uploaded =
|
||||||
parse_query_param::<_, bool>(&request, "wait_until_uploaded")?.unwrap_or(false);
|
parse_query_param::<_, bool>(&request, "wait_until_uploaded")?.unwrap_or(false);
|
||||||
|
|
||||||
@@ -2689,12 +2649,6 @@ pub fn make_router(
|
|||||||
.post("/v1/tenant/:tenant_shard_id/timeline", |r| {
|
.post("/v1/tenant/:tenant_shard_id/timeline", |r| {
|
||||||
api_handler(r, timeline_create_handler)
|
api_handler(r, timeline_create_handler)
|
||||||
})
|
})
|
||||||
.post("/v1/tenant/:tenant_id/attach", |r| {
|
|
||||||
api_handler(r, tenant_attach_handler)
|
|
||||||
})
|
|
||||||
.post("/v1/tenant/:tenant_id/detach", |r| {
|
|
||||||
api_handler(r, tenant_detach_handler)
|
|
||||||
})
|
|
||||||
.post("/v1/tenant/:tenant_shard_id/reset", |r| {
|
.post("/v1/tenant/:tenant_shard_id/reset", |r| {
|
||||||
api_handler(r, tenant_reset_handler)
|
api_handler(r, tenant_reset_handler)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -113,11 +113,7 @@ pub async fn shutdown_pageserver(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Per-tenant configuration file.
|
/// Per-tenant configuration file.
|
||||||
/// Full path: `tenants/<tenant_id>/config`.
|
/// Full path: `tenants/<tenant_id>/config-v1`.
|
||||||
pub(crate) const TENANT_CONFIG_NAME: &str = "config";
|
|
||||||
|
|
||||||
/// Per-tenant configuration file.
|
|
||||||
/// Full path: `tenants/<tenant_id>/config`.
|
|
||||||
pub(crate) const TENANT_LOCATION_CONFIG_NAME: &str = "config-v1";
|
pub(crate) const TENANT_LOCATION_CONFIG_NAME: &str = "config-v1";
|
||||||
|
|
||||||
/// Per-tenant copy of their remote heatmap, downloaded into the local
|
/// Per-tenant copy of their remote heatmap, downloaded into the local
|
||||||
|
|||||||
@@ -545,6 +545,15 @@ static AUX_FILE_SIZE: Lazy<IntGaugeVec> = Lazy::new(|| {
|
|||||||
.expect("failed to define a metric")
|
.expect("failed to define a metric")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static VALID_LSN_LEASE_COUNT: Lazy<UIntGaugeVec> = Lazy::new(|| {
|
||||||
|
register_uint_gauge_vec!(
|
||||||
|
"pageserver_valid_lsn_lease_count",
|
||||||
|
"The number of valid leases after refreshing gc info.",
|
||||||
|
&["tenant_id", "shard_id", "timeline_id"],
|
||||||
|
)
|
||||||
|
.expect("failed to define a metric")
|
||||||
|
});
|
||||||
|
|
||||||
pub(crate) mod initial_logical_size {
|
pub(crate) mod initial_logical_size {
|
||||||
use metrics::{register_int_counter, register_int_counter_vec, IntCounter, IntCounterVec};
|
use metrics::{register_int_counter, register_int_counter_vec, IntCounter, IntCounterVec};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
@@ -1436,6 +1445,46 @@ pub(crate) static LIVE_CONNECTIONS_COUNT: Lazy<IntGaugeVec> = Lazy::new(|| {
|
|||||||
.expect("failed to define a metric")
|
.expect("failed to define a metric")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, enum_map::Enum, IntoStaticStr)]
|
||||||
|
pub(crate) enum ComputeCommandKind {
|
||||||
|
PageStreamV2,
|
||||||
|
PageStream,
|
||||||
|
Basebackup,
|
||||||
|
GetLastRecordRlsn,
|
||||||
|
Fullbackup,
|
||||||
|
ImportBasebackup,
|
||||||
|
ImportWal,
|
||||||
|
LeaseLsn,
|
||||||
|
Show,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ComputeCommandCounters {
|
||||||
|
map: EnumMap<ComputeCommandKind, IntCounter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) static COMPUTE_COMMANDS_COUNTERS: Lazy<ComputeCommandCounters> = Lazy::new(|| {
|
||||||
|
let inner = register_int_counter_vec!(
|
||||||
|
"pageserver_compute_commands",
|
||||||
|
"Number of compute -> pageserver commands processed",
|
||||||
|
&["command"]
|
||||||
|
)
|
||||||
|
.expect("failed to define a metric");
|
||||||
|
|
||||||
|
ComputeCommandCounters {
|
||||||
|
map: EnumMap::from_array(std::array::from_fn(|i| {
|
||||||
|
let command = <ComputeCommandKind as enum_map::Enum>::from_usize(i);
|
||||||
|
let command_str: &'static str = command.into();
|
||||||
|
inner.with_label_values(&[command_str])
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
impl ComputeCommandCounters {
|
||||||
|
pub(crate) fn for_command(&self, command: ComputeCommandKind) -> &IntCounter {
|
||||||
|
&self.map[command]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// remote storage metrics
|
// remote storage metrics
|
||||||
|
|
||||||
static REMOTE_TIMELINE_CLIENT_CALLS: Lazy<IntCounterPairVec> = Lazy::new(|| {
|
static REMOTE_TIMELINE_CLIENT_CALLS: Lazy<IntCounterPairVec> = Lazy::new(|| {
|
||||||
@@ -2055,6 +2104,8 @@ pub(crate) struct TimelineMetrics {
|
|||||||
pub directory_entries_count_gauge: Lazy<UIntGauge, Box<dyn Send + Fn() -> UIntGauge>>,
|
pub directory_entries_count_gauge: Lazy<UIntGauge, Box<dyn Send + Fn() -> UIntGauge>>,
|
||||||
pub evictions: IntCounter,
|
pub evictions: IntCounter,
|
||||||
pub evictions_with_low_residence_duration: std::sync::RwLock<EvictionsWithLowResidenceDuration>,
|
pub evictions_with_low_residence_duration: std::sync::RwLock<EvictionsWithLowResidenceDuration>,
|
||||||
|
/// Number of valid LSN leases.
|
||||||
|
pub valid_lsn_lease_count_gauge: UIntGauge,
|
||||||
shutdown: std::sync::atomic::AtomicBool,
|
shutdown: std::sync::atomic::AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2153,6 +2204,10 @@ impl TimelineMetrics {
|
|||||||
let evictions_with_low_residence_duration = evictions_with_low_residence_duration_builder
|
let evictions_with_low_residence_duration = evictions_with_low_residence_duration_builder
|
||||||
.build(&tenant_id, &shard_id, &timeline_id);
|
.build(&tenant_id, &shard_id, &timeline_id);
|
||||||
|
|
||||||
|
let valid_lsn_lease_count_gauge = VALID_LSN_LEASE_COUNT
|
||||||
|
.get_metric_with_label_values(&[&tenant_id, &shard_id, &timeline_id])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
TimelineMetrics {
|
TimelineMetrics {
|
||||||
tenant_id,
|
tenant_id,
|
||||||
shard_id,
|
shard_id,
|
||||||
@@ -2175,6 +2230,7 @@ impl TimelineMetrics {
|
|||||||
evictions_with_low_residence_duration: std::sync::RwLock::new(
|
evictions_with_low_residence_duration: std::sync::RwLock::new(
|
||||||
evictions_with_low_residence_duration,
|
evictions_with_low_residence_duration,
|
||||||
),
|
),
|
||||||
|
valid_lsn_lease_count_gauge,
|
||||||
shutdown: std::sync::atomic::AtomicBool::default(),
|
shutdown: std::sync::atomic::AtomicBool::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2224,6 +2280,7 @@ impl TimelineMetrics {
|
|||||||
}
|
}
|
||||||
let _ = EVICTIONS.remove_label_values(&[tenant_id, shard_id, timeline_id]);
|
let _ = EVICTIONS.remove_label_values(&[tenant_id, shard_id, timeline_id]);
|
||||||
let _ = AUX_FILE_SIZE.remove_label_values(&[tenant_id, shard_id, timeline_id]);
|
let _ = AUX_FILE_SIZE.remove_label_values(&[tenant_id, shard_id, timeline_id]);
|
||||||
|
let _ = VALID_LSN_LEASE_COUNT.remove_label_values(&[tenant_id, shard_id, timeline_id]);
|
||||||
|
|
||||||
self.evictions_with_low_residence_duration
|
self.evictions_with_low_residence_duration
|
||||||
.write()
|
.write()
|
||||||
@@ -2932,4 +2989,5 @@ pub fn preinitialize_metrics() {
|
|||||||
Lazy::force(&RECONSTRUCT_TIME);
|
Lazy::force(&RECONSTRUCT_TIME);
|
||||||
Lazy::force(&tenant_throttling::TIMELINE_GET);
|
Lazy::force(&tenant_throttling::TIMELINE_GET);
|
||||||
Lazy::force(&BASEBACKUP_QUERY_TIME);
|
Lazy::force(&BASEBACKUP_QUERY_TIME);
|
||||||
|
Lazy::force(&COMPUTE_COMMANDS_COUNTERS);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ use crate::basebackup::BasebackupError;
|
|||||||
use crate::context::{DownloadBehavior, RequestContext};
|
use crate::context::{DownloadBehavior, RequestContext};
|
||||||
use crate::import_datadir::import_wal_from_tar;
|
use crate::import_datadir::import_wal_from_tar;
|
||||||
use crate::metrics;
|
use crate::metrics;
|
||||||
use crate::metrics::LIVE_CONNECTIONS_COUNT;
|
use crate::metrics::{ComputeCommandKind, COMPUTE_COMMANDS_COUNTERS, LIVE_CONNECTIONS_COUNT};
|
||||||
use crate::pgdatadir_mapping::Version;
|
use crate::pgdatadir_mapping::Version;
|
||||||
use crate::span::debug_assert_current_span_has_tenant_and_timeline_id;
|
use crate::span::debug_assert_current_span_has_tenant_and_timeline_id;
|
||||||
use crate::span::debug_assert_current_span_has_tenant_and_timeline_id_no_shard_id;
|
use crate::span::debug_assert_current_span_has_tenant_and_timeline_id_no_shard_id;
|
||||||
@@ -1554,6 +1554,10 @@ where
|
|||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
|
COMPUTE_COMMANDS_COUNTERS
|
||||||
|
.for_command(ComputeCommandKind::PageStreamV2)
|
||||||
|
.inc();
|
||||||
|
|
||||||
self.handle_pagerequests(
|
self.handle_pagerequests(
|
||||||
pgb,
|
pgb,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
@@ -1579,6 +1583,10 @@ where
|
|||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
|
COMPUTE_COMMANDS_COUNTERS
|
||||||
|
.for_command(ComputeCommandKind::PageStream)
|
||||||
|
.inc();
|
||||||
|
|
||||||
self.handle_pagerequests(
|
self.handle_pagerequests(
|
||||||
pgb,
|
pgb,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
@@ -1605,6 +1613,10 @@ where
|
|||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
|
COMPUTE_COMMANDS_COUNTERS
|
||||||
|
.for_command(ComputeCommandKind::Basebackup)
|
||||||
|
.inc();
|
||||||
|
|
||||||
let lsn = if let Some(lsn_str) = params.get(2) {
|
let lsn = if let Some(lsn_str) = params.get(2) {
|
||||||
Some(
|
Some(
|
||||||
Lsn::from_str(lsn_str)
|
Lsn::from_str(lsn_str)
|
||||||
@@ -1662,6 +1674,11 @@ where
|
|||||||
.record("timeline_id", field::display(timeline_id));
|
.record("timeline_id", field::display(timeline_id));
|
||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
|
COMPUTE_COMMANDS_COUNTERS
|
||||||
|
.for_command(ComputeCommandKind::GetLastRecordRlsn)
|
||||||
|
.inc();
|
||||||
|
|
||||||
async {
|
async {
|
||||||
let timeline = self
|
let timeline = self
|
||||||
.get_active_tenant_timeline(tenant_id, timeline_id, ShardSelector::Zero)
|
.get_active_tenant_timeline(tenant_id, timeline_id, ShardSelector::Zero)
|
||||||
@@ -1723,6 +1740,10 @@ where
|
|||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
|
COMPUTE_COMMANDS_COUNTERS
|
||||||
|
.for_command(ComputeCommandKind::Fullbackup)
|
||||||
|
.inc();
|
||||||
|
|
||||||
// Check that the timeline exists
|
// Check that the timeline exists
|
||||||
self.handle_basebackup_request(
|
self.handle_basebackup_request(
|
||||||
pgb,
|
pgb,
|
||||||
@@ -1771,6 +1792,10 @@ where
|
|||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
|
COMPUTE_COMMANDS_COUNTERS
|
||||||
|
.for_command(ComputeCommandKind::ImportBasebackup)
|
||||||
|
.inc();
|
||||||
|
|
||||||
match self
|
match self
|
||||||
.handle_import_basebackup(
|
.handle_import_basebackup(
|
||||||
pgb,
|
pgb,
|
||||||
@@ -1818,6 +1843,10 @@ where
|
|||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
|
COMPUTE_COMMANDS_COUNTERS
|
||||||
|
.for_command(ComputeCommandKind::ImportWal)
|
||||||
|
.inc();
|
||||||
|
|
||||||
match self
|
match self
|
||||||
.handle_import_wal(pgb, tenant_id, timeline_id, start_lsn, end_lsn, ctx)
|
.handle_import_wal(pgb, tenant_id, timeline_id, start_lsn, end_lsn, ctx)
|
||||||
.await
|
.await
|
||||||
@@ -1855,6 +1884,10 @@ where
|
|||||||
|
|
||||||
self.check_permission(Some(tenant_shard_id.tenant_id))?;
|
self.check_permission(Some(tenant_shard_id.tenant_id))?;
|
||||||
|
|
||||||
|
COMPUTE_COMMANDS_COUNTERS
|
||||||
|
.for_command(ComputeCommandKind::LeaseLsn)
|
||||||
|
.inc();
|
||||||
|
|
||||||
// The caller is responsible for providing correct lsn.
|
// The caller is responsible for providing correct lsn.
|
||||||
let lsn = Lsn::from_str(params[2])
|
let lsn = 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]))?;
|
||||||
@@ -1886,6 +1919,10 @@ where
|
|||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
|
COMPUTE_COMMANDS_COUNTERS
|
||||||
|
.for_command(ComputeCommandKind::Show)
|
||||||
|
.inc();
|
||||||
|
|
||||||
let tenant = self
|
let tenant = self
|
||||||
.get_active_tenant_with_timeout(
|
.get_active_tenant_with_timeout(
|
||||||
tenant_id,
|
tenant_id,
|
||||||
|
|||||||
+273
-204
@@ -55,11 +55,9 @@ use self::config::AttachedLocationConfig;
|
|||||||
use self::config::AttachmentMode;
|
use self::config::AttachmentMode;
|
||||||
use self::config::LocationConf;
|
use self::config::LocationConf;
|
||||||
use self::config::TenantConf;
|
use self::config::TenantConf;
|
||||||
use self::delete::DeleteTenantFlow;
|
|
||||||
use self::metadata::TimelineMetadata;
|
use self::metadata::TimelineMetadata;
|
||||||
use self::mgr::GetActiveTenantError;
|
use self::mgr::GetActiveTenantError;
|
||||||
use self::mgr::GetTenantError;
|
use self::mgr::GetTenantError;
|
||||||
use self::mgr::TenantsMap;
|
|
||||||
use self::remote_timeline_client::upload::upload_index_part;
|
use self::remote_timeline_client::upload::upload_index_part;
|
||||||
use self::remote_timeline_client::RemoteTimelineClient;
|
use self::remote_timeline_client::RemoteTimelineClient;
|
||||||
use self::timeline::uninit::TimelineCreateGuard;
|
use self::timeline::uninit::TimelineCreateGuard;
|
||||||
@@ -90,6 +88,7 @@ use crate::tenant::remote_timeline_client::MaybeDeletedIndexPart;
|
|||||||
use crate::tenant::remote_timeline_client::INITDB_PATH;
|
use crate::tenant::remote_timeline_client::INITDB_PATH;
|
||||||
use crate::tenant::storage_layer::DeltaLayer;
|
use crate::tenant::storage_layer::DeltaLayer;
|
||||||
use crate::tenant::storage_layer::ImageLayer;
|
use crate::tenant::storage_layer::ImageLayer;
|
||||||
|
use crate::walredo;
|
||||||
use crate::InitializationOrder;
|
use crate::InitializationOrder;
|
||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
@@ -137,7 +136,6 @@ pub mod remote_timeline_client;
|
|||||||
pub mod storage_layer;
|
pub mod storage_layer;
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod delete;
|
|
||||||
pub mod mgr;
|
pub mod mgr;
|
||||||
pub mod secondary;
|
pub mod secondary;
|
||||||
pub mod tasks;
|
pub mod tasks;
|
||||||
@@ -161,8 +159,6 @@ pub const TENANTS_SEGMENT_NAME: &str = "tenants";
|
|||||||
/// Parts of the `.neon/tenants/<tenant_id>/timelines/<timeline_id>` directory prefix.
|
/// Parts of the `.neon/tenants/<tenant_id>/timelines/<timeline_id>` directory prefix.
|
||||||
pub const TIMELINES_SEGMENT_NAME: &str = "timelines";
|
pub const TIMELINES_SEGMENT_NAME: &str = "timelines";
|
||||||
|
|
||||||
pub const TENANT_DELETED_MARKER_FILE_NAME: &str = "deleted";
|
|
||||||
|
|
||||||
/// References to shared objects that are passed into each tenant, such
|
/// References to shared objects that are passed into each tenant, such
|
||||||
/// as the shared remote storage client and process initialization state.
|
/// as the shared remote storage client and process initialization state.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -207,7 +203,6 @@ struct TimelinePreload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct TenantPreload {
|
pub(crate) struct TenantPreload {
|
||||||
deleting: bool,
|
|
||||||
timelines: HashMap<TimelineId, TimelinePreload>,
|
timelines: HashMap<TimelineId, TimelinePreload>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,8 +281,6 @@ pub struct Tenant {
|
|||||||
/// background warmup.
|
/// background warmup.
|
||||||
pub(crate) activate_now_sem: tokio::sync::Semaphore,
|
pub(crate) activate_now_sem: tokio::sync::Semaphore,
|
||||||
|
|
||||||
pub(crate) delete_progress: Arc<tokio::sync::Mutex<DeleteTenantFlow>>,
|
|
||||||
|
|
||||||
// Cancellation token fires when we have entered shutdown(). This is a parent of
|
// Cancellation token fires when we have entered shutdown(). This is a parent of
|
||||||
// Timelines' cancellation token.
|
// Timelines' cancellation token.
|
||||||
pub(crate) cancel: CancellationToken,
|
pub(crate) cancel: CancellationToken,
|
||||||
@@ -331,6 +324,16 @@ impl From<harness::TestRedoManager> for WalRedoManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl WalRedoManager {
|
impl WalRedoManager {
|
||||||
|
pub(crate) async fn shutdown(&self) {
|
||||||
|
match self {
|
||||||
|
Self::Prod(mgr) => mgr.shutdown().await,
|
||||||
|
#[cfg(test)]
|
||||||
|
Self::Test(_) => {
|
||||||
|
// Not applicable to test redo manager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn maybe_quiesce(&self, idle_timeout: Duration) {
|
pub(crate) fn maybe_quiesce(&self, idle_timeout: Duration) {
|
||||||
match self {
|
match self {
|
||||||
Self::Prod(mgr) => mgr.maybe_quiesce(idle_timeout),
|
Self::Prod(mgr) => mgr.maybe_quiesce(idle_timeout),
|
||||||
@@ -351,7 +354,7 @@ impl WalRedoManager {
|
|||||||
base_img: Option<(Lsn, bytes::Bytes)>,
|
base_img: Option<(Lsn, bytes::Bytes)>,
|
||||||
records: Vec<(Lsn, crate::walrecord::NeonWalRecord)>,
|
records: Vec<(Lsn, crate::walrecord::NeonWalRecord)>,
|
||||||
pg_version: u32,
|
pg_version: u32,
|
||||||
) -> anyhow::Result<bytes::Bytes> {
|
) -> Result<bytes::Bytes, walredo::Error> {
|
||||||
match self {
|
match self {
|
||||||
Self::Prod(mgr) => {
|
Self::Prod(mgr) => {
|
||||||
mgr.request_redo(key, lsn, base_img, records, pg_version)
|
mgr.request_redo(key, lsn, base_img, records, pg_version)
|
||||||
@@ -654,10 +657,9 @@ impl Tenant {
|
|||||||
attached_conf: AttachedTenantConf,
|
attached_conf: AttachedTenantConf,
|
||||||
shard_identity: ShardIdentity,
|
shard_identity: ShardIdentity,
|
||||||
init_order: Option<InitializationOrder>,
|
init_order: Option<InitializationOrder>,
|
||||||
tenants: &'static std::sync::RwLock<TenantsMap>,
|
|
||||||
mode: SpawnMode,
|
mode: SpawnMode,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<Arc<Tenant>> {
|
) -> Arc<Tenant> {
|
||||||
let wal_redo_manager = Arc::new(WalRedoManager::from(PostgresRedoManager::new(
|
let wal_redo_manager = Arc::new(WalRedoManager::from(PostgresRedoManager::new(
|
||||||
conf,
|
conf,
|
||||||
tenant_shard_id,
|
tenant_shard_id,
|
||||||
@@ -828,52 +830,6 @@ impl Tenant {
|
|||||||
// Remote preload is complete.
|
// Remote preload is complete.
|
||||||
drop(remote_load_completion);
|
drop(remote_load_completion);
|
||||||
|
|
||||||
let pending_deletion = {
|
|
||||||
match DeleteTenantFlow::should_resume_deletion(
|
|
||||||
conf,
|
|
||||||
preload.as_ref().map(|p| p.deleting).unwrap_or(false),
|
|
||||||
&tenant_clone,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(should_resume_deletion) => should_resume_deletion,
|
|
||||||
Err(err) => {
|
|
||||||
make_broken(&tenant_clone, anyhow::anyhow!(err), BrokenVerbosity::Error);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
info!("pending_deletion {}", pending_deletion.is_some());
|
|
||||||
|
|
||||||
if let Some(deletion) = pending_deletion {
|
|
||||||
// as we are no longer loading, signal completion by dropping
|
|
||||||
// the completion while we resume deletion
|
|
||||||
drop(_completion);
|
|
||||||
let background_jobs_can_start =
|
|
||||||
init_order.as_ref().map(|x| &x.background_jobs_can_start);
|
|
||||||
if let Some(background) = background_jobs_can_start {
|
|
||||||
info!("waiting for backgound jobs barrier");
|
|
||||||
background.clone().wait().await;
|
|
||||||
info!("ready for backgound jobs barrier");
|
|
||||||
}
|
|
||||||
|
|
||||||
let deleted = DeleteTenantFlow::resume_from_attach(
|
|
||||||
deletion,
|
|
||||||
&tenant_clone,
|
|
||||||
preload,
|
|
||||||
tenants,
|
|
||||||
&ctx,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Err(e) = deleted {
|
|
||||||
make_broken(&tenant_clone, anyhow::anyhow!(e), BrokenVerbosity::Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// We will time the duration of the attach phase unless this is a creation (attach will do no work)
|
// We will time the duration of the attach phase unless this is a creation (attach will do no work)
|
||||||
let attached = {
|
let attached = {
|
||||||
let _attach_timer = match mode {
|
let _attach_timer = match mode {
|
||||||
@@ -911,7 +867,7 @@ impl Tenant {
|
|||||||
}
|
}
|
||||||
.instrument(tracing::info_span!(parent: None, "attach", tenant_id=%tenant_shard_id.tenant_id, shard_id=%tenant_shard_id.shard_slug(), gen=?generation)),
|
.instrument(tracing::info_span!(parent: None, "attach", tenant_id=%tenant_shard_id.tenant_id, shard_id=%tenant_shard_id.shard_slug(), gen=?generation)),
|
||||||
);
|
);
|
||||||
Ok(tenant)
|
tenant
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
@@ -931,21 +887,13 @@ impl Tenant {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let deleting = other_keys.contains(TENANT_DELETED_MARKER_FILE_NAME);
|
info!("found {} timelines", remote_timeline_ids.len(),);
|
||||||
info!(
|
|
||||||
"found {} timelines, deleting={}",
|
|
||||||
remote_timeline_ids.len(),
|
|
||||||
deleting
|
|
||||||
);
|
|
||||||
|
|
||||||
for k in other_keys {
|
for k in other_keys {
|
||||||
if k != TENANT_DELETED_MARKER_FILE_NAME {
|
warn!("Unexpected non timeline key {k}");
|
||||||
warn!("Unexpected non timeline key {k}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(TenantPreload {
|
Ok(TenantPreload {
|
||||||
deleting,
|
|
||||||
timelines: Self::load_timeline_metadata(
|
timelines: Self::load_timeline_metadata(
|
||||||
self,
|
self,
|
||||||
remote_timeline_ids,
|
remote_timeline_ids,
|
||||||
@@ -974,7 +922,6 @@ impl Tenant {
|
|||||||
let preload = match (preload, mode) {
|
let preload = match (preload, mode) {
|
||||||
(Some(p), _) => p,
|
(Some(p), _) => p,
|
||||||
(None, SpawnMode::Create) => TenantPreload {
|
(None, SpawnMode::Create) => TenantPreload {
|
||||||
deleting: false,
|
|
||||||
timelines: HashMap::new(),
|
timelines: HashMap::new(),
|
||||||
},
|
},
|
||||||
(None, _) => {
|
(None, _) => {
|
||||||
@@ -1211,30 +1158,6 @@ impl Tenant {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a placeholder Tenant object for a broken tenant
|
|
||||||
pub fn create_broken_tenant(
|
|
||||||
conf: &'static PageServerConf,
|
|
||||||
tenant_shard_id: TenantShardId,
|
|
||||||
remote_storage: GenericRemoteStorage,
|
|
||||||
reason: String,
|
|
||||||
) -> Arc<Tenant> {
|
|
||||||
Arc::new(Tenant::new(
|
|
||||||
TenantState::Broken {
|
|
||||||
reason,
|
|
||||||
backtrace: String::new(),
|
|
||||||
},
|
|
||||||
conf,
|
|
||||||
AttachedTenantConf::try_from(LocationConf::default()).unwrap(),
|
|
||||||
// Shard identity isn't meaningful for a broken tenant: it's just a placeholder
|
|
||||||
// to occupy the slot for this TenantShardId.
|
|
||||||
ShardIdentity::broken(tenant_shard_id.shard_number, tenant_shard_id.shard_count),
|
|
||||||
None,
|
|
||||||
tenant_shard_id,
|
|
||||||
remote_storage,
|
|
||||||
DeletionQueueClient::broken(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_timeline_metadata(
|
async fn load_timeline_metadata(
|
||||||
self: &Arc<Tenant>,
|
self: &Arc<Tenant>,
|
||||||
timeline_ids: HashSet<TimelineId>,
|
timeline_ids: HashSet<TimelineId>,
|
||||||
@@ -1941,6 +1864,10 @@ impl Tenant {
|
|||||||
tracing::debug!("Waiting for tasks...");
|
tracing::debug!("Waiting for tasks...");
|
||||||
task_mgr::shutdown_tasks(None, Some(self.tenant_shard_id), None).await;
|
task_mgr::shutdown_tasks(None, Some(self.tenant_shard_id), None).await;
|
||||||
|
|
||||||
|
if let Some(walredo_mgr) = self.walredo_mgr.as_ref() {
|
||||||
|
walredo_mgr.shutdown().await;
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for any in-flight operations to complete
|
// Wait for any in-flight operations to complete
|
||||||
self.gate.close().await;
|
self.gate.close().await;
|
||||||
|
|
||||||
@@ -2215,6 +2142,7 @@ impl Tenant {
|
|||||||
// Upload an index from the parent: this is partly to provide freshness for the
|
// Upload an index from the parent: this is partly to provide freshness for the
|
||||||
// child tenants that will copy it, and partly for general ease-of-debugging: there will
|
// child tenants that will copy it, and partly for general ease-of-debugging: there will
|
||||||
// always be a parent shard index in the same generation as we wrote the child shard index.
|
// always be a parent shard index in the same generation as we wrote the child shard index.
|
||||||
|
tracing::info!(timeline_id=%timeline.timeline_id, "Uploading index");
|
||||||
timeline
|
timeline
|
||||||
.remote_client
|
.remote_client
|
||||||
.schedule_index_upload_for_file_changes()?;
|
.schedule_index_upload_for_file_changes()?;
|
||||||
@@ -2222,12 +2150,14 @@ impl Tenant {
|
|||||||
|
|
||||||
// Shut down the timeline's remote client: this means that the indices we write
|
// Shut down the timeline's remote client: this means that the indices we write
|
||||||
// for child shards will not be invalidated by the parent shard deleting layers.
|
// for child shards will not be invalidated by the parent shard deleting layers.
|
||||||
|
tracing::info!(timeline_id=%timeline.timeline_id, "Shutting down remote storage client");
|
||||||
timeline.remote_client.shutdown().await;
|
timeline.remote_client.shutdown().await;
|
||||||
|
|
||||||
// Download methods can still be used after shutdown, as they don't flow through the remote client's
|
// Download methods can still be used after shutdown, as they don't flow through the remote client's
|
||||||
// queue. In principal the RemoteTimelineClient could provide this without downloading it, but this
|
// queue. In principal the RemoteTimelineClient could provide this without downloading it, but this
|
||||||
// operation is rare, so it's simpler to just download it (and robustly guarantees that the index
|
// operation is rare, so it's simpler to just download it (and robustly guarantees that the index
|
||||||
// we use here really is the remotely persistent one).
|
// we use here really is the remotely persistent one).
|
||||||
|
tracing::info!(timeline_id=%timeline.timeline_id, "Downloading index_part from parent");
|
||||||
let result = timeline.remote_client
|
let result = timeline.remote_client
|
||||||
.download_index_file(&self.cancel)
|
.download_index_file(&self.cancel)
|
||||||
.instrument(info_span!("download_index_file", tenant_id=%self.tenant_shard_id.tenant_id, shard_id=%self.tenant_shard_id.shard_slug(), timeline_id=%timeline.timeline_id))
|
.instrument(info_span!("download_index_file", tenant_id=%self.tenant_shard_id.tenant_id, shard_id=%self.tenant_shard_id.shard_slug(), timeline_id=%timeline.timeline_id))
|
||||||
@@ -2240,6 +2170,7 @@ impl Tenant {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for child_shard in child_shards {
|
for child_shard in child_shards {
|
||||||
|
tracing::info!(timeline_id=%timeline.timeline_id, "Uploading index_part for child {}", child_shard.to_index());
|
||||||
upload_index_part(
|
upload_index_part(
|
||||||
&self.remote_storage,
|
&self.remote_storage,
|
||||||
child_shard,
|
child_shard,
|
||||||
@@ -2554,6 +2485,10 @@ impl Tenant {
|
|||||||
remote_storage: GenericRemoteStorage,
|
remote_storage: GenericRemoteStorage,
|
||||||
deletion_queue_client: DeletionQueueClient,
|
deletion_queue_client: DeletionQueueClient,
|
||||||
) -> Tenant {
|
) -> Tenant {
|
||||||
|
debug_assert!(
|
||||||
|
!attached_conf.location.generation.is_none() || conf.control_plane_api.is_none()
|
||||||
|
);
|
||||||
|
|
||||||
let (state, mut rx) = watch::channel(state);
|
let (state, mut rx) = watch::channel(state);
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
@@ -2628,7 +2563,6 @@ impl Tenant {
|
|||||||
cached_synthetic_tenant_size: Arc::new(AtomicU64::new(0)),
|
cached_synthetic_tenant_size: Arc::new(AtomicU64::new(0)),
|
||||||
eviction_task_tenant_state: tokio::sync::Mutex::new(EvictionTaskTenantState::default()),
|
eviction_task_tenant_state: tokio::sync::Mutex::new(EvictionTaskTenantState::default()),
|
||||||
activate_now_sem: tokio::sync::Semaphore::new(0),
|
activate_now_sem: tokio::sync::Semaphore::new(0),
|
||||||
delete_progress: Arc::new(tokio::sync::Mutex::new(DeleteTenantFlow::default())),
|
|
||||||
cancel: CancellationToken::default(),
|
cancel: CancellationToken::default(),
|
||||||
gate: Gate::default(),
|
gate: Gate::default(),
|
||||||
timeline_get_throttle: Arc::new(throttle::Throttle::new(
|
timeline_get_throttle: Arc::new(throttle::Throttle::new(
|
||||||
@@ -2645,45 +2579,22 @@ impl Tenant {
|
|||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
tenant_shard_id: &TenantShardId,
|
tenant_shard_id: &TenantShardId,
|
||||||
) -> anyhow::Result<LocationConf> {
|
) -> anyhow::Result<LocationConf> {
|
||||||
let legacy_config_path = conf.tenant_config_path(tenant_shard_id);
|
|
||||||
let config_path = conf.tenant_location_config_path(tenant_shard_id);
|
let config_path = conf.tenant_location_config_path(tenant_shard_id);
|
||||||
|
|
||||||
if config_path.exists() {
|
if config_path.exists() {
|
||||||
// New-style config takes precedence
|
// New-style config takes precedence
|
||||||
let deserialized = Self::read_config(&config_path)?;
|
let deserialized = Self::read_config(&config_path)?;
|
||||||
Ok(toml_edit::de::from_document::<LocationConf>(deserialized)?)
|
Ok(toml_edit::de::from_document::<LocationConf>(deserialized)?)
|
||||||
} else if legacy_config_path.exists() {
|
|
||||||
// Upgrade path: found an old-style configuration only
|
|
||||||
let deserialized = Self::read_config(&legacy_config_path)?;
|
|
||||||
|
|
||||||
let mut tenant_conf = TenantConfOpt::default();
|
|
||||||
for (key, item) in deserialized.iter() {
|
|
||||||
match key {
|
|
||||||
"tenant_config" => {
|
|
||||||
tenant_conf = TenantConfOpt::try_from(item.to_owned()).context(format!("Failed to parse config from file '{legacy_config_path}' as pageserver config"))?;
|
|
||||||
}
|
|
||||||
_ => bail!(
|
|
||||||
"config file {legacy_config_path} has unrecognized pageserver option '{key}'"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy configs are implicitly in attached state, and do not support sharding
|
|
||||||
Ok(LocationConf::attached_single(
|
|
||||||
tenant_conf,
|
|
||||||
Generation::none(),
|
|
||||||
&models::ShardParameters::default(),
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
// FIXME If the config file is not found, assume that we're attaching
|
// The config should almost always exist for a tenant directory:
|
||||||
// a detached tenant and config is passed via attach command.
|
// - When attaching a tenant, the config is the first thing we write
|
||||||
// https://github.com/neondatabase/neon/issues/1555
|
// - When detaching a tenant, we atomically move the directory to a tmp location
|
||||||
// OR: we're loading after incomplete deletion that managed to remove config.
|
// before deleting contents.
|
||||||
info!(
|
//
|
||||||
"tenant config not found in {} or {}",
|
// The very rare edge case that can result in a missing config is if we crash during attach
|
||||||
config_path, legacy_config_path
|
// between creating directory and writing config. Callers should handle that as if the
|
||||||
);
|
// directory didn't exist.
|
||||||
Ok(LocationConf::default())
|
anyhow::bail!("tenant config not found in {}", config_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2705,47 +2616,17 @@ impl Tenant {
|
|||||||
tenant_shard_id: &TenantShardId,
|
tenant_shard_id: &TenantShardId,
|
||||||
location_conf: &LocationConf,
|
location_conf: &LocationConf,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let legacy_config_path = conf.tenant_config_path(tenant_shard_id);
|
|
||||||
let config_path = conf.tenant_location_config_path(tenant_shard_id);
|
let config_path = conf.tenant_location_config_path(tenant_shard_id);
|
||||||
|
|
||||||
Self::persist_tenant_config_at(
|
Self::persist_tenant_config_at(tenant_shard_id, &config_path, location_conf).await
|
||||||
tenant_shard_id,
|
|
||||||
&config_path,
|
|
||||||
&legacy_config_path,
|
|
||||||
location_conf,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(tenant_id=%tenant_shard_id.tenant_id, shard_id=%tenant_shard_id.shard_slug()))]
|
#[tracing::instrument(skip_all, fields(tenant_id=%tenant_shard_id.tenant_id, shard_id=%tenant_shard_id.shard_slug()))]
|
||||||
pub(super) async fn persist_tenant_config_at(
|
pub(super) async fn persist_tenant_config_at(
|
||||||
tenant_shard_id: &TenantShardId,
|
tenant_shard_id: &TenantShardId,
|
||||||
config_path: &Utf8Path,
|
config_path: &Utf8Path,
|
||||||
legacy_config_path: &Utf8Path,
|
|
||||||
location_conf: &LocationConf,
|
location_conf: &LocationConf,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if let LocationMode::Attached(attach_conf) = &location_conf.mode {
|
|
||||||
// The modern-style LocationConf config file requires a generation to be set. In case someone
|
|
||||||
// is running a pageserver without the infrastructure to set generations, write out the legacy-style
|
|
||||||
// config file that only contains TenantConf.
|
|
||||||
//
|
|
||||||
// This will eventually be removed in https://github.com/neondatabase/neon/issues/5388
|
|
||||||
|
|
||||||
if attach_conf.generation.is_none() {
|
|
||||||
tracing::info!(
|
|
||||||
"Running without generations, writing legacy-style tenant config file"
|
|
||||||
);
|
|
||||||
Self::persist_tenant_config_legacy(
|
|
||||||
tenant_shard_id,
|
|
||||||
legacy_config_path,
|
|
||||||
&location_conf.tenant_conf,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("persisting tenantconf to {config_path}");
|
debug!("persisting tenantconf to {config_path}");
|
||||||
|
|
||||||
let mut conf_content = r#"# This file contains a specific per-tenant's config.
|
let mut conf_content = r#"# This file contains a specific per-tenant's config.
|
||||||
@@ -2772,37 +2653,6 @@ impl Tenant {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(tenant_id=%tenant_shard_id.tenant_id, shard_id=%tenant_shard_id.shard_slug()))]
|
|
||||||
async fn persist_tenant_config_legacy(
|
|
||||||
tenant_shard_id: &TenantShardId,
|
|
||||||
target_config_path: &Utf8Path,
|
|
||||||
tenant_conf: &TenantConfOpt,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
debug!("persisting tenantconf to {target_config_path}");
|
|
||||||
|
|
||||||
let mut conf_content = r#"# This file contains a specific per-tenant's config.
|
|
||||||
# It is read in case of pageserver restart.
|
|
||||||
|
|
||||||
[tenant_config]
|
|
||||||
"#
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
// Convert the config to a toml file.
|
|
||||||
conf_content += &toml_edit::ser::to_string(&tenant_conf)?;
|
|
||||||
|
|
||||||
let temp_path = path_with_suffix_extension(target_config_path, TEMP_FILE_SUFFIX);
|
|
||||||
|
|
||||||
let tenant_shard_id = *tenant_shard_id;
|
|
||||||
let target_config_path = target_config_path.to_owned();
|
|
||||||
let conf_content = conf_content.into_bytes();
|
|
||||||
VirtualFile::crashsafe_overwrite(target_config_path.clone(), temp_path, conf_content)
|
|
||||||
.await
|
|
||||||
.with_context(|| {
|
|
||||||
format!("write tenant {tenant_shard_id} config to {target_config_path}")
|
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// How garbage collection works:
|
// How garbage collection works:
|
||||||
//
|
//
|
||||||
@@ -3021,6 +2871,11 @@ impl Tenant {
|
|||||||
let now = SystemTime::now();
|
let now = SystemTime::now();
|
||||||
target.leases.retain(|_, lease| !lease.is_expired(&now));
|
target.leases.retain(|_, lease| !lease.is_expired(&now));
|
||||||
|
|
||||||
|
timeline
|
||||||
|
.metrics
|
||||||
|
.valid_lsn_lease_count_gauge
|
||||||
|
.set(target.leases.len() as u64);
|
||||||
|
|
||||||
match gc_cutoffs.remove(&timeline.timeline_id) {
|
match gc_cutoffs.remove(&timeline.timeline_id) {
|
||||||
Some(cutoffs) => {
|
Some(cutoffs) => {
|
||||||
target.retain_lsns = branchpoints;
|
target.retain_lsns = branchpoints;
|
||||||
@@ -4014,7 +3869,7 @@ pub(crate) mod harness {
|
|||||||
base_img: Option<(Lsn, Bytes)>,
|
base_img: Option<(Lsn, Bytes)>,
|
||||||
records: Vec<(Lsn, NeonWalRecord)>,
|
records: Vec<(Lsn, NeonWalRecord)>,
|
||||||
_pg_version: u32,
|
_pg_version: u32,
|
||||||
) -> anyhow::Result<Bytes> {
|
) -> Result<Bytes, walredo::Error> {
|
||||||
let records_neon = records.iter().all(|r| apply_neon::can_apply_in_neon(&r.1));
|
let records_neon = records.iter().all(|r| apply_neon::can_apply_in_neon(&r.1));
|
||||||
if records_neon {
|
if records_neon {
|
||||||
// For Neon wal records, we can decode without spawning postgres, so do so.
|
// For Neon wal records, we can decode without spawning postgres, so do so.
|
||||||
@@ -4068,6 +3923,7 @@ mod tests {
|
|||||||
use storage_layer::PersistentLayerKey;
|
use storage_layer::PersistentLayerKey;
|
||||||
use tests::storage_layer::ValuesReconstructState;
|
use tests::storage_layer::ValuesReconstructState;
|
||||||
use tests::timeline::{GetVectoredError, ShutdownMode};
|
use tests::timeline::{GetVectoredError, ShutdownMode};
|
||||||
|
use timeline::GcInfo;
|
||||||
use utils::bin_ser::BeSer;
|
use utils::bin_ser::BeSer;
|
||||||
use utils::id::TenantId;
|
use utils::id::TenantId;
|
||||||
|
|
||||||
@@ -6745,49 +6601,48 @@ mod tests {
|
|||||||
|
|
||||||
// img layer at 0x10
|
// img layer at 0x10
|
||||||
let img_layer = (0..10)
|
let img_layer = (0..10)
|
||||||
.map(|id| (get_key(id), test_img(&format!("value {id}@0x10"))))
|
.map(|id| (get_key(id), Bytes::from(format!("value {id}@0x10"))))
|
||||||
.collect_vec();
|
.collect_vec();
|
||||||
|
|
||||||
let delta1 = vec![
|
let delta1 = vec![
|
||||||
// TODO: we should test a real delta record here, which requires us to add a variant of NeonWalRecord for testing purpose.
|
|
||||||
(
|
(
|
||||||
get_key(1),
|
get_key(1),
|
||||||
Lsn(0x20),
|
Lsn(0x20),
|
||||||
Value::Image(test_img("value 1@0x20")),
|
Value::Image(Bytes::from("value 1@0x20")),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
get_key(2),
|
get_key(2),
|
||||||
Lsn(0x30),
|
Lsn(0x30),
|
||||||
Value::Image(test_img("value 2@0x30")),
|
Value::Image(Bytes::from("value 2@0x30")),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
get_key(3),
|
get_key(3),
|
||||||
Lsn(0x40),
|
Lsn(0x40),
|
||||||
Value::Image(test_img("value 3@0x40")),
|
Value::Image(Bytes::from("value 3@0x40")),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
let delta2 = vec![
|
let delta2 = vec![
|
||||||
(
|
(
|
||||||
get_key(5),
|
get_key(5),
|
||||||
Lsn(0x20),
|
Lsn(0x20),
|
||||||
Value::Image(test_img("value 5@0x20")),
|
Value::Image(Bytes::from("value 5@0x20")),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
get_key(6),
|
get_key(6),
|
||||||
Lsn(0x20),
|
Lsn(0x20),
|
||||||
Value::Image(test_img("value 6@0x20")),
|
Value::Image(Bytes::from("value 6@0x20")),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
let delta3 = vec![
|
let delta3 = vec![
|
||||||
(
|
(
|
||||||
get_key(8),
|
get_key(8),
|
||||||
Lsn(0x40),
|
Lsn(0x40),
|
||||||
Value::Image(test_img("value 8@0x40")),
|
Value::Image(Bytes::from("value 8@0x40")),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
get_key(9),
|
get_key(9),
|
||||||
Lsn(0x40),
|
Lsn(0x40),
|
||||||
Value::Image(test_img("value 9@0x40")),
|
Value::Image(Bytes::from("value 9@0x40")),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -6809,9 +6664,42 @@ mod tests {
|
|||||||
guard.cutoffs.horizon = Lsn(0x30);
|
guard.cutoffs.horizon = Lsn(0x30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let expected_result = [
|
||||||
|
Bytes::from_static(b"value 0@0x10"),
|
||||||
|
Bytes::from_static(b"value 1@0x20"),
|
||||||
|
Bytes::from_static(b"value 2@0x30"),
|
||||||
|
Bytes::from_static(b"value 3@0x40"),
|
||||||
|
Bytes::from_static(b"value 4@0x10"),
|
||||||
|
Bytes::from_static(b"value 5@0x20"),
|
||||||
|
Bytes::from_static(b"value 6@0x20"),
|
||||||
|
Bytes::from_static(b"value 7@0x10"),
|
||||||
|
Bytes::from_static(b"value 8@0x40"),
|
||||||
|
Bytes::from_static(b"value 9@0x40"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (idx, expected) in expected_result.iter().enumerate() {
|
||||||
|
assert_eq!(
|
||||||
|
tline
|
||||||
|
.get(get_key(idx as u32), Lsn(0x50), &ctx)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let cancel = CancellationToken::new();
|
let cancel = CancellationToken::new();
|
||||||
tline.compact_with_gc(&cancel, &ctx).await.unwrap();
|
tline.compact_with_gc(&cancel, &ctx).await.unwrap();
|
||||||
|
|
||||||
|
for (idx, expected) in expected_result.iter().enumerate() {
|
||||||
|
assert_eq!(
|
||||||
|
tline
|
||||||
|
.get(get_key(idx as u32), Lsn(0x50), &ctx)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the image layer at the GC horizon contains exactly what we want
|
// Check if the image layer at the GC horizon contains exactly what we want
|
||||||
let image_at_gc_horizon = tline
|
let image_at_gc_horizon = tline
|
||||||
.inspect_image_layers(Lsn(0x30), &ctx)
|
.inspect_image_layers(Lsn(0x30), &ctx)
|
||||||
@@ -6822,14 +6710,22 @@ mod tests {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
assert_eq!(image_at_gc_horizon.len(), 10);
|
assert_eq!(image_at_gc_horizon.len(), 10);
|
||||||
let expected_lsn = [0x10, 0x20, 0x30, 0x10, 0x10, 0x20, 0x20, 0x10, 0x10, 0x10];
|
let expected_result = [
|
||||||
|
Bytes::from_static(b"value 0@0x10"),
|
||||||
|
Bytes::from_static(b"value 1@0x20"),
|
||||||
|
Bytes::from_static(b"value 2@0x30"),
|
||||||
|
Bytes::from_static(b"value 3@0x10"),
|
||||||
|
Bytes::from_static(b"value 4@0x10"),
|
||||||
|
Bytes::from_static(b"value 5@0x20"),
|
||||||
|
Bytes::from_static(b"value 6@0x20"),
|
||||||
|
Bytes::from_static(b"value 7@0x10"),
|
||||||
|
Bytes::from_static(b"value 8@0x10"),
|
||||||
|
Bytes::from_static(b"value 9@0x10"),
|
||||||
|
];
|
||||||
for idx in 0..10 {
|
for idx in 0..10 {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
image_at_gc_horizon[idx],
|
image_at_gc_horizon[idx],
|
||||||
(
|
(get_key(idx as u32), expected_result[idx].clone())
|
||||||
get_key(idx as u32),
|
|
||||||
test_img(&format!("value {idx}@{:#x}", expected_lsn[idx]))
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6862,7 +6758,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
// The delta layer that is cut in the middle
|
// The delta layer that is cut in the middle
|
||||||
PersistentLayerKey {
|
PersistentLayerKey {
|
||||||
key_range: Key::MIN..get_key(9),
|
key_range: get_key(3)..get_key(4),
|
||||||
lsn_range: Lsn(0x30)..Lsn(0x41),
|
lsn_range: Lsn(0x30)..Lsn(0x41),
|
||||||
is_delta: true
|
is_delta: true
|
||||||
},
|
},
|
||||||
@@ -6947,6 +6843,9 @@ mod tests {
|
|||||||
tline.get(get_key(2), Lsn(0x50), &ctx).await?,
|
tline.get(get_key(2), Lsn(0x50), &ctx).await?,
|
||||||
Bytes::from_static(b"0x10,0x20,0x30")
|
Bytes::from_static(b"0x10,0x20,0x30")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Need to remove the limit of "Neon WAL redo requires base image".
|
||||||
|
|
||||||
// assert_eq!(tline.get(get_key(3), Lsn(0x50), &ctx).await?, Bytes::new());
|
// assert_eq!(tline.get(get_key(3), Lsn(0x50), &ctx).await?, Bytes::new());
|
||||||
// assert_eq!(tline.get(get_key(4), Lsn(0x50), &ctx).await?, Bytes::new());
|
// assert_eq!(tline.get(get_key(4), Lsn(0x50), &ctx).await?, Bytes::new());
|
||||||
|
|
||||||
@@ -7041,4 +6940,174 @@ mod tests {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_simple_bottom_most_compaction_deltas() -> anyhow::Result<()> {
|
||||||
|
let harness = TenantHarness::create("test_simple_bottom_most_compaction_deltas")?;
|
||||||
|
let (tenant, ctx) = harness.load().await;
|
||||||
|
|
||||||
|
fn get_key(id: u32) -> Key {
|
||||||
|
// using aux key here b/c they are guaranteed to be inside `collect_keyspace`.
|
||||||
|
let mut key = Key::from_hex("620000000033333333444444445500000000").unwrap();
|
||||||
|
key.field6 = id;
|
||||||
|
key
|
||||||
|
}
|
||||||
|
|
||||||
|
// We create one bottom-most image layer, a delta layer D1 crossing the GC horizon, D2 below the horizon, and D3 above the horizon.
|
||||||
|
//
|
||||||
|
// | D1 | | D3 |
|
||||||
|
// -| |-- gc horizon -----------------
|
||||||
|
// | | | D2 |
|
||||||
|
// --------- img layer ------------------
|
||||||
|
//
|
||||||
|
// What we should expact from this compaction is:
|
||||||
|
// | Part of D1 | | D3 |
|
||||||
|
// --------- img layer with D1+D2 at GC horizon------------------
|
||||||
|
|
||||||
|
// img layer at 0x10
|
||||||
|
let img_layer = (0..10)
|
||||||
|
.map(|id| (get_key(id), Bytes::from(format!("value {id}@0x10"))))
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
let delta1 = vec![
|
||||||
|
(
|
||||||
|
get_key(1),
|
||||||
|
Lsn(0x20),
|
||||||
|
Value::WalRecord(NeonWalRecord::wal_append("@0x20")),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
get_key(2),
|
||||||
|
Lsn(0x30),
|
||||||
|
Value::WalRecord(NeonWalRecord::wal_append("@0x30")),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
get_key(3),
|
||||||
|
Lsn(0x28),
|
||||||
|
Value::WalRecord(NeonWalRecord::wal_append("@0x28")),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
get_key(3),
|
||||||
|
Lsn(0x30),
|
||||||
|
Value::WalRecord(NeonWalRecord::wal_append("@0x30")),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
get_key(3),
|
||||||
|
Lsn(0x40),
|
||||||
|
Value::WalRecord(NeonWalRecord::wal_append("@0x40")),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let delta2 = vec![
|
||||||
|
(
|
||||||
|
get_key(5),
|
||||||
|
Lsn(0x20),
|
||||||
|
Value::WalRecord(NeonWalRecord::wal_append("@0x20")),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
get_key(6),
|
||||||
|
Lsn(0x20),
|
||||||
|
Value::WalRecord(NeonWalRecord::wal_append("@0x20")),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let delta3 = vec![
|
||||||
|
(
|
||||||
|
get_key(8),
|
||||||
|
Lsn(0x40),
|
||||||
|
Value::WalRecord(NeonWalRecord::wal_append("@0x40")),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
get_key(9),
|
||||||
|
Lsn(0x40),
|
||||||
|
Value::WalRecord(NeonWalRecord::wal_append("@0x40")),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let tline = tenant
|
||||||
|
.create_test_timeline_with_layers(
|
||||||
|
TIMELINE_ID,
|
||||||
|
Lsn(0x10),
|
||||||
|
DEFAULT_PG_VERSION,
|
||||||
|
&ctx,
|
||||||
|
vec![delta1, delta2, delta3], // delta layers
|
||||||
|
vec![(Lsn(0x10), img_layer)], // image layers
|
||||||
|
Lsn(0x50),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
{
|
||||||
|
// Update GC info
|
||||||
|
let mut guard = tline.gc_info.write().unwrap();
|
||||||
|
*guard = GcInfo {
|
||||||
|
retain_lsns: vec![],
|
||||||
|
cutoffs: GcCutoffs {
|
||||||
|
pitr: Lsn(0x30),
|
||||||
|
horizon: Lsn(0x30),
|
||||||
|
},
|
||||||
|
leases: Default::default(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected_result = [
|
||||||
|
Bytes::from_static(b"value 0@0x10"),
|
||||||
|
Bytes::from_static(b"value 1@0x10@0x20"),
|
||||||
|
Bytes::from_static(b"value 2@0x10@0x30"),
|
||||||
|
Bytes::from_static(b"value 3@0x10@0x28@0x30@0x40"),
|
||||||
|
Bytes::from_static(b"value 4@0x10"),
|
||||||
|
Bytes::from_static(b"value 5@0x10@0x20"),
|
||||||
|
Bytes::from_static(b"value 6@0x10@0x20"),
|
||||||
|
Bytes::from_static(b"value 7@0x10"),
|
||||||
|
Bytes::from_static(b"value 8@0x10@0x40"),
|
||||||
|
Bytes::from_static(b"value 9@0x10@0x40"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let expected_result_at_gc_horizon = [
|
||||||
|
Bytes::from_static(b"value 0@0x10"),
|
||||||
|
Bytes::from_static(b"value 1@0x10@0x20"),
|
||||||
|
Bytes::from_static(b"value 2@0x10@0x30"),
|
||||||
|
Bytes::from_static(b"value 3@0x10@0x28@0x30"),
|
||||||
|
Bytes::from_static(b"value 4@0x10"),
|
||||||
|
Bytes::from_static(b"value 5@0x10@0x20"),
|
||||||
|
Bytes::from_static(b"value 6@0x10@0x20"),
|
||||||
|
Bytes::from_static(b"value 7@0x10"),
|
||||||
|
Bytes::from_static(b"value 8@0x10"),
|
||||||
|
Bytes::from_static(b"value 9@0x10"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for idx in 0..10 {
|
||||||
|
assert_eq!(
|
||||||
|
tline
|
||||||
|
.get(get_key(idx as u32), Lsn(0x50), &ctx)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
&expected_result[idx]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tline
|
||||||
|
.get(get_key(idx as u32), Lsn(0x30), &ctx)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
&expected_result_at_gc_horizon[idx]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancel = CancellationToken::new();
|
||||||
|
tline.compact_with_gc(&cancel, &ctx).await.unwrap();
|
||||||
|
|
||||||
|
for idx in 0..10 {
|
||||||
|
assert_eq!(
|
||||||
|
tline
|
||||||
|
.get(get_key(idx as u32), Lsn(0x50), &ctx)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
&expected_result[idx]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tline
|
||||||
|
.get(get_key(idx as u32), Lsn(0x30), &ctx)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
&expected_result_at_gc_horizon[idx]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ impl<'a> BlockCursor<'a> {
|
|||||||
///
|
///
|
||||||
/// The file is assumed to be immutable. This doesn't provide any functions
|
/// The file is assumed to be immutable. This doesn't provide any functions
|
||||||
/// for modifying the file, nor for invalidating the cache if it is modified.
|
/// for modifying the file, nor for invalidating the cache if it is modified.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct FileBlockReader<'a> {
|
pub struct FileBlockReader<'a> {
|
||||||
pub file: &'a VirtualFile,
|
pub file: &'a VirtualFile,
|
||||||
|
|
||||||
|
|||||||
@@ -281,22 +281,6 @@ impl LocationConf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LocationConf {
|
|
||||||
// TODO: this should be removed once tenant loading can guarantee that we are never
|
|
||||||
// loading from a directory without a configuration.
|
|
||||||
// => tech debt since https://github.com/neondatabase/neon/issues/1555
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
mode: LocationMode::Attached(AttachedLocationConfig {
|
|
||||||
generation: Generation::none(),
|
|
||||||
attach_mode: AttachmentMode::Single,
|
|
||||||
}),
|
|
||||||
tenant_conf: TenantConfOpt::default(),
|
|
||||||
shard: ShardIdentity::unsharded(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A tenant's calcuated configuration, which is the result of merging a
|
/// A tenant's calcuated configuration, which is the result of merging a
|
||||||
/// tenant's TenantConfOpt with the global TenantConf from PageServerConf.
|
/// tenant's TenantConfOpt with the global TenantConf from PageServerConf.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,426 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use camino::{Utf8Path, Utf8PathBuf};
|
|
||||||
use pageserver_api::{models::TenantState, shard::TenantShardId};
|
|
||||||
use remote_storage::{GenericRemoteStorage, RemotePath, TimeoutOrCancel};
|
|
||||||
use tokio::sync::OwnedMutexGuard;
|
|
||||||
use tokio_util::sync::CancellationToken;
|
|
||||||
use tracing::{error, Instrument};
|
|
||||||
|
|
||||||
use utils::{backoff, completion, crashsafe, fs_ext, id::TimelineId, pausable_failpoint};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
config::PageServerConf,
|
|
||||||
context::RequestContext,
|
|
||||||
task_mgr::{self},
|
|
||||||
tenant::{
|
|
||||||
mgr::{TenantSlot, TenantsMapRemoveResult},
|
|
||||||
remote_timeline_client::remote_heatmap_path,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
mgr::{GetTenantError, TenantSlotError, TenantSlotUpsertError, TenantsMap},
|
|
||||||
remote_timeline_client::{FAILED_REMOTE_OP_RETRIES, FAILED_UPLOAD_WARN_THRESHOLD},
|
|
||||||
timeline::delete::DeleteTimelineFlow,
|
|
||||||
tree_sort_timelines, DeleteTimelineError, Tenant, TenantPreload,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub(crate) enum DeleteTenantError {
|
|
||||||
#[error("GetTenant {0}")]
|
|
||||||
Get(#[from] GetTenantError),
|
|
||||||
|
|
||||||
#[error("Tenant map slot error {0}")]
|
|
||||||
SlotError(#[from] TenantSlotError),
|
|
||||||
|
|
||||||
#[error("Tenant map slot upsert error {0}")]
|
|
||||||
SlotUpsertError(#[from] TenantSlotUpsertError),
|
|
||||||
|
|
||||||
#[error("Timeline {0}")]
|
|
||||||
Timeline(#[from] DeleteTimelineError),
|
|
||||||
|
|
||||||
#[error("Cancelled")]
|
|
||||||
Cancelled,
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Other(#[from] anyhow::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeletionGuard = tokio::sync::OwnedMutexGuard<DeleteTenantFlow>;
|
|
||||||
|
|
||||||
fn remote_tenant_delete_mark_path(
|
|
||||||
conf: &PageServerConf,
|
|
||||||
tenant_shard_id: &TenantShardId,
|
|
||||||
) -> anyhow::Result<RemotePath> {
|
|
||||||
let tenant_remote_path = conf
|
|
||||||
.tenant_path(tenant_shard_id)
|
|
||||||
.strip_prefix(&conf.workdir)
|
|
||||||
.context("Failed to strip workdir prefix")
|
|
||||||
.and_then(RemotePath::new)
|
|
||||||
.context("tenant path")?;
|
|
||||||
Ok(tenant_remote_path.join(Utf8Path::new("timelines/deleted")))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn schedule_ordered_timeline_deletions(
|
|
||||||
tenant: &Arc<Tenant>,
|
|
||||||
) -> Result<Vec<(Arc<tokio::sync::Mutex<DeleteTimelineFlow>>, TimelineId)>, DeleteTenantError> {
|
|
||||||
// Tenant is stopping at this point. We know it will be deleted.
|
|
||||||
// No new timelines should be created.
|
|
||||||
// Tree sort timelines to delete from leafs to the root.
|
|
||||||
// NOTE: by calling clone we release the mutex which creates a possibility for a race: pending deletion
|
|
||||||
// can complete and remove timeline from the map in between our call to clone
|
|
||||||
// and `DeleteTimelineFlow::run`, so `run` wont find timeline in `timelines` map.
|
|
||||||
// timelines.lock is currently synchronous so we cant hold it across await point.
|
|
||||||
// So just ignore NotFound error if we get it from `run`.
|
|
||||||
// Beware: in case it becomes async and we try to hold it here, `run` also locks it, which can create a deadlock.
|
|
||||||
let timelines = tenant.timelines.lock().unwrap().clone();
|
|
||||||
let sorted =
|
|
||||||
tree_sort_timelines(timelines, |t| t.get_ancestor_timeline_id()).context("tree sort")?;
|
|
||||||
|
|
||||||
let mut already_running_deletions = vec![];
|
|
||||||
|
|
||||||
for (timeline_id, _) in sorted.into_iter().rev() {
|
|
||||||
let span = tracing::info_span!("timeline_delete", %timeline_id);
|
|
||||||
let res = DeleteTimelineFlow::run(tenant, timeline_id, true)
|
|
||||||
.instrument(span)
|
|
||||||
.await;
|
|
||||||
if let Err(e) = res {
|
|
||||||
match e {
|
|
||||||
DeleteTimelineError::NotFound => {
|
|
||||||
// Timeline deletion finished after call to clone above but before call
|
|
||||||
// to `DeleteTimelineFlow::run` and removed timeline from the map.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
DeleteTimelineError::AlreadyInProgress(guard) => {
|
|
||||||
already_running_deletions.push((guard, timeline_id));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
e => return Err(DeleteTenantError::Timeline(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(already_running_deletions)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn ensure_timelines_dir_empty(timelines_path: &Utf8Path) -> Result<(), DeleteTenantError> {
|
|
||||||
// Assert timelines dir is empty.
|
|
||||||
if !fs_ext::is_directory_empty(timelines_path).await? {
|
|
||||||
// Display first 10 items in directory
|
|
||||||
let list = fs_ext::list_dir(timelines_path).await.context("list_dir")?;
|
|
||||||
let list = &list.into_iter().take(10).collect::<Vec<_>>();
|
|
||||||
return Err(DeleteTenantError::Other(anyhow::anyhow!(
|
|
||||||
"Timelines directory is not empty after all timelines deletion: {list:?}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_tenant_remote_delete_mark(
|
|
||||||
conf: &PageServerConf,
|
|
||||||
remote_storage: &GenericRemoteStorage,
|
|
||||||
tenant_shard_id: &TenantShardId,
|
|
||||||
cancel: &CancellationToken,
|
|
||||||
) -> Result<(), DeleteTenantError> {
|
|
||||||
let path = remote_tenant_delete_mark_path(conf, tenant_shard_id)?;
|
|
||||||
backoff::retry(
|
|
||||||
|| async { remote_storage.delete(&path, cancel).await },
|
|
||||||
TimeoutOrCancel::caused_by_cancel,
|
|
||||||
FAILED_UPLOAD_WARN_THRESHOLD,
|
|
||||||
FAILED_REMOTE_OP_RETRIES,
|
|
||||||
"remove_tenant_remote_delete_mark",
|
|
||||||
cancel,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.ok_or_else(|| anyhow::Error::new(TimeoutOrCancel::Cancel))
|
|
||||||
.and_then(|x| x)
|
|
||||||
.context("remove_tenant_remote_delete_mark")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup fs traces: tenant config, timelines dir local delete mark, tenant dir
|
|
||||||
async fn cleanup_remaining_fs_traces(
|
|
||||||
conf: &PageServerConf,
|
|
||||||
tenant_shard_id: &TenantShardId,
|
|
||||||
) -> Result<(), DeleteTenantError> {
|
|
||||||
let rm = |p: Utf8PathBuf, is_dir: bool| async move {
|
|
||||||
if is_dir {
|
|
||||||
tokio::fs::remove_dir(&p).await
|
|
||||||
} else {
|
|
||||||
tokio::fs::remove_file(&p).await
|
|
||||||
}
|
|
||||||
.or_else(fs_ext::ignore_not_found)
|
|
||||||
.with_context(|| format!("failed to delete {p}"))
|
|
||||||
};
|
|
||||||
|
|
||||||
rm(conf.tenant_config_path(tenant_shard_id), false).await?;
|
|
||||||
rm(conf.tenant_location_config_path(tenant_shard_id), false).await?;
|
|
||||||
|
|
||||||
fail::fail_point!("tenant-delete-before-remove-timelines-dir", |_| {
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"failpoint: tenant-delete-before-remove-timelines-dir"
|
|
||||||
))?
|
|
||||||
});
|
|
||||||
|
|
||||||
rm(conf.timelines_path(tenant_shard_id), true).await?;
|
|
||||||
|
|
||||||
fail::fail_point!("tenant-delete-before-remove-deleted-mark", |_| {
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"failpoint: tenant-delete-before-remove-deleted-mark"
|
|
||||||
))?
|
|
||||||
});
|
|
||||||
|
|
||||||
// Make sure previous deletions are ordered before mark removal.
|
|
||||||
// Otherwise there is no guarantee that they reach the disk before mark deletion.
|
|
||||||
// So its possible for mark to reach disk first and for other deletions
|
|
||||||
// to be reordered later and thus missed if a crash occurs.
|
|
||||||
// Note that we dont need to sync after mark file is removed
|
|
||||||
// because we can tolerate the case when mark file reappears on startup.
|
|
||||||
let tenant_path = &conf.tenant_path(tenant_shard_id);
|
|
||||||
if tenant_path.exists() {
|
|
||||||
crashsafe::fsync_async(&conf.tenant_path(tenant_shard_id))
|
|
||||||
.await
|
|
||||||
.context("fsync_pre_mark_remove")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
rm(conf.tenant_deleted_mark_file_path(tenant_shard_id), false).await?;
|
|
||||||
|
|
||||||
rm(conf.tenant_heatmap_path(tenant_shard_id), false).await?;
|
|
||||||
|
|
||||||
fail::fail_point!("tenant-delete-before-remove-tenant-dir", |_| {
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"failpoint: tenant-delete-before-remove-tenant-dir"
|
|
||||||
))?
|
|
||||||
});
|
|
||||||
|
|
||||||
rm(conf.tenant_path(tenant_shard_id), true).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub enum DeleteTenantFlow {
|
|
||||||
#[default]
|
|
||||||
NotStarted,
|
|
||||||
InProgress,
|
|
||||||
Finished,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeleteTenantFlow {
|
|
||||||
pub(crate) async fn should_resume_deletion(
|
|
||||||
conf: &'static PageServerConf,
|
|
||||||
remote_mark_exists: bool,
|
|
||||||
tenant: &Tenant,
|
|
||||||
) -> Result<Option<DeletionGuard>, DeleteTenantError> {
|
|
||||||
let acquire = |t: &Tenant| {
|
|
||||||
Some(
|
|
||||||
Arc::clone(&t.delete_progress)
|
|
||||||
.try_lock_owned()
|
|
||||||
.expect("we're the only owner during init"),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if remote_mark_exists {
|
|
||||||
return Ok(acquire(tenant));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check local mark first, if its there there is no need to go to s3 to check whether remote one exists.
|
|
||||||
if conf
|
|
||||||
.tenant_deleted_mark_file_path(&tenant.tenant_shard_id)
|
|
||||||
.exists()
|
|
||||||
{
|
|
||||||
Ok(acquire(tenant))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn resume_from_attach(
|
|
||||||
guard: DeletionGuard,
|
|
||||||
tenant: &Arc<Tenant>,
|
|
||||||
preload: Option<TenantPreload>,
|
|
||||||
tenants: &'static std::sync::RwLock<TenantsMap>,
|
|
||||||
ctx: &RequestContext,
|
|
||||||
) -> Result<(), DeleteTenantError> {
|
|
||||||
let (_, progress) = completion::channel();
|
|
||||||
|
|
||||||
tenant
|
|
||||||
.set_stopping(progress, false, true)
|
|
||||||
.await
|
|
||||||
.expect("cant be stopping or broken");
|
|
||||||
|
|
||||||
tenant
|
|
||||||
.attach(preload, super::SpawnMode::Eager, ctx)
|
|
||||||
.await
|
|
||||||
.context("attach")?;
|
|
||||||
|
|
||||||
Self::background(
|
|
||||||
guard,
|
|
||||||
tenant.conf,
|
|
||||||
tenant.remote_storage.clone(),
|
|
||||||
tenants,
|
|
||||||
tenant,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn background(
|
|
||||||
mut guard: OwnedMutexGuard<Self>,
|
|
||||||
conf: &PageServerConf,
|
|
||||||
remote_storage: GenericRemoteStorage,
|
|
||||||
tenants: &'static std::sync::RwLock<TenantsMap>,
|
|
||||||
tenant: &Arc<Tenant>,
|
|
||||||
) -> Result<(), DeleteTenantError> {
|
|
||||||
// Tree sort timelines, schedule delete for them. Mention retries from the console side.
|
|
||||||
// Note that if deletion fails we dont mark timelines as broken,
|
|
||||||
// the whole tenant will become broken as by `Self::schedule_background` logic
|
|
||||||
let already_running_timeline_deletions = schedule_ordered_timeline_deletions(tenant)
|
|
||||||
.await
|
|
||||||
.context("schedule_ordered_timeline_deletions")?;
|
|
||||||
|
|
||||||
fail::fail_point!("tenant-delete-before-polling-ongoing-deletions", |_| {
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"failpoint: tenant-delete-before-polling-ongoing-deletions"
|
|
||||||
))?
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for deletions that were already running at the moment when tenant deletion was requested.
|
|
||||||
// When we can lock deletion guard it means that corresponding timeline deletion finished.
|
|
||||||
for (guard, timeline_id) in already_running_timeline_deletions {
|
|
||||||
let flow = guard.lock().await;
|
|
||||||
if !flow.is_finished() {
|
|
||||||
return Err(DeleteTenantError::Other(anyhow::anyhow!(
|
|
||||||
"already running timeline deletion failed: {timeline_id}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove top-level tenant objects that don't belong to a timeline, such as heatmap
|
|
||||||
let heatmap_path = remote_heatmap_path(&tenant.tenant_shard_id());
|
|
||||||
if let Some(Err(e)) = backoff::retry(
|
|
||||||
|| async {
|
|
||||||
remote_storage
|
|
||||||
.delete(&heatmap_path, &task_mgr::shutdown_token())
|
|
||||||
.await
|
|
||||||
},
|
|
||||||
TimeoutOrCancel::caused_by_cancel,
|
|
||||||
FAILED_UPLOAD_WARN_THRESHOLD,
|
|
||||||
FAILED_REMOTE_OP_RETRIES,
|
|
||||||
"remove_remote_tenant_heatmap",
|
|
||||||
&task_mgr::shutdown_token(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::warn!("Failed to delete heatmap at {heatmap_path}: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
let timelines_path = conf.timelines_path(&tenant.tenant_shard_id);
|
|
||||||
// May not exist if we fail in cleanup_remaining_fs_traces after removing it
|
|
||||||
if timelines_path.exists() {
|
|
||||||
// sanity check to guard against layout changes
|
|
||||||
ensure_timelines_dir_empty(&timelines_path)
|
|
||||||
.await
|
|
||||||
.context("timelines dir not empty")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
remove_tenant_remote_delete_mark(
|
|
||||||
conf,
|
|
||||||
&remote_storage,
|
|
||||||
&tenant.tenant_shard_id,
|
|
||||||
&task_mgr::shutdown_token(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
pausable_failpoint!("tenant-delete-before-cleanup-remaining-fs-traces-pausable");
|
|
||||||
fail::fail_point!("tenant-delete-before-cleanup-remaining-fs-traces", |_| {
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"failpoint: tenant-delete-before-cleanup-remaining-fs-traces"
|
|
||||||
))?
|
|
||||||
});
|
|
||||||
|
|
||||||
cleanup_remaining_fs_traces(conf, &tenant.tenant_shard_id)
|
|
||||||
.await
|
|
||||||
.context("cleanup_remaining_fs_traces")?;
|
|
||||||
|
|
||||||
{
|
|
||||||
// This block is simply removing the TenantSlot for this tenant. It requires a loop because
|
|
||||||
// we might conflict with a TenantSlot::InProgress marker and need to wait for it.
|
|
||||||
//
|
|
||||||
// This complexity will go away when we simplify how deletion works:
|
|
||||||
// https://github.com/neondatabase/neon/issues/5080
|
|
||||||
loop {
|
|
||||||
// Under the TenantMap lock, try to remove the tenant. We usually succeed, but if
|
|
||||||
// we encounter an InProgress marker, yield the barrier it contains and wait on it.
|
|
||||||
let barrier = {
|
|
||||||
let mut locked = tenants.write().unwrap();
|
|
||||||
let removed = locked.remove(tenant.tenant_shard_id);
|
|
||||||
|
|
||||||
// FIXME: we should not be modifying this from outside of mgr.rs.
|
|
||||||
// This will go away when we simplify deletion (https://github.com/neondatabase/neon/issues/5080)
|
|
||||||
|
|
||||||
// Update stats
|
|
||||||
match &removed {
|
|
||||||
TenantsMapRemoveResult::Occupied(slot) => {
|
|
||||||
crate::metrics::TENANT_MANAGER.slot_removed(slot);
|
|
||||||
}
|
|
||||||
TenantsMapRemoveResult::InProgress(barrier) => {
|
|
||||||
crate::metrics::TENANT_MANAGER
|
|
||||||
.slot_removed(&TenantSlot::InProgress(barrier.clone()));
|
|
||||||
}
|
|
||||||
TenantsMapRemoveResult::Vacant => {
|
|
||||||
// Nothing changed in map, no metric update
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match removed {
|
|
||||||
TenantsMapRemoveResult::Occupied(TenantSlot::Attached(tenant)) => {
|
|
||||||
match tenant.current_state() {
|
|
||||||
TenantState::Stopping { .. } | TenantState::Broken { .. } => {
|
|
||||||
// Expected: we put the tenant into stopping state before we start deleting it
|
|
||||||
}
|
|
||||||
state => {
|
|
||||||
// Unexpected state
|
|
||||||
tracing::warn!(
|
|
||||||
"Tenant in unexpected state {state} after deletion"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
TenantsMapRemoveResult::Occupied(TenantSlot::Secondary(_)) => {
|
|
||||||
// This is unexpected: this secondary tenants should not have been created, and we
|
|
||||||
// are not in a position to shut it down from here.
|
|
||||||
tracing::warn!("Tenant transitioned to secondary mode while deleting!");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
TenantsMapRemoveResult::Occupied(TenantSlot::InProgress(_)) => {
|
|
||||||
unreachable!("TenantsMap::remove handles InProgress separately, should never return it here");
|
|
||||||
}
|
|
||||||
TenantsMapRemoveResult::Vacant => {
|
|
||||||
tracing::warn!(
|
|
||||||
"Tenant removed from TenantsMap before deletion completed"
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
TenantsMapRemoveResult::InProgress(barrier) => {
|
|
||||||
// An InProgress entry was found, we must wait on its barrier
|
|
||||||
barrier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::info!(
|
|
||||||
"Waiting for competing operation to complete before deleting state for tenant"
|
|
||||||
);
|
|
||||||
barrier.wait().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*guard = Self::Finished;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -212,6 +212,7 @@ impl<'a, const L: usize> OnDiskNode<'a, L> {
|
|||||||
///
|
///
|
||||||
/// Public reader object, to search the tree.
|
/// Public reader object, to search the tree.
|
||||||
///
|
///
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct DiskBtreeReader<R, const L: usize>
|
pub struct DiskBtreeReader<R, const L: usize>
|
||||||
where
|
where
|
||||||
R: BlockReader,
|
R: BlockReader,
|
||||||
@@ -259,27 +260,38 @@ where
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter<'a>(
|
pub fn iter<'a>(self, start_key: &'a [u8; L], ctx: &'a RequestContext) -> DiskBtreeIterator<'a>
|
||||||
&'a self,
|
where
|
||||||
start_key: &'a [u8; L],
|
R: 'a,
|
||||||
ctx: &'a RequestContext,
|
{
|
||||||
) -> DiskBtreeIterator<'a> {
|
|
||||||
DiskBtreeIterator {
|
DiskBtreeIterator {
|
||||||
stream: Box::pin(self.get_stream_from(start_key, ctx)),
|
stream: Box::pin(self.into_stream(start_key, ctx)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a stream which yields all key, value pairs from the index
|
/// Return a stream which yields all key, value pairs from the index
|
||||||
/// starting from the first key greater or equal to `start_key`.
|
/// starting from the first key greater or equal to `start_key`.
|
||||||
///
|
///
|
||||||
/// Note that this is a copy of [`Self::visit`].
|
/// Note 1: that this is a copy of [`Self::visit`].
|
||||||
/// TODO: Once the sequential read path is removed this will become
|
/// TODO: Once the sequential read path is removed this will become
|
||||||
/// the only index traversal method.
|
/// the only index traversal method.
|
||||||
pub fn get_stream_from<'a>(
|
///
|
||||||
&'a self,
|
/// Note 2: this function used to take `&self` but it now consumes `self`. This is due to
|
||||||
|
/// the lifetime constraints of the reader and the stream / iterator it creates. Using `&self`
|
||||||
|
/// requires the reader to be present when the stream is used, and this creates a lifetime
|
||||||
|
/// dependency between the reader and the stream. Now if we want to create an iterator that
|
||||||
|
/// holds the stream, someone will need to keep a reference to the reader, which is inconvenient
|
||||||
|
/// to use from the image/delta layer APIs.
|
||||||
|
///
|
||||||
|
/// Feel free to add the `&self` variant back if it's necessary.
|
||||||
|
pub fn into_stream<'a>(
|
||||||
|
self,
|
||||||
start_key: &'a [u8; L],
|
start_key: &'a [u8; L],
|
||||||
ctx: &'a RequestContext,
|
ctx: &'a RequestContext,
|
||||||
) -> impl Stream<Item = std::result::Result<(Vec<u8>, u64), DiskBtreeError>> + 'a {
|
) -> impl Stream<Item = std::result::Result<(Vec<u8>, u64), DiskBtreeError>> + 'a
|
||||||
|
where
|
||||||
|
R: 'a,
|
||||||
|
{
|
||||||
try_stream! {
|
try_stream! {
|
||||||
let mut stack = Vec::new();
|
let mut stack = Vec::new();
|
||||||
stack.push((self.root_blk, None));
|
stack.push((self.root_blk, None));
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ use utils::fs_ext::PathExt;
|
|||||||
use utils::generation::Generation;
|
use utils::generation::Generation;
|
||||||
use utils::id::{TenantId, TimelineId};
|
use utils::id::{TenantId, TimelineId};
|
||||||
|
|
||||||
use super::delete::DeleteTenantError;
|
|
||||||
use super::remote_timeline_client::remote_tenant_path;
|
use super::remote_timeline_client::remote_tenant_path;
|
||||||
use super::secondary::SecondaryTenant;
|
use super::secondary::SecondaryTenant;
|
||||||
use super::timeline::detach_ancestor::PreparedTimelineDetach;
|
use super::timeline::detach_ancestor::PreparedTimelineDetach;
|
||||||
@@ -109,12 +108,6 @@ pub(crate) enum TenantsMap {
|
|||||||
ShuttingDown(BTreeMap<TenantShardId, TenantSlot>),
|
ShuttingDown(BTreeMap<TenantShardId, TenantSlot>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum TenantsMapRemoveResult {
|
|
||||||
Occupied(TenantSlot),
|
|
||||||
Vacant,
|
|
||||||
InProgress(utils::completion::Barrier),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// When resolving a TenantId to a shard, we may be looking for the 0th
|
/// When resolving a TenantId to a shard, we may be looking for the 0th
|
||||||
/// shard, or we might be looking for whichever shard holds a particular page.
|
/// shard, or we might be looking for whichever shard holds a particular page.
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
@@ -191,26 +184,6 @@ impl TenantsMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Only for use from DeleteTenantFlow. This method directly removes a TenantSlot from the map.
|
|
||||||
///
|
|
||||||
/// The normal way to remove a tenant is using a SlotGuard, which will gracefully remove the guarded
|
|
||||||
/// slot if the enclosed tenant is shutdown.
|
|
||||||
pub(crate) fn remove(&mut self, tenant_shard_id: TenantShardId) -> TenantsMapRemoveResult {
|
|
||||||
use std::collections::btree_map::Entry;
|
|
||||||
match self {
|
|
||||||
TenantsMap::Initializing => TenantsMapRemoveResult::Vacant,
|
|
||||||
TenantsMap::Open(m) | TenantsMap::ShuttingDown(m) => match m.entry(tenant_shard_id) {
|
|
||||||
Entry::Occupied(entry) => match entry.get() {
|
|
||||||
TenantSlot::InProgress(barrier) => {
|
|
||||||
TenantsMapRemoveResult::InProgress(barrier.clone())
|
|
||||||
}
|
|
||||||
_ => TenantsMapRemoveResult::Occupied(entry.remove()),
|
|
||||||
},
|
|
||||||
Entry::Vacant(_entry) => TenantsMapRemoveResult::Vacant,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(debug_assertions, not(test)))]
|
#[cfg(all(debug_assertions, not(test)))]
|
||||||
pub(crate) fn len(&self) -> usize {
|
pub(crate) fn len(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
@@ -460,6 +433,18 @@ async fn init_load_tenant_configs(
|
|||||||
Ok(configs)
|
Ok(configs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub(crate) enum DeleteTenantError {
|
||||||
|
#[error("Tenant map slot error {0}")]
|
||||||
|
SlotError(#[from] TenantSlotError),
|
||||||
|
|
||||||
|
#[error("Cancelled")]
|
||||||
|
Cancelled,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Other(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
/// Initialize repositories with locally available timelines.
|
/// Initialize repositories with locally available timelines.
|
||||||
/// Timelines that are only partially available locally (remote storage has more data than this pageserver)
|
/// Timelines that are only partially available locally (remote storage has more data than this pageserver)
|
||||||
/// are scheduled for download and added to the tenant once download is completed.
|
/// are scheduled for download and added to the tenant once download is completed.
|
||||||
@@ -510,17 +495,8 @@ pub async fn init_tenant_mgr(
|
|||||||
let mut location_conf = match location_conf {
|
let mut location_conf = match location_conf {
|
||||||
Ok(l) => l,
|
Ok(l) => l,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(tenant_id=%tenant_shard_id.tenant_id, shard_id=%tenant_shard_id.shard_slug(), "Marking tenant broken, failed to {e:#}");
|
// This should only happen in the case of a serialization bug or critical local I/O error: we cannot load this tenant
|
||||||
|
error!(tenant_id=%tenant_shard_id.tenant_id, shard_id=%tenant_shard_id.shard_slug(), "Failed to load tenant config, failed to {e:#}");
|
||||||
tenants.insert(
|
|
||||||
tenant_shard_id,
|
|
||||||
TenantSlot::Attached(Tenant::create_broken_tenant(
|
|
||||||
conf,
|
|
||||||
tenant_shard_id,
|
|
||||||
resources.remote_storage.clone(),
|
|
||||||
format!("{}", e),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -629,7 +605,6 @@ pub async fn init_tenant_mgr(
|
|||||||
AttachedTenantConf::new(location_conf.tenant_conf, attached_conf),
|
AttachedTenantConf::new(location_conf.tenant_conf, attached_conf),
|
||||||
shard_identity,
|
shard_identity,
|
||||||
Some(init_order.clone()),
|
Some(init_order.clone()),
|
||||||
&TENANTS,
|
|
||||||
SpawnMode::Lazy,
|
SpawnMode::Lazy,
|
||||||
&ctx,
|
&ctx,
|
||||||
) {
|
) {
|
||||||
@@ -685,7 +660,6 @@ fn tenant_spawn(
|
|||||||
location_conf: AttachedTenantConf,
|
location_conf: AttachedTenantConf,
|
||||||
shard_identity: ShardIdentity,
|
shard_identity: ShardIdentity,
|
||||||
init_order: Option<InitializationOrder>,
|
init_order: Option<InitializationOrder>,
|
||||||
tenants: &'static std::sync::RwLock<TenantsMap>,
|
|
||||||
mode: SpawnMode,
|
mode: SpawnMode,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<Arc<Tenant>> {
|
) -> anyhow::Result<Arc<Tenant>> {
|
||||||
@@ -704,24 +678,16 @@ fn tenant_spawn(
|
|||||||
"Cannot load tenant from empty directory {tenant_path:?}"
|
"Cannot load tenant from empty directory {tenant_path:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
let remote_storage = resources.remote_storage.clone();
|
let tenant = Tenant::spawn(
|
||||||
let tenant = match Tenant::spawn(
|
|
||||||
conf,
|
conf,
|
||||||
tenant_shard_id,
|
tenant_shard_id,
|
||||||
resources,
|
resources,
|
||||||
location_conf,
|
location_conf,
|
||||||
shard_identity,
|
shard_identity,
|
||||||
init_order,
|
init_order,
|
||||||
tenants,
|
|
||||||
mode,
|
mode,
|
||||||
ctx,
|
ctx,
|
||||||
) {
|
);
|
||||||
Ok(tenant) => tenant,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to spawn tenant {tenant_shard_id}, reason: {e:#}");
|
|
||||||
Tenant::create_broken_tenant(conf, tenant_shard_id, remote_storage, format!("{e:#}"))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(tenant)
|
Ok(tenant)
|
||||||
}
|
}
|
||||||
@@ -1161,7 +1127,6 @@ impl TenantManager {
|
|||||||
attached_conf,
|
attached_conf,
|
||||||
shard_identity,
|
shard_identity,
|
||||||
None,
|
None,
|
||||||
self.tenants,
|
|
||||||
spawn_mode,
|
spawn_mode,
|
||||||
ctx,
|
ctx,
|
||||||
)?;
|
)?;
|
||||||
@@ -1283,7 +1248,6 @@ impl TenantManager {
|
|||||||
AttachedTenantConf::try_from(config)?,
|
AttachedTenantConf::try_from(config)?,
|
||||||
shard_identity,
|
shard_identity,
|
||||||
None,
|
None,
|
||||||
self.tenants,
|
|
||||||
SpawnMode::Eager,
|
SpawnMode::Eager,
|
||||||
ctx,
|
ctx,
|
||||||
)?;
|
)?;
|
||||||
@@ -1634,7 +1598,7 @@ impl TenantManager {
|
|||||||
for child_shard_id in &child_shards {
|
for child_shard_id in &child_shards {
|
||||||
let child_shard_id = *child_shard_id;
|
let child_shard_id = *child_shard_id;
|
||||||
let child_shard = {
|
let child_shard = {
|
||||||
let locked = TENANTS.read().unwrap();
|
let locked = self.tenants.read().unwrap();
|
||||||
let peek_slot =
|
let peek_slot =
|
||||||
tenant_map_peek_slot(&locked, &child_shard_id, TenantSlotPeekMode::Read)?;
|
tenant_map_peek_slot(&locked, &child_shard_id, TenantSlotPeekMode::Read)?;
|
||||||
peek_slot.and_then(|s| s.get_attached()).cloned()
|
peek_slot.and_then(|s| s.get_attached()).cloned()
|
||||||
@@ -1735,6 +1699,7 @@ impl TenantManager {
|
|||||||
let timelines = parent_shard.timelines.lock().unwrap().clone();
|
let timelines = parent_shard.timelines.lock().unwrap().clone();
|
||||||
let parent_timelines = timelines.keys().cloned().collect::<Vec<_>>();
|
let parent_timelines = timelines.keys().cloned().collect::<Vec<_>>();
|
||||||
for timeline in timelines.values() {
|
for timeline in timelines.values() {
|
||||||
|
tracing::info!(timeline_id=%timeline.timeline_id, "Loading list of layers to hardlink");
|
||||||
let timeline_layers = timeline
|
let timeline_layers = timeline
|
||||||
.layers
|
.layers
|
||||||
.read()
|
.read()
|
||||||
@@ -1774,7 +1739,12 @@ impl TenantManager {
|
|||||||
|
|
||||||
// Since we will do a large number of small filesystem metadata operations, batch them into
|
// Since we will do a large number of small filesystem metadata operations, batch them into
|
||||||
// spawn_blocking calls rather than doing each one as a tokio::fs round-trip.
|
// spawn_blocking calls rather than doing each one as a tokio::fs round-trip.
|
||||||
|
let span = tracing::Span::current();
|
||||||
let jh = tokio::task::spawn_blocking(move || -> anyhow::Result<usize> {
|
let jh = tokio::task::spawn_blocking(move || -> anyhow::Result<usize> {
|
||||||
|
// Run this synchronous code in the same log context as the outer function that spawned it.
|
||||||
|
let _span = span.enter();
|
||||||
|
|
||||||
|
tracing::info!("Creating {} directories", create_dirs.len());
|
||||||
for dir in &create_dirs {
|
for dir in &create_dirs {
|
||||||
if let Err(e) = std::fs::create_dir_all(dir) {
|
if let Err(e) = std::fs::create_dir_all(dir) {
|
||||||
// Ignore AlreadyExists errors, drop out on all other errors
|
// Ignore AlreadyExists errors, drop out on all other errors
|
||||||
@@ -1788,6 +1758,11 @@ impl TenantManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for child_prefix in child_prefixes {
|
for child_prefix in child_prefixes {
|
||||||
|
tracing::info!(
|
||||||
|
"Hard-linking {} parent layers into child path {}",
|
||||||
|
parent_layers.len(),
|
||||||
|
child_prefix
|
||||||
|
);
|
||||||
for relative_layer in &parent_layers {
|
for relative_layer in &parent_layers {
|
||||||
let parent_path = parent_path.join(relative_layer);
|
let parent_path = parent_path.join(relative_layer);
|
||||||
let child_path = child_prefix.join(relative_layer);
|
let child_path = child_prefix.join(relative_layer);
|
||||||
@@ -1813,6 +1788,7 @@ impl TenantManager {
|
|||||||
// Durability is not required for correctness, but if we crashed during split and
|
// Durability is not required for correctness, but if we crashed during split and
|
||||||
// then came restarted with empty timeline dirs, it would be very inefficient to
|
// then came restarted with empty timeline dirs, it would be very inefficient to
|
||||||
// re-populate from remote storage.
|
// re-populate from remote storage.
|
||||||
|
tracing::info!("fsyncing {} directories", create_dirs.len());
|
||||||
for dir in create_dirs {
|
for dir in create_dirs {
|
||||||
if let Err(e) = crashsafe::fsync(&dir) {
|
if let Err(e) = crashsafe::fsync(&dir) {
|
||||||
// Something removed a newly created timeline dir out from underneath us? Extremely
|
// Something removed a newly created timeline dir out from underneath us? Extremely
|
||||||
@@ -1866,7 +1842,7 @@ impl TenantManager {
|
|||||||
deletion_queue_client: &DeletionQueueClient,
|
deletion_queue_client: &DeletionQueueClient,
|
||||||
) -> Result<(), TenantStateError> {
|
) -> Result<(), TenantStateError> {
|
||||||
let tmp_path = self
|
let tmp_path = self
|
||||||
.detach_tenant0(conf, &TENANTS, tenant_shard_id, deletion_queue_client)
|
.detach_tenant0(conf, tenant_shard_id, deletion_queue_client)
|
||||||
.await?;
|
.await?;
|
||||||
spawn_background_purge(tmp_path);
|
spawn_background_purge(tmp_path);
|
||||||
|
|
||||||
@@ -1876,7 +1852,6 @@ impl TenantManager {
|
|||||||
async fn detach_tenant0(
|
async fn detach_tenant0(
|
||||||
&self,
|
&self,
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
tenants: &std::sync::RwLock<TenantsMap>,
|
|
||||||
tenant_shard_id: TenantShardId,
|
tenant_shard_id: TenantShardId,
|
||||||
deletion_queue_client: &DeletionQueueClient,
|
deletion_queue_client: &DeletionQueueClient,
|
||||||
) -> Result<Utf8PathBuf, TenantStateError> {
|
) -> Result<Utf8PathBuf, TenantStateError> {
|
||||||
@@ -1890,7 +1865,7 @@ impl TenantManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let removal_result = remove_tenant_from_memory(
|
let removal_result = remove_tenant_from_memory(
|
||||||
tenants,
|
self.tenants,
|
||||||
tenant_shard_id,
|
tenant_shard_id,
|
||||||
tenant_dir_rename_operation(tenant_shard_id),
|
tenant_dir_rename_operation(tenant_shard_id),
|
||||||
)
|
)
|
||||||
@@ -1906,7 +1881,7 @@ impl TenantManager {
|
|||||||
pub(crate) fn list_tenants(
|
pub(crate) fn list_tenants(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<(TenantShardId, TenantState, Generation)>, TenantMapListError> {
|
) -> Result<Vec<(TenantShardId, TenantState, Generation)>, TenantMapListError> {
|
||||||
let tenants = TENANTS.read().unwrap();
|
let tenants = self.tenants.read().unwrap();
|
||||||
let m = match &*tenants {
|
let m = match &*tenants {
|
||||||
TenantsMap::Initializing => return Err(TenantMapListError::Initializing),
|
TenantsMap::Initializing => return Err(TenantMapListError::Initializing),
|
||||||
TenantsMap::Open(m) | TenantsMap::ShuttingDown(m) => m,
|
TenantsMap::Open(m) | TenantsMap::ShuttingDown(m) => m,
|
||||||
@@ -2007,7 +1982,6 @@ impl TenantManager {
|
|||||||
AttachedTenantConf::try_from(config)?,
|
AttachedTenantConf::try_from(config)?,
|
||||||
shard_identity,
|
shard_identity,
|
||||||
None,
|
None,
|
||||||
self.tenants,
|
|
||||||
SpawnMode::Eager,
|
SpawnMode::Eager,
|
||||||
ctx,
|
ctx,
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -367,10 +367,9 @@ async fn upload_tenant_heatmap(
|
|||||||
debug_assert_current_span_has_tenant_id();
|
debug_assert_current_span_has_tenant_id();
|
||||||
|
|
||||||
let generation = tenant.get_generation();
|
let generation = tenant.get_generation();
|
||||||
|
debug_assert!(!generation.is_none());
|
||||||
if generation.is_none() {
|
if generation.is_none() {
|
||||||
// We do not expect this: generations were implemented before heatmap uploads. However,
|
// We do not expect this: None generations should only appear in historic layer metadata, not in running Tenants
|
||||||
// handle it so that we don't have to make the generation in the heatmap an Option<>
|
|
||||||
// (Generation::none is not serializable)
|
|
||||||
tracing::warn!("Skipping heatmap upload for tenant with generation==None");
|
tracing::warn!("Skipping heatmap upload for tenant with generation==None");
|
||||||
return Ok(UploadHeatmapOutcome::Skipped);
|
return Ok(UploadHeatmapOutcome::Skipped);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -928,7 +928,6 @@ impl DeltaLayerInner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Load all key-values in the delta layer, should be replaced by an iterator-based interface in the future.
|
/// Load all key-values in the delta layer, should be replaced by an iterator-based interface in the future.
|
||||||
#[cfg(test)]
|
|
||||||
pub(super) async fn load_key_values(
|
pub(super) async fn load_key_values(
|
||||||
&self,
|
&self,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
@@ -941,7 +940,7 @@ impl DeltaLayerInner {
|
|||||||
);
|
);
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut stream =
|
let mut stream =
|
||||||
Box::pin(self.stream_index_forwards(&index_reader, &[0; DELTA_KEY_SIZE], ctx));
|
Box::pin(self.stream_index_forwards(index_reader, &[0; DELTA_KEY_SIZE], ctx));
|
||||||
let block_reader = FileBlockReader::new(&self.file, self.file_id);
|
let block_reader = FileBlockReader::new(&self.file, self.file_id);
|
||||||
let cursor = block_reader.block_cursor();
|
let cursor = block_reader.block_cursor();
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
@@ -976,7 +975,7 @@ impl DeltaLayerInner {
|
|||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<Vec<VectoredRead>>
|
) -> anyhow::Result<Vec<VectoredRead>>
|
||||||
where
|
where
|
||||||
Reader: BlockReader,
|
Reader: BlockReader + Clone,
|
||||||
{
|
{
|
||||||
let ctx = RequestContextBuilder::extend(ctx)
|
let ctx = RequestContextBuilder::extend(ctx)
|
||||||
.page_content_kind(PageContentKind::DeltaLayerBtreeNode)
|
.page_content_kind(PageContentKind::DeltaLayerBtreeNode)
|
||||||
@@ -986,7 +985,7 @@ impl DeltaLayerInner {
|
|||||||
let mut range_end_handled = false;
|
let mut range_end_handled = false;
|
||||||
|
|
||||||
let start_key = DeltaKey::from_key_lsn(&range.start, lsn_range.start);
|
let start_key = DeltaKey::from_key_lsn(&range.start, lsn_range.start);
|
||||||
let index_stream = index_reader.get_stream_from(&start_key.0, &ctx);
|
let index_stream = index_reader.clone().into_stream(&start_key.0, &ctx);
|
||||||
let mut index_stream = std::pin::pin!(index_stream);
|
let mut index_stream = std::pin::pin!(index_stream);
|
||||||
|
|
||||||
while let Some(index_entry) = index_stream.next().await {
|
while let Some(index_entry) = index_stream.next().await {
|
||||||
@@ -1241,7 +1240,7 @@ impl DeltaLayerInner {
|
|||||||
block_reader,
|
block_reader,
|
||||||
);
|
);
|
||||||
|
|
||||||
let stream = self.stream_index_forwards(&tree_reader, &[0u8; DELTA_KEY_SIZE], ctx);
|
let stream = self.stream_index_forwards(tree_reader, &[0u8; DELTA_KEY_SIZE], ctx);
|
||||||
let stream = stream.map_ok(|(key, lsn, pos)| Item::Actual(key, lsn, pos));
|
let stream = stream.map_ok(|(key, lsn, pos)| Item::Actual(key, lsn, pos));
|
||||||
// put in a sentinel value for getting the end offset for last item, and not having to
|
// put in a sentinel value for getting the end offset for last item, and not having to
|
||||||
// repeat the whole read part
|
// repeat the whole read part
|
||||||
@@ -1300,7 +1299,7 @@ impl DeltaLayerInner {
|
|||||||
offsets.start.pos(),
|
offsets.start.pos(),
|
||||||
offsets.end.pos(),
|
offsets.end.pos(),
|
||||||
meta,
|
meta,
|
||||||
max_read_size,
|
Some(max_read_size),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1459,17 +1458,17 @@ impl DeltaLayerInner {
|
|||||||
|
|
||||||
fn stream_index_forwards<'a, R>(
|
fn stream_index_forwards<'a, R>(
|
||||||
&'a self,
|
&'a self,
|
||||||
reader: &'a DiskBtreeReader<R, DELTA_KEY_SIZE>,
|
reader: DiskBtreeReader<R, DELTA_KEY_SIZE>,
|
||||||
start: &'a [u8; DELTA_KEY_SIZE],
|
start: &'a [u8; DELTA_KEY_SIZE],
|
||||||
ctx: &'a RequestContext,
|
ctx: &'a RequestContext,
|
||||||
) -> impl futures::stream::Stream<
|
) -> impl futures::stream::Stream<
|
||||||
Item = Result<(Key, Lsn, BlobRef), crate::tenant::disk_btree::DiskBtreeError>,
|
Item = Result<(Key, Lsn, BlobRef), crate::tenant::disk_btree::DiskBtreeError>,
|
||||||
> + 'a
|
> + 'a
|
||||||
where
|
where
|
||||||
R: BlockReader,
|
R: BlockReader + 'a,
|
||||||
{
|
{
|
||||||
use futures::stream::TryStreamExt;
|
use futures::stream::TryStreamExt;
|
||||||
let stream = reader.get_stream_from(start, ctx);
|
let stream = reader.into_stream(start, ctx);
|
||||||
stream.map_ok(|(key, value)| {
|
stream.map_ok(|(key, value)| {
|
||||||
let key = DeltaKey::from_slice(&key);
|
let key = DeltaKey::from_slice(&key);
|
||||||
let (key, lsn) = (key.key(), key.lsn());
|
let (key, lsn) = (key.key(), key.lsn());
|
||||||
@@ -1493,6 +1492,24 @@ impl DeltaLayerInner {
|
|||||||
);
|
);
|
||||||
offset
|
offset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn iter<'a>(&'a self, ctx: &'a RequestContext) -> DeltaLayerIterator<'a> {
|
||||||
|
let block_reader = FileBlockReader::new(&self.file, self.file_id);
|
||||||
|
let tree_reader =
|
||||||
|
DiskBtreeReader::new(self.index_start_blk, self.index_root_blk, block_reader);
|
||||||
|
DeltaLayerIterator {
|
||||||
|
delta_layer: self,
|
||||||
|
ctx,
|
||||||
|
index_iter: tree_reader.iter(&[0; DELTA_KEY_SIZE], ctx),
|
||||||
|
key_values_batch: std::collections::VecDeque::new(),
|
||||||
|
is_end: false,
|
||||||
|
planner: crate::tenant::vectored_blob_io::StreamingVectoredReadPlanner::new(
|
||||||
|
1024 * 8192, // The default value. Unit tests might use a different value. 1024 * 8K = 8MB buffer.
|
||||||
|
1024, // The default value. Unit tests might use a different value
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A set of data associated with a delta layer key and its value
|
/// A set of data associated with a delta layer key and its value
|
||||||
@@ -1552,6 +1569,70 @@ impl<'a> pageserver_compaction::interface::CompactionDeltaEntry<'a, Key> for Del
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub struct DeltaLayerIterator<'a> {
|
||||||
|
delta_layer: &'a DeltaLayerInner,
|
||||||
|
ctx: &'a RequestContext,
|
||||||
|
planner: crate::tenant::vectored_blob_io::StreamingVectoredReadPlanner,
|
||||||
|
index_iter: crate::tenant::disk_btree::DiskBtreeIterator<'a>,
|
||||||
|
key_values_batch: std::collections::VecDeque<(Key, Lsn, Value)>,
|
||||||
|
is_end: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl<'a> DeltaLayerIterator<'a> {
|
||||||
|
/// Retrieve a batch of key-value pairs into the iterator buffer.
|
||||||
|
async fn next_batch(&mut self) -> anyhow::Result<()> {
|
||||||
|
assert!(self.key_values_batch.is_empty());
|
||||||
|
assert!(!self.is_end);
|
||||||
|
|
||||||
|
let plan = loop {
|
||||||
|
if let Some(res) = self.index_iter.next().await {
|
||||||
|
let (raw_key, value) = res?;
|
||||||
|
let key = Key::from_slice(&raw_key[..KEY_SIZE]);
|
||||||
|
let lsn = DeltaKey::extract_lsn_from_buf(&raw_key);
|
||||||
|
let blob_ref = BlobRef(value);
|
||||||
|
let offset = blob_ref.pos();
|
||||||
|
if let Some(batch_plan) = self.planner.handle(key, lsn, offset, BlobFlag::None) {
|
||||||
|
break batch_plan;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.is_end = true;
|
||||||
|
let data_end_offset = self.delta_layer.index_start_offset();
|
||||||
|
break self.planner.handle_range_end(data_end_offset);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let vectored_blob_reader = VectoredBlobReader::new(&self.delta_layer.file);
|
||||||
|
let mut next_batch = std::collections::VecDeque::new();
|
||||||
|
let buf_size = plan.size();
|
||||||
|
let buf = BytesMut::with_capacity(buf_size);
|
||||||
|
let blobs_buf = vectored_blob_reader
|
||||||
|
.read_blobs(&plan, buf, self.ctx)
|
||||||
|
.await?;
|
||||||
|
let frozen_buf = blobs_buf.buf.freeze();
|
||||||
|
for meta in blobs_buf.blobs.iter() {
|
||||||
|
let value = Value::des(&frozen_buf[meta.start..meta.end])?;
|
||||||
|
next_batch.push_back((meta.meta.key, meta.meta.lsn, value));
|
||||||
|
}
|
||||||
|
self.key_values_batch = next_batch;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn next(&mut self) -> anyhow::Result<Option<(Key, Lsn, Value)>> {
|
||||||
|
if self.key_values_batch.is_empty() {
|
||||||
|
if self.is_end {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
self.next_batch().await?;
|
||||||
|
}
|
||||||
|
Ok(Some(
|
||||||
|
self.key_values_batch
|
||||||
|
.pop_front()
|
||||||
|
.expect("should not be empty"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@@ -1561,6 +1642,9 @@ mod test {
|
|||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::tenant::harness::TIMELINE_ID;
|
||||||
|
use crate::tenant::vectored_blob_io::StreamingVectoredReadPlanner;
|
||||||
|
use crate::tenant::Tenant;
|
||||||
use crate::{
|
use crate::{
|
||||||
context::DownloadBehavior,
|
context::DownloadBehavior,
|
||||||
task_mgr::TaskKind,
|
task_mgr::TaskKind,
|
||||||
@@ -1857,7 +1941,7 @@ mod test {
|
|||||||
.finish(entries_meta.key_range.end, &timeline, &ctx)
|
.finish(entries_meta.key_range.end, &timeline, &ctx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let inner = resident.as_delta(&ctx).await?;
|
let inner = resident.get_as_delta(&ctx).await?;
|
||||||
|
|
||||||
let file_size = inner.file.metadata().await?.len();
|
let file_size = inner.file.metadata().await?.len();
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -2044,11 +2128,11 @@ mod test {
|
|||||||
|
|
||||||
let copied_layer = writer.finish(Key::MAX, &branch, ctx).await.unwrap();
|
let copied_layer = writer.finish(Key::MAX, &branch, ctx).await.unwrap();
|
||||||
|
|
||||||
copied_layer.as_delta(ctx).await.unwrap();
|
copied_layer.get_as_delta(ctx).await.unwrap();
|
||||||
|
|
||||||
assert_keys_and_values_eq(
|
assert_keys_and_values_eq(
|
||||||
new_layer.as_delta(ctx).await.unwrap(),
|
new_layer.get_as_delta(ctx).await.unwrap(),
|
||||||
copied_layer.as_delta(ctx).await.unwrap(),
|
copied_layer.get_as_delta(ctx).await.unwrap(),
|
||||||
truncate_at,
|
truncate_at,
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
@@ -2073,7 +2157,7 @@ mod test {
|
|||||||
source.index_root_blk,
|
source.index_root_blk,
|
||||||
&source_reader,
|
&source_reader,
|
||||||
);
|
);
|
||||||
let source_stream = source.stream_index_forwards(&source_tree, &start_key, ctx);
|
let source_stream = source.stream_index_forwards(source_tree, &start_key, ctx);
|
||||||
let source_stream = source_stream.filter(|res| match res {
|
let source_stream = source_stream.filter(|res| match res {
|
||||||
Ok((_, lsn, _)) => ready(lsn < &truncated_at),
|
Ok((_, lsn, _)) => ready(lsn < &truncated_at),
|
||||||
_ => ready(true),
|
_ => ready(true),
|
||||||
@@ -2086,7 +2170,7 @@ mod test {
|
|||||||
truncated.index_root_blk,
|
truncated.index_root_blk,
|
||||||
&truncated_reader,
|
&truncated_reader,
|
||||||
);
|
);
|
||||||
let truncated_stream = truncated.stream_index_forwards(&truncated_tree, &start_key, ctx);
|
let truncated_stream = truncated.stream_index_forwards(truncated_tree, &start_key, ctx);
|
||||||
let mut truncated_stream = std::pin::pin!(truncated_stream);
|
let mut truncated_stream = std::pin::pin!(truncated_stream);
|
||||||
|
|
||||||
let mut scratch_left = Vec::new();
|
let mut scratch_left = Vec::new();
|
||||||
@@ -2127,4 +2211,116 @@ mod test {
|
|||||||
assert_eq!(utils::Hex(&scratch_left), utils::Hex(&scratch_right));
|
assert_eq!(utils::Hex(&scratch_left), utils::Hex(&scratch_right));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn produce_delta_layer(
|
||||||
|
tenant: &Tenant,
|
||||||
|
tline: &Arc<Timeline>,
|
||||||
|
mut deltas: Vec<(Key, Lsn, Value)>,
|
||||||
|
ctx: &RequestContext,
|
||||||
|
) -> anyhow::Result<ResidentLayer> {
|
||||||
|
deltas.sort_by(|(k1, l1, _), (k2, l2, _)| (k1, l1).cmp(&(k2, l2)));
|
||||||
|
let (key_start, _, _) = deltas.first().unwrap();
|
||||||
|
let (key_max, _, _) = deltas.first().unwrap();
|
||||||
|
let lsn_min = deltas.iter().map(|(_, lsn, _)| lsn).min().unwrap();
|
||||||
|
let lsn_max = deltas.iter().map(|(_, lsn, _)| lsn).max().unwrap();
|
||||||
|
let lsn_end = Lsn(lsn_max.0 + 1);
|
||||||
|
let mut writer = DeltaLayerWriter::new(
|
||||||
|
tenant.conf,
|
||||||
|
tline.timeline_id,
|
||||||
|
tenant.tenant_shard_id,
|
||||||
|
*key_start,
|
||||||
|
(*lsn_min)..lsn_end,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let key_end = key_max.next();
|
||||||
|
|
||||||
|
for (key, lsn, value) in deltas {
|
||||||
|
writer.put_value(key, lsn, value, ctx).await?;
|
||||||
|
}
|
||||||
|
let delta_layer = writer.finish(key_end, tline, ctx).await?;
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>(delta_layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn assert_delta_iter_equal(
|
||||||
|
delta_iter: &mut DeltaLayerIterator<'_>,
|
||||||
|
expect: &[(Key, Lsn, Value)],
|
||||||
|
) {
|
||||||
|
let mut expect_iter = expect.iter();
|
||||||
|
loop {
|
||||||
|
let o1 = delta_iter.next().await.unwrap();
|
||||||
|
let o2 = expect_iter.next();
|
||||||
|
assert_eq!(o1.is_some(), o2.is_some());
|
||||||
|
if o1.is_none() && o2.is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let (k1, l1, v1) = o1.unwrap();
|
||||||
|
let (k2, l2, v2) = o2.unwrap();
|
||||||
|
assert_eq!(&k1, k2);
|
||||||
|
assert_eq!(l1, *l2);
|
||||||
|
assert_eq!(&v1, v2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn delta_layer_iterator() {
|
||||||
|
use crate::repository::Value;
|
||||||
|
use bytes::Bytes;
|
||||||
|
|
||||||
|
let harness = TenantHarness::create("delta_layer_iterator").unwrap();
|
||||||
|
let (tenant, ctx) = harness.load().await;
|
||||||
|
|
||||||
|
let tline = tenant
|
||||||
|
.create_test_timeline(TIMELINE_ID, Lsn(0x10), DEFAULT_PG_VERSION, &ctx)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
fn get_key(id: u32) -> Key {
|
||||||
|
let mut key = Key::from_hex("000000000033333333444444445500000000").unwrap();
|
||||||
|
key.field6 = id;
|
||||||
|
key
|
||||||
|
}
|
||||||
|
const N: usize = 1000;
|
||||||
|
let test_deltas = (0..N)
|
||||||
|
.map(|idx| {
|
||||||
|
(
|
||||||
|
get_key(idx as u32 / 10),
|
||||||
|
Lsn(0x10 * ((idx as u64) % 10 + 1)),
|
||||||
|
Value::Image(Bytes::from(format!("img{idx:05}"))),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect_vec();
|
||||||
|
let resident_layer = produce_delta_layer(&tenant, &tline, test_deltas.clone(), &ctx)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let delta_layer = resident_layer.get_as_delta(&ctx).await.unwrap();
|
||||||
|
for max_read_size in [1, 1024] {
|
||||||
|
for batch_size in [1, 2, 4, 8, 3, 7, 13] {
|
||||||
|
println!("running with batch_size={batch_size} max_read_size={max_read_size}");
|
||||||
|
// Test if the batch size is correctly determined
|
||||||
|
let mut iter = delta_layer.iter(&ctx);
|
||||||
|
iter.planner = StreamingVectoredReadPlanner::new(max_read_size, batch_size);
|
||||||
|
let mut num_items = 0;
|
||||||
|
for _ in 0..3 {
|
||||||
|
iter.next_batch().await.unwrap();
|
||||||
|
num_items += iter.key_values_batch.len();
|
||||||
|
if max_read_size == 1 {
|
||||||
|
// every key should be a batch b/c the value is larger than max_read_size
|
||||||
|
assert_eq!(iter.key_values_batch.len(), 1);
|
||||||
|
} else {
|
||||||
|
assert_eq!(iter.key_values_batch.len(), batch_size);
|
||||||
|
}
|
||||||
|
if num_items >= N {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
iter.key_values_batch.clear();
|
||||||
|
}
|
||||||
|
// Test if the result is correct
|
||||||
|
let mut iter = delta_layer.iter(&ctx);
|
||||||
|
iter.planner = StreamingVectoredReadPlanner::new(max_read_size, batch_size);
|
||||||
|
assert_delta_iter_equal(&mut iter, &test_deltas).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,7 +486,6 @@ impl ImageLayerInner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Load all key-values in the delta layer, should be replaced by an iterator-based interface in the future.
|
/// Load all key-values in the delta layer, should be replaced by an iterator-based interface in the future.
|
||||||
#[cfg(test)]
|
|
||||||
pub(super) async fn load_key_values(
|
pub(super) async fn load_key_values(
|
||||||
&self,
|
&self,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
@@ -495,7 +494,7 @@ impl ImageLayerInner {
|
|||||||
let tree_reader =
|
let tree_reader =
|
||||||
DiskBtreeReader::new(self.index_start_blk, self.index_root_blk, &block_reader);
|
DiskBtreeReader::new(self.index_start_blk, self.index_root_blk, &block_reader);
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut stream = Box::pin(tree_reader.get_stream_from(&[0; KEY_SIZE], ctx));
|
let mut stream = Box::pin(tree_reader.into_stream(&[0; KEY_SIZE], ctx));
|
||||||
let block_reader = FileBlockReader::new(&self.file, self.file_id);
|
let block_reader = FileBlockReader::new(&self.file, self.file_id);
|
||||||
let cursor = block_reader.block_cursor();
|
let cursor = block_reader.block_cursor();
|
||||||
while let Some(item) = stream.next().await {
|
while let Some(item) = stream.next().await {
|
||||||
@@ -544,7 +543,7 @@ impl ImageLayerInner {
|
|||||||
let mut search_key: [u8; KEY_SIZE] = [0u8; KEY_SIZE];
|
let mut search_key: [u8; KEY_SIZE] = [0u8; KEY_SIZE];
|
||||||
range.start.write_to_byte_slice(&mut search_key);
|
range.start.write_to_byte_slice(&mut search_key);
|
||||||
|
|
||||||
let index_stream = tree_reader.get_stream_from(&search_key, &ctx);
|
let index_stream = tree_reader.clone().into_stream(&search_key, &ctx);
|
||||||
let mut index_stream = std::pin::pin!(index_stream);
|
let mut index_stream = std::pin::pin!(index_stream);
|
||||||
|
|
||||||
while let Some(index_entry) = index_stream.next().await {
|
while let Some(index_entry) = index_stream.next().await {
|
||||||
@@ -689,6 +688,24 @@ impl ImageLayerInner {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn iter<'a>(&'a self, ctx: &'a RequestContext) -> ImageLayerIterator<'a> {
|
||||||
|
let block_reader = FileBlockReader::new(&self.file, self.file_id);
|
||||||
|
let tree_reader =
|
||||||
|
DiskBtreeReader::new(self.index_start_blk, self.index_root_blk, block_reader);
|
||||||
|
ImageLayerIterator {
|
||||||
|
image_layer: self,
|
||||||
|
ctx,
|
||||||
|
index_iter: tree_reader.iter(&[0; KEY_SIZE], ctx),
|
||||||
|
key_values_batch: std::collections::VecDeque::new(),
|
||||||
|
is_end: false,
|
||||||
|
planner: crate::tenant::vectored_blob_io::StreamingVectoredReadPlanner::new(
|
||||||
|
1024 * 8192, // The default value. Unit tests might use a different value. 1024 * 8K = 8MB buffer.
|
||||||
|
1024, // The default value. Unit tests might use a different value
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A builder object for constructing a new image layer.
|
/// A builder object for constructing a new image layer.
|
||||||
@@ -943,11 +960,77 @@ impl Drop for ImageLayerWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub struct ImageLayerIterator<'a> {
|
||||||
|
image_layer: &'a ImageLayerInner,
|
||||||
|
ctx: &'a RequestContext,
|
||||||
|
planner: crate::tenant::vectored_blob_io::StreamingVectoredReadPlanner,
|
||||||
|
index_iter: crate::tenant::disk_btree::DiskBtreeIterator<'a>,
|
||||||
|
key_values_batch: std::collections::VecDeque<(Key, Lsn, Value)>,
|
||||||
|
is_end: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl<'a> ImageLayerIterator<'a> {
|
||||||
|
/// Retrieve a batch of key-value pairs into the iterator buffer.
|
||||||
|
async fn next_batch(&mut self) -> anyhow::Result<()> {
|
||||||
|
assert!(self.key_values_batch.is_empty());
|
||||||
|
assert!(!self.is_end);
|
||||||
|
|
||||||
|
let plan = loop {
|
||||||
|
if let Some(res) = self.index_iter.next().await {
|
||||||
|
let (raw_key, offset) = res?;
|
||||||
|
if let Some(batch_plan) = self.planner.handle(
|
||||||
|
Key::from_slice(&raw_key[..KEY_SIZE]),
|
||||||
|
self.image_layer.lsn,
|
||||||
|
offset,
|
||||||
|
BlobFlag::None,
|
||||||
|
) {
|
||||||
|
break batch_plan;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.is_end = true;
|
||||||
|
let payload_end = self.image_layer.index_start_blk as u64 * PAGE_SZ as u64;
|
||||||
|
break self.planner.handle_range_end(payload_end);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let vectored_blob_reader = VectoredBlobReader::new(&self.image_layer.file);
|
||||||
|
let mut next_batch = std::collections::VecDeque::new();
|
||||||
|
let buf_size = plan.size();
|
||||||
|
let buf = BytesMut::with_capacity(buf_size);
|
||||||
|
let blobs_buf = vectored_blob_reader
|
||||||
|
.read_blobs(&plan, buf, self.ctx)
|
||||||
|
.await?;
|
||||||
|
let frozen_buf: Bytes = blobs_buf.buf.freeze();
|
||||||
|
for meta in blobs_buf.blobs.iter() {
|
||||||
|
let img_buf = frozen_buf.slice(meta.start..meta.end);
|
||||||
|
next_batch.push_back((meta.meta.key, self.image_layer.lsn, Value::Image(img_buf)));
|
||||||
|
}
|
||||||
|
self.key_values_batch = next_batch;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn next(&mut self) -> anyhow::Result<Option<(Key, Lsn, Value)>> {
|
||||||
|
if self.key_values_batch.is_empty() {
|
||||||
|
if self.is_end {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
self.next_batch().await?;
|
||||||
|
}
|
||||||
|
Ok(Some(
|
||||||
|
self.key_values_batch
|
||||||
|
.pop_front()
|
||||||
|
.expect("should not be empty"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::time::Duration;
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
use itertools::Itertools;
|
||||||
use pageserver_api::{
|
use pageserver_api::{
|
||||||
key::Key,
|
key::Key,
|
||||||
shard::{ShardCount, ShardIdentity, ShardNumber, ShardStripeSize},
|
shard::{ShardCount, ShardIdentity, ShardNumber, ShardStripeSize},
|
||||||
@@ -959,11 +1042,19 @@ mod test {
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
tenant::{config::TenantConf, harness::TenantHarness},
|
context::RequestContext,
|
||||||
|
repository::Value,
|
||||||
|
tenant::{
|
||||||
|
config::TenantConf,
|
||||||
|
harness::{TenantHarness, TIMELINE_ID},
|
||||||
|
storage_layer::ResidentLayer,
|
||||||
|
vectored_blob_io::StreamingVectoredReadPlanner,
|
||||||
|
Tenant, Timeline,
|
||||||
|
},
|
||||||
DEFAULT_PG_VERSION,
|
DEFAULT_PG_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::ImageLayerWriter;
|
use super::{ImageLayerIterator, ImageLayerWriter};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn image_layer_rewrite() {
|
async fn image_layer_rewrite() {
|
||||||
@@ -1134,4 +1225,111 @@ mod test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn produce_image_layer(
|
||||||
|
tenant: &Tenant,
|
||||||
|
tline: &Arc<Timeline>,
|
||||||
|
mut images: Vec<(Key, Bytes)>,
|
||||||
|
lsn: Lsn,
|
||||||
|
ctx: &RequestContext,
|
||||||
|
) -> anyhow::Result<ResidentLayer> {
|
||||||
|
images.sort();
|
||||||
|
let (key_start, _) = images.first().unwrap();
|
||||||
|
let (key_last, _) = images.last().unwrap();
|
||||||
|
let key_end = key_last.next();
|
||||||
|
let key_range = *key_start..key_end;
|
||||||
|
let mut writer = ImageLayerWriter::new(
|
||||||
|
tenant.conf,
|
||||||
|
tline.timeline_id,
|
||||||
|
tenant.tenant_shard_id,
|
||||||
|
&key_range,
|
||||||
|
lsn,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for (key, img) in images {
|
||||||
|
writer.put_image(key, img, ctx).await?;
|
||||||
|
}
|
||||||
|
let img_layer = writer.finish(tline, ctx).await?;
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>(img_layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn assert_img_iter_equal(
|
||||||
|
img_iter: &mut ImageLayerIterator<'_>,
|
||||||
|
expect: &[(Key, Bytes)],
|
||||||
|
expect_lsn: Lsn,
|
||||||
|
) {
|
||||||
|
let mut expect_iter = expect.iter();
|
||||||
|
loop {
|
||||||
|
let o1 = img_iter.next().await.unwrap();
|
||||||
|
let o2 = expect_iter.next();
|
||||||
|
match (o1, o2) {
|
||||||
|
(None, None) => break,
|
||||||
|
(Some((k1, l1, v1)), Some((k2, i2))) => {
|
||||||
|
let Value::Image(i1) = v1 else {
|
||||||
|
panic!("expect Value::Image")
|
||||||
|
};
|
||||||
|
assert_eq!(&k1, k2);
|
||||||
|
assert_eq!(l1, expect_lsn);
|
||||||
|
assert_eq!(&i1, i2);
|
||||||
|
}
|
||||||
|
(o1, o2) => panic!("iterators length mismatch: {:?}, {:?}", o1, o2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn image_layer_iterator() {
|
||||||
|
let harness = TenantHarness::create("image_layer_iterator").unwrap();
|
||||||
|
let (tenant, ctx) = harness.load().await;
|
||||||
|
|
||||||
|
let tline = tenant
|
||||||
|
.create_test_timeline(TIMELINE_ID, Lsn(0x10), DEFAULT_PG_VERSION, &ctx)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
fn get_key(id: u32) -> Key {
|
||||||
|
let mut key = Key::from_hex("000000000033333333444444445500000000").unwrap();
|
||||||
|
key.field6 = id;
|
||||||
|
key
|
||||||
|
}
|
||||||
|
const N: usize = 1000;
|
||||||
|
let test_imgs = (0..N)
|
||||||
|
.map(|idx| (get_key(idx as u32), Bytes::from(format!("img{idx:05}"))))
|
||||||
|
.collect_vec();
|
||||||
|
let resident_layer =
|
||||||
|
produce_image_layer(&tenant, &tline, test_imgs.clone(), Lsn(0x10), &ctx)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let img_layer = resident_layer.get_as_image(&ctx).await.unwrap();
|
||||||
|
for max_read_size in [1, 1024] {
|
||||||
|
for batch_size in [1, 2, 4, 8, 3, 7, 13] {
|
||||||
|
println!("running with batch_size={batch_size} max_read_size={max_read_size}");
|
||||||
|
// Test if the batch size is correctly determined
|
||||||
|
let mut iter = img_layer.iter(&ctx);
|
||||||
|
iter.planner = StreamingVectoredReadPlanner::new(max_read_size, batch_size);
|
||||||
|
let mut num_items = 0;
|
||||||
|
for _ in 0..3 {
|
||||||
|
iter.next_batch().await.unwrap();
|
||||||
|
num_items += iter.key_values_batch.len();
|
||||||
|
if max_read_size == 1 {
|
||||||
|
// every key should be a batch b/c the value is larger than max_read_size
|
||||||
|
assert_eq!(iter.key_values_batch.len(), 1);
|
||||||
|
} else {
|
||||||
|
assert_eq!(iter.key_values_batch.len(), batch_size);
|
||||||
|
}
|
||||||
|
if num_items >= N {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
iter.key_values_batch.clear();
|
||||||
|
}
|
||||||
|
// Test if the result is correct
|
||||||
|
let mut iter = img_layer.iter(&ctx);
|
||||||
|
iter.planner = StreamingVectoredReadPlanner::new(max_read_size, batch_size);
|
||||||
|
assert_img_iter_equal(&mut iter, &test_imgs, Lsn(0x10)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -622,18 +622,16 @@ impl InMemoryLayer {
|
|||||||
|
|
||||||
let end_lsn = *self.end_lsn.get().unwrap();
|
let end_lsn = *self.end_lsn.get().unwrap();
|
||||||
|
|
||||||
let keys: Vec<_> = if let Some(key_range) = key_range {
|
let key_count = if let Some(key_range) = key_range {
|
||||||
inner
|
inner
|
||||||
.index
|
.index
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(k, _)| key_range.contains(k))
|
.filter(|(k, _)| key_range.contains(k))
|
||||||
.map(|(k, m)| (k.to_i128(), m))
|
.count()
|
||||||
.collect()
|
|
||||||
} else {
|
} else {
|
||||||
inner.index.iter().map(|(k, m)| (k.to_i128(), m)).collect()
|
inner.index.len()
|
||||||
};
|
};
|
||||||
|
if key_count == 0 {
|
||||||
if keys.is_empty() {
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,16 +93,12 @@ pub(crate) struct Layer(Arc<LayerInner>);
|
|||||||
|
|
||||||
impl std::fmt::Display for Layer {
|
impl std::fmt::Display for Layer {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
if matches!(self.0.generation, Generation::Broken) {
|
write!(
|
||||||
write!(f, "{}-broken", self.layer_desc().short_id())
|
f,
|
||||||
} else {
|
"{}{}",
|
||||||
write!(
|
self.layer_desc().short_id(),
|
||||||
f,
|
self.0.generation.get_suffix()
|
||||||
"{}{}",
|
)
|
||||||
self.layer_desc().short_id(),
|
|
||||||
self.0.generation.get_suffix()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,7 +385,6 @@ impl Layer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get all key/values in the layer. Should be replaced with an iterator-based API in the future.
|
/// Get all key/values in the layer. Should be replaced with an iterator-based API in the future.
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) async fn load_key_values(
|
pub(crate) async fn load_key_values(
|
||||||
&self,
|
&self,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
@@ -1774,7 +1769,6 @@ impl DownloadedLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
async fn load_key_values(
|
async fn load_key_values(
|
||||||
&self,
|
&self,
|
||||||
owner: &Arc<LayerInner>,
|
owner: &Arc<LayerInner>,
|
||||||
@@ -1905,7 +1899,7 @@ impl ResidentLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) async fn as_delta(
|
pub(crate) async fn get_as_delta(
|
||||||
&self,
|
&self,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<&delta_layer::DeltaLayerInner> {
|
) -> anyhow::Result<&delta_layer::DeltaLayerInner> {
|
||||||
@@ -1915,6 +1909,18 @@ impl ResidentLayer {
|
|||||||
Image(_) => Err(anyhow::anyhow!("image layer")),
|
Image(_) => Err(anyhow::anyhow!("image layer")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) async fn get_as_image(
|
||||||
|
&self,
|
||||||
|
ctx: &RequestContext,
|
||||||
|
) -> anyhow::Result<&image_layer::ImageLayerInner> {
|
||||||
|
use LayerKind::*;
|
||||||
|
match self.downloaded.get(&self.owner.0, ctx).await? {
|
||||||
|
Image(ref d) => Ok(d),
|
||||||
|
Delta(_) => Err(anyhow::anyhow!("delta layer")),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsLayerDesc for ResidentLayer {
|
impl AsLayerDesc for ResidentLayer {
|
||||||
|
|||||||
@@ -686,6 +686,7 @@ pub enum GetLogicalSizePriority {
|
|||||||
pub(crate) enum CompactFlags {
|
pub(crate) enum CompactFlags {
|
||||||
ForceRepartition,
|
ForceRepartition,
|
||||||
ForceImageLayerCreation,
|
ForceImageLayerCreation,
|
||||||
|
EnhancedGcBottomMostCompaction,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Timeline {
|
impl std::fmt::Debug for Timeline {
|
||||||
@@ -1096,7 +1097,6 @@ impl Timeline {
|
|||||||
/// scan iterator interface. We could optimize this interface later to avoid some checks in the vectored
|
/// scan iterator interface. We could optimize this interface later to avoid some checks in the vectored
|
||||||
/// get path to maintain and split the probing and to-be-probe keyspace. We also need to ensure that
|
/// get path to maintain and split the probing and to-be-probe keyspace. We also need to ensure that
|
||||||
/// the scan operation will not cause OOM in the future.
|
/// the scan operation will not cause OOM in the future.
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) async fn scan(
|
pub(crate) async fn scan(
|
||||||
&self,
|
&self,
|
||||||
keyspace: KeySpace,
|
keyspace: KeySpace,
|
||||||
@@ -5481,12 +5481,12 @@ impl Timeline {
|
|||||||
}
|
}
|
||||||
images.sort_unstable_by(|(ka, _), (kb, _)| ka.cmp(kb));
|
images.sort_unstable_by(|(ka, _), (kb, _)| ka.cmp(kb));
|
||||||
let min_key = *images.first().map(|(k, _)| k).unwrap();
|
let min_key = *images.first().map(|(k, _)| k).unwrap();
|
||||||
let max_key = images.last().map(|(k, _)| k).unwrap().next();
|
let end_key = images.last().map(|(k, _)| k).unwrap().next();
|
||||||
let mut image_layer_writer = ImageLayerWriter::new(
|
let mut image_layer_writer = ImageLayerWriter::new(
|
||||||
self.conf,
|
self.conf,
|
||||||
self.timeline_id,
|
self.timeline_id,
|
||||||
self.tenant_shard_id,
|
self.tenant_shard_id,
|
||||||
&(min_key..max_key),
|
&(min_key..end_key),
|
||||||
lsn,
|
lsn,
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
@@ -5518,7 +5518,7 @@ impl Timeline {
|
|||||||
let last_record_lsn = self.get_last_record_lsn();
|
let last_record_lsn = self.get_last_record_lsn();
|
||||||
deltas.sort_unstable_by(|(ka, la, _), (kb, lb, _)| (ka, la).cmp(&(kb, lb)));
|
deltas.sort_unstable_by(|(ka, la, _), (kb, lb, _)| (ka, la).cmp(&(kb, lb)));
|
||||||
let min_key = *deltas.first().map(|(k, _, _)| k).unwrap();
|
let min_key = *deltas.first().map(|(k, _, _)| k).unwrap();
|
||||||
let max_key = deltas.last().map(|(k, _, _)| k).unwrap().next();
|
let end_key = deltas.last().map(|(k, _, _)| k).unwrap().next();
|
||||||
let min_lsn = *deltas.iter().map(|(_, lsn, _)| lsn).min().unwrap();
|
let min_lsn = *deltas.iter().map(|(_, lsn, _)| lsn).min().unwrap();
|
||||||
let max_lsn = *deltas.iter().map(|(_, lsn, _)| lsn).max().unwrap();
|
let max_lsn = *deltas.iter().map(|(_, lsn, _)| lsn).max().unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -5541,7 +5541,7 @@ impl Timeline {
|
|||||||
for (key, lsn, val) in deltas {
|
for (key, lsn, val) in deltas {
|
||||||
delta_layer_writer.put_value(key, lsn, val, ctx).await?;
|
delta_layer_writer.put_value(key, lsn, val, ctx).await?;
|
||||||
}
|
}
|
||||||
let delta_layer = delta_layer_writer.finish(max_key, self, ctx).await?;
|
let delta_layer = delta_layer_writer.finish(end_key, self, ctx).await?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut guard = self.layers.write().await;
|
let mut guard = self.layers.write().await;
|
||||||
|
|||||||
@@ -47,10 +47,14 @@ impl Timeline {
|
|||||||
/// TODO: cancellation
|
/// TODO: cancellation
|
||||||
pub(crate) async fn compact_legacy(
|
pub(crate) async fn compact_legacy(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
_cancel: &CancellationToken,
|
cancel: &CancellationToken,
|
||||||
flags: EnumSet<CompactFlags>,
|
flags: EnumSet<CompactFlags>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<(), CompactionError> {
|
) -> Result<(), CompactionError> {
|
||||||
|
if flags.contains(CompactFlags::EnhancedGcBottomMostCompaction) {
|
||||||
|
return self.compact_with_gc(cancel, ctx).await;
|
||||||
|
}
|
||||||
|
|
||||||
// High level strategy for compaction / image creation:
|
// High level strategy for compaction / image creation:
|
||||||
//
|
//
|
||||||
// 1. First, calculate the desired "partitioning" of the
|
// 1. First, calculate the desired "partitioning" of the
|
||||||
@@ -959,13 +963,20 @@ impl Timeline {
|
|||||||
/// the GC horizon without considering retain_lsns. Then, it does a full compaction over all these delta
|
/// the GC horizon without considering retain_lsns. Then, it does a full compaction over all these delta
|
||||||
/// layers and image layers, which generates image layers on the gc horizon, drop deltas below gc horizon,
|
/// layers and image layers, which generates image layers on the gc horizon, drop deltas below gc horizon,
|
||||||
/// and create delta layers with all deltas >= gc horizon.
|
/// and create delta layers with all deltas >= gc horizon.
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) async fn compact_with_gc(
|
pub(crate) async fn compact_with_gc(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
_cancel: &CancellationToken,
|
_cancel: &CancellationToken,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<(), CompactionError> {
|
) -> Result<(), CompactionError> {
|
||||||
use crate::tenant::storage_layer::ValueReconstructState;
|
use crate::tenant::storage_layer::ValueReconstructState;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
info!("running enhanced gc bottom-most compaction");
|
||||||
|
|
||||||
|
scopeguard::defer! {
|
||||||
|
info!("done enhanced gc bottom-most compaction");
|
||||||
|
};
|
||||||
|
|
||||||
// Step 0: pick all delta layers + image layers below/intersect with the GC horizon.
|
// Step 0: pick all delta layers + image layers below/intersect with the GC horizon.
|
||||||
// The layer selection has the following properties:
|
// The layer selection has the following properties:
|
||||||
// 1. If a layer is in the selection, all layers below it are in the selection.
|
// 1. If a layer is in the selection, all layers below it are in the selection.
|
||||||
@@ -974,6 +985,11 @@ impl Timeline {
|
|||||||
let guard = self.layers.read().await;
|
let guard = self.layers.read().await;
|
||||||
let layers = guard.layer_map();
|
let layers = guard.layer_map();
|
||||||
let gc_info = self.gc_info.read().unwrap();
|
let gc_info = self.gc_info.read().unwrap();
|
||||||
|
if !gc_info.retain_lsns.is_empty() || !gc_info.leases.is_empty() {
|
||||||
|
return Err(CompactionError::Other(anyhow!(
|
||||||
|
"enhanced legacy compaction currently does not support retain_lsns (branches)"
|
||||||
|
)));
|
||||||
|
}
|
||||||
let gc_cutoff = Lsn::min(gc_info.cutoffs.horizon, gc_info.cutoffs.pitr);
|
let gc_cutoff = Lsn::min(gc_info.cutoffs.horizon, gc_info.cutoffs.pitr);
|
||||||
let mut selected_layers = Vec::new();
|
let mut selected_layers = Vec::new();
|
||||||
// TODO: consider retain_lsns
|
// TODO: consider retain_lsns
|
||||||
@@ -985,21 +1001,36 @@ impl Timeline {
|
|||||||
}
|
}
|
||||||
(selected_layers, gc_cutoff)
|
(selected_layers, gc_cutoff)
|
||||||
};
|
};
|
||||||
|
info!(
|
||||||
|
"picked {} layers for compaction with gc_cutoff={}",
|
||||||
|
layer_selection.len(),
|
||||||
|
gc_cutoff
|
||||||
|
);
|
||||||
// Step 1: (In the future) construct a k-merge iterator over all layers. For now, simply collect all keys + LSNs.
|
// Step 1: (In the future) construct a k-merge iterator over all layers. For now, simply collect all keys + LSNs.
|
||||||
|
// Also, collect the layer information to decide when to split the new delta layers.
|
||||||
let mut all_key_values = Vec::new();
|
let mut all_key_values = Vec::new();
|
||||||
|
let mut delta_split_points = BTreeSet::new();
|
||||||
for layer in &layer_selection {
|
for layer in &layer_selection {
|
||||||
all_key_values.extend(layer.load_key_values(ctx).await?);
|
all_key_values.extend(layer.load_key_values(ctx).await?);
|
||||||
|
let desc = layer.layer_desc();
|
||||||
|
if desc.is_delta() {
|
||||||
|
// TODO: is it correct to only record split points for deltas intersecting with the GC horizon? (exclude those below/above the horizon)
|
||||||
|
// so that we can avoid having too many small delta layers.
|
||||||
|
let key_range = desc.get_key_range();
|
||||||
|
delta_split_points.insert(key_range.start);
|
||||||
|
delta_split_points.insert(key_range.end);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Key small to large, LSN low to high, if the same LSN has both image and delta due to the merge of delta layers and
|
// Key small to large, LSN low to high, if the same LSN has both image and delta due to the merge of delta layers and
|
||||||
// image layers, make image appear later than delta.
|
// image layers, make image appear before than delta.
|
||||||
struct ValueWrapper<'a>(&'a crate::repository::Value);
|
struct ValueWrapper<'a>(&'a crate::repository::Value);
|
||||||
impl Ord for ValueWrapper<'_> {
|
impl Ord for ValueWrapper<'_> {
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
use crate::repository::Value;
|
use crate::repository::Value;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
match (self.0, other.0) {
|
match (self.0, other.0) {
|
||||||
(Value::Image(_), Value::WalRecord(_)) => Ordering::Greater,
|
(Value::Image(_), Value::WalRecord(_)) => Ordering::Less,
|
||||||
(Value::WalRecord(_), Value::Image(_)) => Ordering::Less,
|
(Value::WalRecord(_), Value::Image(_)) => Ordering::Greater,
|
||||||
_ => Ordering::Equal,
|
_ => Ordering::Equal,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1018,13 +1049,6 @@ impl Timeline {
|
|||||||
all_key_values.sort_by(|(k1, l1, v1), (k2, l2, v2)| {
|
all_key_values.sort_by(|(k1, l1, v1), (k2, l2, v2)| {
|
||||||
(k1, l1, ValueWrapper(v1)).cmp(&(k2, l2, ValueWrapper(v2)))
|
(k1, l1, ValueWrapper(v1)).cmp(&(k2, l2, ValueWrapper(v2)))
|
||||||
});
|
});
|
||||||
let max_lsn = all_key_values
|
|
||||||
.iter()
|
|
||||||
.map(|(_, lsn, _)| lsn)
|
|
||||||
.max()
|
|
||||||
.copied()
|
|
||||||
.unwrap()
|
|
||||||
+ 1;
|
|
||||||
// Step 2: Produce images+deltas. TODO: ensure newly-produced delta does not overlap with other deltas.
|
// Step 2: Produce images+deltas. TODO: ensure newly-produced delta does not overlap with other deltas.
|
||||||
// Data of the same key.
|
// Data of the same key.
|
||||||
let mut accumulated_values = Vec::new();
|
let mut accumulated_values = Vec::new();
|
||||||
@@ -1043,14 +1067,24 @@ impl Timeline {
|
|||||||
// We have a list of deltas/images. We want to create image layers while collect garbages.
|
// We have a list of deltas/images. We want to create image layers while collect garbages.
|
||||||
for (key, lsn, val) in accumulated_values.iter().rev() {
|
for (key, lsn, val) in accumulated_values.iter().rev() {
|
||||||
if *lsn > horizon {
|
if *lsn > horizon {
|
||||||
keys_above_horizon.push((*key, *lsn, val.clone())); // TODO: ensure one LSN corresponds to either delta or image instead of both
|
if let Some((_, prev_lsn, _)) = keys_above_horizon.last_mut() {
|
||||||
|
if *prev_lsn == *lsn {
|
||||||
|
// The case that we have an LSN with both data from the delta layer and the image layer. As
|
||||||
|
// `ValueWrapper` ensures that an image is ordered before a delta at the same LSN, we simply
|
||||||
|
// drop this delta and keep the image.
|
||||||
|
//
|
||||||
|
// For example, we have delta layer key1@0x10, key1@0x20, and image layer key1@0x10, we will
|
||||||
|
// keep the image for key1@0x10 and the delta for key1@0x20. key1@0x10 delta will be simply
|
||||||
|
// dropped.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys_above_horizon.push((*key, *lsn, val.clone()));
|
||||||
} else if *lsn <= horizon {
|
} else if *lsn <= horizon {
|
||||||
match val {
|
match val {
|
||||||
crate::repository::Value::Image(image) => {
|
crate::repository::Value::Image(image) => {
|
||||||
if lsn <= &horizon {
|
base_image = Some((*lsn, image.clone()));
|
||||||
base_image = Some((*lsn, image.clone()));
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
crate::repository::Value::WalRecord(wal) => {
|
crate::repository::Value::WalRecord(wal) => {
|
||||||
delta_above_base_image.push((*lsn, wal.clone()));
|
delta_above_base_image.push((*lsn, wal.clone()));
|
||||||
@@ -1058,7 +1092,7 @@ impl Timeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delta_above_base_image.reverse();
|
// do not reverse delta_above_base_image, reconstruct state expects reversely-ordered records
|
||||||
keys_above_horizon.reverse();
|
keys_above_horizon.reverse();
|
||||||
let state = ValueReconstructState {
|
let state = ValueReconstructState {
|
||||||
img: base_image,
|
img: base_image,
|
||||||
@@ -1068,15 +1102,59 @@ impl Timeline {
|
|||||||
Ok((keys_above_horizon, img))
|
Ok((keys_above_horizon, img))
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut delta_layer_writer = DeltaLayerWriter::new(
|
async fn flush_deltas(
|
||||||
self.conf,
|
deltas: &mut Vec<(Key, Lsn, crate::repository::Value)>,
|
||||||
self.timeline_id,
|
last_key: Key,
|
||||||
self.tenant_shard_id,
|
delta_split_points: &[Key],
|
||||||
all_key_values.first().unwrap().0,
|
current_delta_split_point: &mut usize,
|
||||||
gc_cutoff..max_lsn, // TODO: off by one?
|
tline: &Arc<Timeline>,
|
||||||
ctx,
|
gc_cutoff: Lsn,
|
||||||
)
|
ctx: &RequestContext,
|
||||||
.await?;
|
) -> anyhow::Result<Option<ResidentLayer>> {
|
||||||
|
// Check if we need to split the delta layer. We split at the original delta layer boundary to avoid
|
||||||
|
// overlapping layers.
|
||||||
|
//
|
||||||
|
// If we have a structure like this:
|
||||||
|
//
|
||||||
|
// | Delta 1 | | Delta 4 |
|
||||||
|
// |---------| Delta 2 |---------|
|
||||||
|
// | Delta 3 | | Delta 5 |
|
||||||
|
//
|
||||||
|
// And we choose to compact delta 2+3+5. We will get an overlapping delta layer with delta 1+4.
|
||||||
|
// A simple solution here is to split the delta layers using the original boundary, while this
|
||||||
|
// might produce a lot of small layers. This should be improved and fixed in the future.
|
||||||
|
let mut need_split = false;
|
||||||
|
while *current_delta_split_point < delta_split_points.len()
|
||||||
|
&& last_key >= delta_split_points[*current_delta_split_point]
|
||||||
|
{
|
||||||
|
*current_delta_split_point += 1;
|
||||||
|
need_split = true;
|
||||||
|
}
|
||||||
|
if !need_split {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let deltas = std::mem::take(deltas);
|
||||||
|
if deltas.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let end_lsn = deltas.iter().map(|(_, lsn, _)| lsn).max().copied().unwrap() + 1;
|
||||||
|
let mut delta_layer_writer = DeltaLayerWriter::new(
|
||||||
|
tline.conf,
|
||||||
|
tline.timeline_id,
|
||||||
|
tline.tenant_shard_id,
|
||||||
|
deltas.first().unwrap().0,
|
||||||
|
gc_cutoff..end_lsn,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let key_end = deltas.last().unwrap().0.next();
|
||||||
|
for (key, lsn, val) in deltas {
|
||||||
|
delta_layer_writer.put_value(key, lsn, val, ctx).await?;
|
||||||
|
}
|
||||||
|
let delta_layer = delta_layer_writer.finish(key_end, tline, ctx).await?;
|
||||||
|
Ok(Some(delta_layer))
|
||||||
|
}
|
||||||
|
|
||||||
let mut image_layer_writer = ImageLayerWriter::new(
|
let mut image_layer_writer = ImageLayerWriter::new(
|
||||||
self.conf,
|
self.conf,
|
||||||
self.timeline_id,
|
self.timeline_id,
|
||||||
@@ -1087,6 +1165,10 @@ impl Timeline {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let mut delta_values = Vec::new();
|
||||||
|
let delta_split_points = delta_split_points.into_iter().collect_vec();
|
||||||
|
let mut current_delta_split_point = 0;
|
||||||
|
let mut delta_layers = Vec::new();
|
||||||
for item @ (key, _, _) in &all_key_values {
|
for item @ (key, _, _) in &all_key_values {
|
||||||
if &last_key == key {
|
if &last_key == key {
|
||||||
accumulated_values.push(item);
|
accumulated_values.push(item);
|
||||||
@@ -1094,34 +1176,63 @@ impl Timeline {
|
|||||||
let (deltas, image) =
|
let (deltas, image) =
|
||||||
flush_accumulated_states(self, last_key, &accumulated_values, gc_cutoff)
|
flush_accumulated_states(self, last_key, &accumulated_values, gc_cutoff)
|
||||||
.await?;
|
.await?;
|
||||||
|
// Put the image into the image layer. Currently we have a single big layer for the compaction.
|
||||||
image_layer_writer.put_image(last_key, image, ctx).await?;
|
image_layer_writer.put_image(last_key, image, ctx).await?;
|
||||||
for (key, lsn, val) in deltas {
|
delta_values.extend(deltas);
|
||||||
delta_layer_writer.put_value(key, lsn, val, ctx).await?;
|
delta_layers.extend(
|
||||||
}
|
flush_deltas(
|
||||||
|
&mut delta_values,
|
||||||
|
last_key,
|
||||||
|
&delta_split_points,
|
||||||
|
&mut current_delta_split_point,
|
||||||
|
self,
|
||||||
|
gc_cutoff,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
accumulated_values.clear();
|
accumulated_values.clear();
|
||||||
accumulated_values.push(item);
|
accumulated_values.push(item);
|
||||||
last_key = *key;
|
last_key = *key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: move this part to the loop body
|
||||||
let (deltas, image) =
|
let (deltas, image) =
|
||||||
flush_accumulated_states(self, last_key, &accumulated_values, gc_cutoff).await?;
|
flush_accumulated_states(self, last_key, &accumulated_values, gc_cutoff).await?;
|
||||||
|
// Put the image into the image layer. Currently we have a single big layer for the compaction.
|
||||||
image_layer_writer.put_image(last_key, image, ctx).await?;
|
image_layer_writer.put_image(last_key, image, ctx).await?;
|
||||||
for (key, lsn, val) in deltas {
|
delta_values.extend(deltas);
|
||||||
delta_layer_writer.put_value(key, lsn, val, ctx).await?;
|
delta_layers.extend(
|
||||||
}
|
flush_deltas(
|
||||||
accumulated_values.clear();
|
&mut delta_values,
|
||||||
// TODO: split layers
|
last_key,
|
||||||
let delta_layer = delta_layer_writer.finish(last_key, self, ctx).await?;
|
&delta_split_points,
|
||||||
|
&mut current_delta_split_point,
|
||||||
|
self,
|
||||||
|
gc_cutoff,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
|
||||||
let image_layer = image_layer_writer.finish(self, ctx).await?;
|
let image_layer = image_layer_writer.finish(self, ctx).await?;
|
||||||
|
info!(
|
||||||
|
"produced {} delta layers and {} image layers",
|
||||||
|
delta_layers.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
let mut compact_to = Vec::new();
|
||||||
|
compact_to.extend(delta_layers);
|
||||||
|
compact_to.push(image_layer);
|
||||||
// Step 3: Place back to the layer map.
|
// Step 3: Place back to the layer map.
|
||||||
{
|
{
|
||||||
let mut guard = self.layers.write().await;
|
let mut guard = self.layers.write().await;
|
||||||
guard.finish_gc_compaction(
|
guard.finish_gc_compaction(&layer_selection, &compact_to, &self.metrics)
|
||||||
&layer_selection,
|
|
||||||
&[delta_layer.clone(), image_layer.clone()],
|
|
||||||
&self.metrics,
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.remote_client
|
||||||
|
.schedule_compaction_update(&layer_selection, &compact_to)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,7 +255,6 @@ impl DeleteTimelineFlow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shortcut to create Timeline in stopping state and spawn deletion task.
|
/// Shortcut to create Timeline in stopping state and spawn deletion task.
|
||||||
/// See corresponding parts of [`crate::tenant::delete::DeleteTenantFlow`]
|
|
||||||
#[instrument(skip_all, fields(%timeline_id))]
|
#[instrument(skip_all, fields(%timeline_id))]
|
||||||
pub async fn resume_deletion(
|
pub async fn resume_deletion(
|
||||||
tenant: Arc<Tenant>,
|
tenant: Arc<Tenant>,
|
||||||
@@ -420,10 +419,6 @@ impl DeleteTimelineFlow {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_finished(&self) -> bool {
|
|
||||||
matches!(self, Self::Finished)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_not_started(&self) -> bool {
|
pub(crate) fn is_not_started(&self) -> bool {
|
||||||
matches!(self, Self::NotStarted)
|
matches!(self, Self::NotStarted)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,7 +227,6 @@ impl LayerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Called when a GC-compaction is completed.
|
/// Called when a GC-compaction is completed.
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn finish_gc_compaction(
|
pub(crate) fn finish_gc_compaction(
|
||||||
&mut self,
|
&mut self,
|
||||||
compact_from: &[Layer],
|
compact_from: &[Layer],
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ pub(crate) struct VectoredReadBuilder {
|
|||||||
start: u64,
|
start: u64,
|
||||||
end: u64,
|
end: u64,
|
||||||
blobs_at: VecMap<u64, BlobMeta>,
|
blobs_at: VecMap<u64, BlobMeta>,
|
||||||
max_read_size: usize,
|
max_read_size: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VectoredReadBuilder {
|
impl VectoredReadBuilder {
|
||||||
@@ -90,7 +90,7 @@ impl VectoredReadBuilder {
|
|||||||
start_offset: u64,
|
start_offset: u64,
|
||||||
end_offset: u64,
|
end_offset: u64,
|
||||||
meta: BlobMeta,
|
meta: BlobMeta,
|
||||||
max_read_size: usize,
|
max_read_size: Option<usize>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut blobs_at = VecMap::default();
|
let mut blobs_at = VecMap::default();
|
||||||
blobs_at
|
blobs_at
|
||||||
@@ -111,7 +111,13 @@ impl VectoredReadBuilder {
|
|||||||
pub(crate) fn extend(&mut self, start: u64, end: u64, meta: BlobMeta) -> VectoredReadExtended {
|
pub(crate) fn extend(&mut self, start: u64, end: u64, meta: BlobMeta) -> VectoredReadExtended {
|
||||||
tracing::trace!(start, end, "trying to extend");
|
tracing::trace!(start, end, "trying to extend");
|
||||||
let size = (end - start) as usize;
|
let size = (end - start) as usize;
|
||||||
if self.end == start && self.size() + size <= self.max_read_size {
|
if self.end == start && {
|
||||||
|
if let Some(max_read_size) = self.max_read_size {
|
||||||
|
self.size() + size <= max_read_size
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} {
|
||||||
self.end = end;
|
self.end = end;
|
||||||
self.blobs_at
|
self.blobs_at
|
||||||
.append(start, meta)
|
.append(start, meta)
|
||||||
@@ -157,7 +163,7 @@ pub struct VectoredReadPlanner {
|
|||||||
// Arguments for previous blob passed into [`VectoredReadPlanner::handle`]
|
// Arguments for previous blob passed into [`VectoredReadPlanner::handle`]
|
||||||
prev: Option<(Key, Lsn, u64, BlobFlag)>,
|
prev: Option<(Key, Lsn, u64, BlobFlag)>,
|
||||||
|
|
||||||
max_read_size: usize,
|
max_read_size: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VectoredReadPlanner {
|
impl VectoredReadPlanner {
|
||||||
@@ -165,7 +171,20 @@ impl VectoredReadPlanner {
|
|||||||
Self {
|
Self {
|
||||||
blobs: BTreeMap::new(),
|
blobs: BTreeMap::new(),
|
||||||
prev: None,
|
prev: None,
|
||||||
max_read_size,
|
max_read_size: Some(max_read_size),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function should *only* be used if the caller has a way to control the limit. e.g., in [`StreamingVectoredReadPlanner`],
|
||||||
|
/// it uses the vectored read planner to avoid duplicated logic on handling blob start/end, while expecting the vectored
|
||||||
|
/// read planner to give a single read to a continuous range of bytes in the image layer. Therefore, it does not need the
|
||||||
|
/// code path to split reads into chunks of `max_read_size`, and controls the read size itself.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn new_caller_controlled_max_limit() -> Self {
|
||||||
|
Self {
|
||||||
|
blobs: BTreeMap::new(),
|
||||||
|
prev: None,
|
||||||
|
max_read_size: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,6 +373,87 @@ impl<'a> VectoredBlobReader<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read planner used in [`crate::tenant::storage_layer::image_layer::ImageLayerIterator`]. It provides a streaming API for
|
||||||
|
/// getting read blobs. It returns a batch when `handle` gets called and when the current key would exceed the read_size and
|
||||||
|
/// max_cnt constraints. Underlying it uses [`VectoredReadPlanner`].
|
||||||
|
#[cfg(test)]
|
||||||
|
pub struct StreamingVectoredReadPlanner {
|
||||||
|
planner: VectoredReadPlanner,
|
||||||
|
/// Max read size per batch
|
||||||
|
max_read_size: u64,
|
||||||
|
/// Max item count per batch
|
||||||
|
max_cnt: usize,
|
||||||
|
/// The first offset of this batch
|
||||||
|
this_batch_first_offset: Option<u64>,
|
||||||
|
/// Size of the current batch
|
||||||
|
cnt: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl StreamingVectoredReadPlanner {
|
||||||
|
pub fn new(max_read_size: u64, max_cnt: usize) -> Self {
|
||||||
|
assert!(max_cnt > 0);
|
||||||
|
assert!(max_read_size > 0);
|
||||||
|
Self {
|
||||||
|
// We want to have exactly one read syscall (plus several others for index lookup) for each `next_batch` call.
|
||||||
|
// Therefore, we enforce `self.max_read_size` by ourselves instead of using the VectoredReadPlanner's capability,
|
||||||
|
// to avoid splitting into two I/Os.
|
||||||
|
planner: VectoredReadPlanner::new_caller_controlled_max_limit(),
|
||||||
|
max_cnt,
|
||||||
|
max_read_size,
|
||||||
|
this_batch_first_offset: None,
|
||||||
|
cnt: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit(&mut self, this_batch_first_offset: u64) -> VectoredRead {
|
||||||
|
let planner = std::mem::replace(
|
||||||
|
&mut self.planner,
|
||||||
|
VectoredReadPlanner::new_caller_controlled_max_limit(),
|
||||||
|
);
|
||||||
|
self.this_batch_first_offset = Some(this_batch_first_offset);
|
||||||
|
self.cnt = 1;
|
||||||
|
let mut batch = planner.finish();
|
||||||
|
assert_eq!(batch.len(), 1, "should have exactly one read batch");
|
||||||
|
batch.pop().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle(
|
||||||
|
&mut self,
|
||||||
|
key: Key,
|
||||||
|
lsn: Lsn,
|
||||||
|
offset: u64,
|
||||||
|
flag: BlobFlag,
|
||||||
|
) -> Option<VectoredRead> {
|
||||||
|
if let Some(begin_offset) = self.this_batch_first_offset {
|
||||||
|
// Each batch will have at least one item b/c `self.this_batch_first_offset` is set
|
||||||
|
// after one item gets processed
|
||||||
|
if offset - begin_offset > self.max_read_size {
|
||||||
|
self.planner.handle_range_end(offset); // End the current batch with the offset
|
||||||
|
let batch = self.emit(offset); // Produce a batch
|
||||||
|
self.planner.handle(key, lsn, offset, flag); // Add this key to the next batch
|
||||||
|
return Some(batch);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.this_batch_first_offset = Some(offset)
|
||||||
|
}
|
||||||
|
if self.cnt >= self.max_cnt {
|
||||||
|
self.planner.handle_range_end(offset); // End the current batch with the offset
|
||||||
|
let batch = self.emit(offset); // Produce a batch
|
||||||
|
self.planner.handle(key, lsn, offset, flag); // Add this key to the next batch
|
||||||
|
return Some(batch);
|
||||||
|
}
|
||||||
|
self.planner.handle(key, lsn, offset, flag); // Add this key to the current batch
|
||||||
|
self.cnt += 1;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_range_end(&mut self, offset: u64) -> VectoredRead {
|
||||||
|
self.planner.handle_range_end(offset);
|
||||||
|
self.emit(offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
+150
-40
@@ -40,6 +40,7 @@ use std::time::Duration;
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
use utils::lsn::Lsn;
|
use utils::lsn::Lsn;
|
||||||
|
use utils::sync::gate::GateError;
|
||||||
use utils::sync::heavier_once_cell;
|
use utils::sync::heavier_once_cell;
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -53,10 +54,18 @@ pub struct PostgresRedoManager {
|
|||||||
tenant_shard_id: TenantShardId,
|
tenant_shard_id: TenantShardId,
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
last_redo_at: std::sync::Mutex<Option<Instant>>,
|
last_redo_at: std::sync::Mutex<Option<Instant>>,
|
||||||
/// The current [`process::WalRedoProcess`] that is used by new redo requests.
|
/// We use [`heavier_once_cell`] for
|
||||||
/// We use [`heavier_once_cell`] for coalescing the spawning, but the redo
|
///
|
||||||
/// requests don't use the [`heavier_once_cell::Guard`] to keep ahold of the
|
/// 1. coalescing the lazy spawning of walredo processes ([`ProcessOnceCell::Spawned`])
|
||||||
|
/// 2. prevent new processes from being spawned on [`Self::shutdown`] (=> [`ProcessOnceCell::ManagerShutDown`]).
|
||||||
|
///
|
||||||
|
/// # Spawning
|
||||||
|
///
|
||||||
|
/// Redo requests use the once cell to coalesce onto one call to [`process::WalRedoProcess::launch`].
|
||||||
|
///
|
||||||
|
/// Notably, requests don't use the [`heavier_once_cell::Guard`] to keep ahold of the
|
||||||
/// their process object; we use [`Arc::clone`] for that.
|
/// their process object; we use [`Arc::clone`] for that.
|
||||||
|
///
|
||||||
/// This is primarily because earlier implementations that didn't use [`heavier_once_cell`]
|
/// This is primarily because earlier implementations that didn't use [`heavier_once_cell`]
|
||||||
/// had that behavior; it's probably unnecessary.
|
/// had that behavior; it's probably unnecessary.
|
||||||
/// The only merit of it is that if one walredo process encounters an error,
|
/// The only merit of it is that if one walredo process encounters an error,
|
||||||
@@ -65,7 +74,63 @@ pub struct PostgresRedoManager {
|
|||||||
/// still be using the old redo process. But, those other tasks will most likely
|
/// still be using the old redo process. But, those other tasks will most likely
|
||||||
/// encounter an error as well, and errors are an unexpected condition anyway.
|
/// encounter an error as well, and errors are an unexpected condition anyway.
|
||||||
/// So, probably we could get rid of the `Arc` in the future.
|
/// So, probably we could get rid of the `Arc` in the future.
|
||||||
redo_process: heavier_once_cell::OnceCell<Arc<process::WalRedoProcess>>,
|
///
|
||||||
|
/// # Shutdown
|
||||||
|
///
|
||||||
|
/// See [`Self::launched_processes`].
|
||||||
|
redo_process: heavier_once_cell::OnceCell<ProcessOnceCell>,
|
||||||
|
|
||||||
|
/// Gate that is entered when launching a walredo process and held open
|
||||||
|
/// until the process has been `kill()`ed and `wait()`ed upon.
|
||||||
|
///
|
||||||
|
/// Manager shutdown waits for this gate to close after setting the
|
||||||
|
/// [`ProcessOnceCell::ManagerShutDown`] state in [`Self::redo_process`].
|
||||||
|
///
|
||||||
|
/// This type of usage is a bit unusual because gates usually keep track of
|
||||||
|
/// concurrent operations, e.g., every [`Self::request_redo`] that is inflight.
|
||||||
|
/// But we use it here to keep track of the _processes_ that we have launched,
|
||||||
|
/// which may outlive any individual redo request because
|
||||||
|
/// - we keep walredo process around until its quiesced to amortize spawn cost and
|
||||||
|
/// - the Arc may be held by multiple concurrent redo requests, so, just because
|
||||||
|
/// you replace the [`Self::redo_process`] cell's content doesn't mean the
|
||||||
|
/// process gets killed immediately.
|
||||||
|
///
|
||||||
|
/// We could simplify this by getting rid of the [`Arc`].
|
||||||
|
/// See the comment on [`Self::redo_process`] for more details.
|
||||||
|
launched_processes: utils::sync::gate::Gate,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See [`PostgresRedoManager::redo_process`].
|
||||||
|
enum ProcessOnceCell {
|
||||||
|
Spawned(Arc<Process>),
|
||||||
|
ManagerShutDown,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Process {
|
||||||
|
_launched_processes_guard: utils::sync::gate::GateGuard,
|
||||||
|
process: process::WalRedoProcess,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for Process {
|
||||||
|
type Target = process::WalRedoProcess;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.process
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("cancelled")]
|
||||||
|
Cancelled,
|
||||||
|
#[error(transparent)]
|
||||||
|
Other(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! bail {
|
||||||
|
($($arg:tt)*) => {
|
||||||
|
return Err($crate::walredo::Error::Other(::anyhow::anyhow!($($arg)*)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -88,9 +153,9 @@ impl PostgresRedoManager {
|
|||||||
base_img: Option<(Lsn, Bytes)>,
|
base_img: Option<(Lsn, Bytes)>,
|
||||||
records: Vec<(Lsn, NeonWalRecord)>,
|
records: Vec<(Lsn, NeonWalRecord)>,
|
||||||
pg_version: u32,
|
pg_version: u32,
|
||||||
) -> anyhow::Result<Bytes> {
|
) -> Result<Bytes, Error> {
|
||||||
if records.is_empty() {
|
if records.is_empty() {
|
||||||
anyhow::bail!("invalid WAL redo request with no records");
|
bail!("invalid WAL redo request with no records");
|
||||||
}
|
}
|
||||||
|
|
||||||
let base_img_lsn = base_img.as_ref().map(|p| p.0).unwrap_or(Lsn::INVALID);
|
let base_img_lsn = base_img.as_ref().map(|p| p.0).unwrap_or(Lsn::INVALID);
|
||||||
@@ -148,10 +213,10 @@ impl PostgresRedoManager {
|
|||||||
chrono::Utc::now().checked_sub_signed(chrono::Duration::from_std(age).ok()?)
|
chrono::Utc::now().checked_sub_signed(chrono::Duration::from_std(age).ok()?)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
process: self
|
process: self.redo_process.get().and_then(|p| match &*p {
|
||||||
.redo_process
|
ProcessOnceCell::Spawned(p) => Some(WalRedoManagerProcessStatus { pid: p.id() }),
|
||||||
.get()
|
ProcessOnceCell::ManagerShutDown => None,
|
||||||
.map(|p| WalRedoManagerProcessStatus { pid: p.id() }),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,9 +235,39 @@ impl PostgresRedoManager {
|
|||||||
conf,
|
conf,
|
||||||
last_redo_at: std::sync::Mutex::default(),
|
last_redo_at: std::sync::Mutex::default(),
|
||||||
redo_process: heavier_once_cell::OnceCell::default(),
|
redo_process: heavier_once_cell::OnceCell::default(),
|
||||||
|
launched_processes: utils::sync::gate::Gate::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shut down the WAL redo manager.
|
||||||
|
///
|
||||||
|
/// After this future completes
|
||||||
|
/// - no redo process is running
|
||||||
|
/// - no new redo process will be spawned
|
||||||
|
/// - redo requests that need walredo process will fail with [`Error::Cancelled`]
|
||||||
|
/// - [`apply_neon`]-only redo requests may still work, but this may change in the future
|
||||||
|
///
|
||||||
|
/// # Cancel-Safety
|
||||||
|
///
|
||||||
|
/// This method is cancellation-safe.
|
||||||
|
pub async fn shutdown(&self) {
|
||||||
|
// prevent new processes from being spawned
|
||||||
|
let permit = match self.redo_process.get_or_init_detached().await {
|
||||||
|
Ok(guard) => {
|
||||||
|
let (proc, permit) = guard.take_and_deinit();
|
||||||
|
drop(proc); // this just drops the Arc, its refcount may not be zero yet
|
||||||
|
permit
|
||||||
|
}
|
||||||
|
Err(permit) => permit,
|
||||||
|
};
|
||||||
|
self.redo_process
|
||||||
|
.set(ProcessOnceCell::ManagerShutDown, permit);
|
||||||
|
// wait for ongoing requests to drain and the refcounts of all Arc<WalRedoProcess> that
|
||||||
|
// we ever launched to drop to zero, which when it happens synchronously kill()s & wait()s
|
||||||
|
// for the underlying process.
|
||||||
|
self.launched_processes.close().await;
|
||||||
|
}
|
||||||
|
|
||||||
/// This type doesn't have its own background task to check for idleness: we
|
/// This type doesn't have its own background task to check for idleness: we
|
||||||
/// rely on our owner calling this function periodically in its own housekeeping
|
/// rely on our owner calling this function periodically in its own housekeeping
|
||||||
/// loops.
|
/// loops.
|
||||||
@@ -203,38 +298,48 @@ impl PostgresRedoManager {
|
|||||||
records: &[(Lsn, NeonWalRecord)],
|
records: &[(Lsn, NeonWalRecord)],
|
||||||
wal_redo_timeout: Duration,
|
wal_redo_timeout: Duration,
|
||||||
pg_version: u32,
|
pg_version: u32,
|
||||||
) -> anyhow::Result<Bytes> {
|
) -> Result<Bytes, Error> {
|
||||||
*(self.last_redo_at.lock().unwrap()) = Some(Instant::now());
|
*(self.last_redo_at.lock().unwrap()) = Some(Instant::now());
|
||||||
|
|
||||||
let (rel, blknum) = key.to_rel_block().context("invalid record")?;
|
let (rel, blknum) = key.to_rel_block().context("invalid record")?;
|
||||||
const MAX_RETRY_ATTEMPTS: u32 = 1;
|
const MAX_RETRY_ATTEMPTS: u32 = 1;
|
||||||
let mut n_attempts = 0u32;
|
let mut n_attempts = 0u32;
|
||||||
loop {
|
loop {
|
||||||
let proc: Arc<process::WalRedoProcess> =
|
let proc: Arc<Process> = match self.redo_process.get_or_init_detached().await {
|
||||||
match self.redo_process.get_or_init_detached().await {
|
Ok(guard) => match &*guard {
|
||||||
Ok(guard) => Arc::clone(&guard),
|
ProcessOnceCell::Spawned(proc) => Arc::clone(proc),
|
||||||
Err(permit) => {
|
ProcessOnceCell::ManagerShutDown => {
|
||||||
// don't hold poison_guard, the launch code can bail
|
return Err(Error::Cancelled);
|
||||||
let start = Instant::now();
|
}
|
||||||
let proc = Arc::new(
|
},
|
||||||
process::WalRedoProcess::launch(
|
Err(permit) => {
|
||||||
|
let start = Instant::now();
|
||||||
|
let proc = Arc::new(Process {
|
||||||
|
_launched_processes_guard: match self.launched_processes.enter() {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(GateError::GateClosed) => unreachable!(
|
||||||
|
"shutdown sets the once cell to `ManagerShutDown` state before closing the gate"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
process: process::WalRedoProcess::launch(
|
||||||
self.conf,
|
self.conf,
|
||||||
self.tenant_shard_id,
|
self.tenant_shard_id,
|
||||||
pg_version,
|
pg_version,
|
||||||
)
|
)
|
||||||
.context("launch walredo process")?,
|
.context("launch walredo process")?,
|
||||||
);
|
});
|
||||||
let duration = start.elapsed();
|
let duration = start.elapsed();
|
||||||
WAL_REDO_PROCESS_LAUNCH_DURATION_HISTOGRAM.observe(duration.as_secs_f64());
|
WAL_REDO_PROCESS_LAUNCH_DURATION_HISTOGRAM.observe(duration.as_secs_f64());
|
||||||
info!(
|
info!(
|
||||||
duration_ms = duration.as_millis(),
|
duration_ms = duration.as_millis(),
|
||||||
pid = proc.id(),
|
pid = proc.id(),
|
||||||
"launched walredo process"
|
"launched walredo process"
|
||||||
);
|
);
|
||||||
self.redo_process.set(Arc::clone(&proc), permit);
|
self.redo_process
|
||||||
proc
|
.set(ProcessOnceCell::Spawned(Arc::clone(&proc)), permit);
|
||||||
}
|
proc
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let started_at = std::time::Instant::now();
|
let started_at = std::time::Instant::now();
|
||||||
|
|
||||||
@@ -299,12 +404,17 @@ impl PostgresRedoManager {
|
|||||||
match self.redo_process.get() {
|
match self.redo_process.get() {
|
||||||
None => (),
|
None => (),
|
||||||
Some(guard) => {
|
Some(guard) => {
|
||||||
if Arc::ptr_eq(&proc, &*guard) {
|
match &*guard {
|
||||||
// We're the first to observe an error from `proc`, it's our job to take it out of rotation.
|
ProcessOnceCell::ManagerShutDown => {}
|
||||||
guard.take_and_deinit();
|
ProcessOnceCell::Spawned(guard_proc) => {
|
||||||
} else {
|
if Arc::ptr_eq(&proc, guard_proc) {
|
||||||
// Another task already spawned another redo process (further up in this method)
|
// We're the first to observe an error from `proc`, it's our job to take it out of rotation.
|
||||||
// and put it into `redo_process`. Do nothing, our view of the world is behind.
|
guard.take_and_deinit();
|
||||||
|
} else {
|
||||||
|
// Another task already spawned another redo process (further up in this method)
|
||||||
|
// and put it into `redo_process`. Do nothing, our view of the world is behind.
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,7 +425,7 @@ impl PostgresRedoManager {
|
|||||||
}
|
}
|
||||||
n_attempts += 1;
|
n_attempts += 1;
|
||||||
if n_attempts > MAX_RETRY_ATTEMPTS || result.is_ok() {
|
if n_attempts > MAX_RETRY_ATTEMPTS || result.is_ok() {
|
||||||
return result;
|
return result.map_err(Error::Other);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,7 +439,7 @@ impl PostgresRedoManager {
|
|||||||
lsn: Lsn,
|
lsn: Lsn,
|
||||||
base_img: Option<Bytes>,
|
base_img: Option<Bytes>,
|
||||||
records: &[(Lsn, NeonWalRecord)],
|
records: &[(Lsn, NeonWalRecord)],
|
||||||
) -> anyhow::Result<Bytes> {
|
) -> Result<Bytes, Error> {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
let mut page = BytesMut::new();
|
let mut page = BytesMut::new();
|
||||||
@@ -338,7 +448,7 @@ impl PostgresRedoManager {
|
|||||||
page.extend_from_slice(&fpi[..]);
|
page.extend_from_slice(&fpi[..]);
|
||||||
} else {
|
} else {
|
||||||
// All the current WAL record types that we can handle require a base image.
|
// All the current WAL record types that we can handle require a base image.
|
||||||
anyhow::bail!("invalid neon WAL redo request with no base image");
|
bail!("invalid neon WAL redo request with no base image");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply all the WAL records in the batch
|
// Apply all the WAL records in the batch
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ PG_MODULE_MAGIC;
|
|||||||
void _PG_init(void);
|
void _PG_init(void);
|
||||||
|
|
||||||
static int logical_replication_max_snap_files = 300;
|
static int logical_replication_max_snap_files = 300;
|
||||||
bool primary_is_running = false;
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
InitLogicalReplicationMonitor(void)
|
InitLogicalReplicationMonitor(void)
|
||||||
@@ -289,15 +288,6 @@ _PG_init(void)
|
|||||||
|
|
||||||
pg_init_extension_server();
|
pg_init_extension_server();
|
||||||
|
|
||||||
DefineCustomBoolVariable(
|
|
||||||
"neon.primary_is_running",
|
|
||||||
"true if the primary was running at replica startup. false otherwise",
|
|
||||||
NULL,
|
|
||||||
&primary_is_running,
|
|
||||||
false,
|
|
||||||
PGC_POSTMASTER,
|
|
||||||
0,
|
|
||||||
NULL, NULL, NULL);
|
|
||||||
/*
|
/*
|
||||||
* Important: This must happen after other parts of the extension are
|
* Important: This must happen after other parts of the extension are
|
||||||
* loaded, otherwise any settings to GUCs that were set before the
|
* loaded, otherwise any settings to GUCs that were set before the
|
||||||
|
|||||||
@@ -1447,7 +1447,7 @@ RecvAppendResponses(Safekeeper *sk)
|
|||||||
* core as this is kinda expected scenario.
|
* core as this is kinda expected scenario.
|
||||||
*/
|
*/
|
||||||
disable_core_dump();
|
disable_core_dump();
|
||||||
wp_log(PANIC, "WAL acceptor %s:%s with term " INT64_FORMAT " rejected our request, our term " INT64_FORMAT "",
|
wp_log(PANIC, "WAL acceptor %s:%s with term " INT64_FORMAT " rejected our request, our term " INT64_FORMAT ", meaning another compute is running at the same time, and it conflicts with us",
|
||||||
sk->host, sk->port,
|
sk->host, sk->port,
|
||||||
sk->appendResponse.term, wp->propTerm);
|
sk->appendResponse.term, wp->propTerm);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ char *wal_acceptors_list = "";
|
|||||||
int wal_acceptor_reconnect_timeout = 1000;
|
int wal_acceptor_reconnect_timeout = 1000;
|
||||||
int wal_acceptor_connection_timeout = 10000;
|
int wal_acceptor_connection_timeout = 10000;
|
||||||
|
|
||||||
|
/* Set to true in the walproposer bgw. */
|
||||||
|
static bool am_walproposer;
|
||||||
static WalproposerShmemState *walprop_shared;
|
static WalproposerShmemState *walprop_shared;
|
||||||
static WalProposerConfig walprop_config;
|
static WalProposerConfig walprop_config;
|
||||||
static XLogRecPtr sentPtr = InvalidXLogRecPtr;
|
static XLogRecPtr sentPtr = InvalidXLogRecPtr;
|
||||||
@@ -76,6 +78,7 @@ static HotStandbyFeedback agg_hs_feedback;
|
|||||||
|
|
||||||
static void nwp_shmem_startup_hook(void);
|
static void nwp_shmem_startup_hook(void);
|
||||||
static void nwp_register_gucs(void);
|
static void nwp_register_gucs(void);
|
||||||
|
static void assign_neon_safekeepers(const char *newval, void *extra);
|
||||||
static void nwp_prepare_shmem(void);
|
static void nwp_prepare_shmem(void);
|
||||||
static uint64 backpressure_lag_impl(void);
|
static uint64 backpressure_lag_impl(void);
|
||||||
static bool backpressure_throttling_impl(void);
|
static bool backpressure_throttling_impl(void);
|
||||||
@@ -111,7 +114,8 @@ init_walprop_config(bool syncSafekeepers)
|
|||||||
{
|
{
|
||||||
walprop_config.neon_tenant = neon_tenant;
|
walprop_config.neon_tenant = neon_tenant;
|
||||||
walprop_config.neon_timeline = neon_timeline;
|
walprop_config.neon_timeline = neon_timeline;
|
||||||
walprop_config.safekeepers_list = wal_acceptors_list;
|
/* WalProposerCreate scribbles directly on it, so pstrdup */
|
||||||
|
walprop_config.safekeepers_list = pstrdup(wal_acceptors_list);
|
||||||
walprop_config.safekeeper_reconnect_timeout = wal_acceptor_reconnect_timeout;
|
walprop_config.safekeeper_reconnect_timeout = wal_acceptor_reconnect_timeout;
|
||||||
walprop_config.safekeeper_connection_timeout = wal_acceptor_connection_timeout;
|
walprop_config.safekeeper_connection_timeout = wal_acceptor_connection_timeout;
|
||||||
walprop_config.wal_segment_size = wal_segment_size;
|
walprop_config.wal_segment_size = wal_segment_size;
|
||||||
@@ -151,6 +155,7 @@ WalProposerMain(Datum main_arg)
|
|||||||
|
|
||||||
init_walprop_config(false);
|
init_walprop_config(false);
|
||||||
walprop_pg_init_bgworker();
|
walprop_pg_init_bgworker();
|
||||||
|
am_walproposer = true;
|
||||||
walprop_pg_load_libpqwalreceiver();
|
walprop_pg_load_libpqwalreceiver();
|
||||||
|
|
||||||
wp = WalProposerCreate(&walprop_config, walprop_pg);
|
wp = WalProposerCreate(&walprop_config, walprop_pg);
|
||||||
@@ -189,10 +194,10 @@ nwp_register_gucs(void)
|
|||||||
NULL, /* long_desc */
|
NULL, /* long_desc */
|
||||||
&wal_acceptors_list, /* valueAddr */
|
&wal_acceptors_list, /* valueAddr */
|
||||||
"", /* bootValue */
|
"", /* bootValue */
|
||||||
PGC_POSTMASTER,
|
PGC_SIGHUP,
|
||||||
GUC_LIST_INPUT, /* extensions can't use*
|
GUC_LIST_INPUT, /* extensions can't use*
|
||||||
* GUC_LIST_QUOTE */
|
* GUC_LIST_QUOTE */
|
||||||
NULL, NULL, NULL);
|
NULL, assign_neon_safekeepers, NULL);
|
||||||
|
|
||||||
DefineCustomIntVariable(
|
DefineCustomIntVariable(
|
||||||
"neon.safekeeper_reconnect_timeout",
|
"neon.safekeeper_reconnect_timeout",
|
||||||
@@ -215,6 +220,33 @@ nwp_register_gucs(void)
|
|||||||
NULL, NULL, NULL);
|
NULL, NULL, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GUC assign_hook for neon.safekeepers. Restarts walproposer through FATAL if
|
||||||
|
* the list changed.
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
assign_neon_safekeepers(const char *newval, void *extra)
|
||||||
|
{
|
||||||
|
if (!am_walproposer)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!newval) {
|
||||||
|
/* should never happen */
|
||||||
|
wpg_log(FATAL, "neon.safekeepers is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: restarting through FATAL is stupid and introduces 1s delay before
|
||||||
|
* next bgw start. We should refactor walproposer to allow graceful exit and
|
||||||
|
* thus remove this delay.
|
||||||
|
*/
|
||||||
|
if (strcmp(wal_acceptors_list, newval) != 0)
|
||||||
|
{
|
||||||
|
wpg_log(FATAL, "restarting walproposer to change safekeeper list from %s to %s",
|
||||||
|
wal_acceptors_list, newval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Check if we need to suspend inserts because of lagging replication. */
|
/* Check if we need to suspend inserts because of lagging replication. */
|
||||||
static uint64
|
static uint64
|
||||||
backpressure_lag_impl(void)
|
backpressure_lag_impl(void)
|
||||||
@@ -363,7 +395,7 @@ walprop_register_bgworker(void)
|
|||||||
snprintf(bgw.bgw_function_name, BGW_MAXLEN, "WalProposerMain");
|
snprintf(bgw.bgw_function_name, BGW_MAXLEN, "WalProposerMain");
|
||||||
snprintf(bgw.bgw_name, BGW_MAXLEN, "WAL proposer");
|
snprintf(bgw.bgw_name, BGW_MAXLEN, "WAL proposer");
|
||||||
snprintf(bgw.bgw_type, BGW_MAXLEN, "WAL proposer");
|
snprintf(bgw.bgw_type, BGW_MAXLEN, "WAL proposer");
|
||||||
bgw.bgw_restart_time = 5;
|
bgw.bgw_restart_time = 1;
|
||||||
bgw.bgw_notify_pid = 0;
|
bgw.bgw_notify_pid = 0;
|
||||||
bgw.bgw_main_arg = (Datum) 0;
|
bgw.bgw_main_arg = (Datum) 0;
|
||||||
|
|
||||||
@@ -1639,6 +1671,18 @@ walprop_pg_wait_event_set(WalProposer *wp, long timeout, Safekeeper **sk, uint32
|
|||||||
late_cv_trigger = ConditionVariableCancelSleep();
|
late_cv_trigger = ConditionVariableCancelSleep();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Process config if requested. This restarts walproposer if safekeepers
|
||||||
|
* list changed. Don't do that for sync-safekeepers because quite probably
|
||||||
|
* it (re-reading config) won't work without some effort, and
|
||||||
|
* sync-safekeepers should be quick to finish anyway.
|
||||||
|
*/
|
||||||
|
if (!wp->config->syncSafekeepers && ConfigReloadPending)
|
||||||
|
{
|
||||||
|
ConfigReloadPending = false;
|
||||||
|
ProcessConfigFile(PGC_SIGHUP);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If wait is terminated by latch set (walsenders' latch is set on each
|
* If wait is terminated by latch set (walsenders' latch is set on each
|
||||||
* wal flush). (no need for pm death check due to WL_EXIT_ON_PM_DEATH)
|
* wal flush). (no need for pm death check due to WL_EXIT_ON_PM_DEATH)
|
||||||
|
|||||||
@@ -168,16 +168,15 @@ close_range_syscall(unsigned int start_fd, unsigned int count, unsigned int flag
|
|||||||
static void
|
static void
|
||||||
enter_seccomp_mode(void)
|
enter_seccomp_mode(void)
|
||||||
{
|
{
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The pageserver process relies on us to close all the file descriptors
|
* The pageserver process relies on us to close all the file descriptors
|
||||||
* it potentially leaked to us, _before_ we start processing potentially dangerous
|
* it potentially leaked to us, _before_ we start processing potentially dangerous
|
||||||
* wal records. See the comment in the Rust code that launches this process.
|
* wal records. See the comment in the Rust code that launches this process.
|
||||||
*/
|
*/
|
||||||
int err;
|
if (close_range_syscall(3, ~0U, 0) != 0)
|
||||||
if (err = close_range_syscall(3, ~0U, 0)) {
|
ereport(FATAL,
|
||||||
ereport(FATAL, (errcode(ERRCODE_SYSTEM_ERROR), errmsg("seccomp: could not close files >= fd 3")));
|
(errcode(ERRCODE_SYSTEM_ERROR),
|
||||||
}
|
errmsg("seccomp: could not close files >= fd 3")));
|
||||||
|
|
||||||
PgSeccompRule syscalls[] =
|
PgSeccompRule syscalls[] =
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ pub struct ComputeUserInfo {
|
|||||||
|
|
||||||
impl ComputeUserInfo {
|
impl ComputeUserInfo {
|
||||||
pub fn endpoint_cache_key(&self) -> EndpointCacheKey {
|
pub fn endpoint_cache_key(&self) -> EndpointCacheKey {
|
||||||
self.options.get_cache_key(&self.endpoint)
|
self.options.get_cache_key((&self.endpoint).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -241,6 +241,8 @@ fn project_name_valid(name: &str) -> bool {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::intern::EndpointIdInt;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use ComputeUserInfoParseError::*;
|
use ComputeUserInfoParseError::*;
|
||||||
@@ -284,7 +286,6 @@ mod tests {
|
|||||||
ComputeUserInfoMaybeEndpoint::parse(&mut ctx, &options, sni, common_names.as_ref())?;
|
ComputeUserInfoMaybeEndpoint::parse(&mut ctx, &options, sni, common_names.as_ref())?;
|
||||||
assert_eq!(user_info.user, "john_doe");
|
assert_eq!(user_info.user, "john_doe");
|
||||||
assert_eq!(user_info.endpoint_id.as_deref(), Some("foo"));
|
assert_eq!(user_info.endpoint_id.as_deref(), Some("foo"));
|
||||||
assert_eq!(user_info.options.get_cache_key("foo"), "foo");
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -442,8 +443,9 @@ mod tests {
|
|||||||
let user_info =
|
let user_info =
|
||||||
ComputeUserInfoMaybeEndpoint::parse(&mut ctx, &options, sni, common_names.as_ref())?;
|
ComputeUserInfoMaybeEndpoint::parse(&mut ctx, &options, sni, common_names.as_ref())?;
|
||||||
assert_eq!(user_info.endpoint_id.as_deref(), Some("project"));
|
assert_eq!(user_info.endpoint_id.as_deref(), Some("project"));
|
||||||
|
let project = EndpointIdInt::from(EndpointId::from("project"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
user_info.options.get_cache_key("project"),
|
user_info.options.get_cache_key(project).to_string(),
|
||||||
"project endpoint_type:read_write lsn:0/2"
|
"project endpoint_type:read_write lsn:0/2"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use itertools::Itertools;
|
|||||||
use proxy::config::TlsServerEndPoint;
|
use proxy::config::TlsServerEndPoint;
|
||||||
use proxy::context::RequestMonitoring;
|
use proxy::context::RequestMonitoring;
|
||||||
use proxy::metrics::{Metrics, ThreadPoolMetrics};
|
use proxy::metrics::{Metrics, ThreadPoolMetrics};
|
||||||
use proxy::proxy::{copy_bidirectional_client_compute, run_until_cancelled};
|
use proxy::proxy::{copy_bidirectional_client_compute, run_until_cancelled, ErrorSource};
|
||||||
use rustls::pki_types::PrivateKeyDer;
|
use rustls::pki_types::PrivateKeyDer;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
@@ -286,7 +286,10 @@ async fn handle_client(
|
|||||||
|
|
||||||
// Starting from here we only proxy the client's traffic.
|
// Starting from here we only proxy the client's traffic.
|
||||||
info!("performing the proxy pass...");
|
info!("performing the proxy pass...");
|
||||||
let _ = copy_bidirectional_client_compute(&mut tls_stream, &mut client).await?;
|
|
||||||
|
|
||||||
Ok(())
|
match copy_bidirectional_client_compute(&mut tls_stream, &mut client).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(ErrorSource::Client(err)) => Err(err).context("client"),
|
||||||
|
Err(ErrorSource::Compute(err)) => Err(err).context("compute"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+9
@@ -43,6 +43,15 @@ impl<C: Cache, V> Cached<C, V> {
|
|||||||
Self { token: None, value }
|
Self { token: None, value }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Place any entry into this wrapper; invalidation will be a no-op.
|
||||||
|
pub fn map<U>(self, f: impl FnOnce(V) -> U) -> Cached<C, U> {
|
||||||
|
let token = self.token;
|
||||||
|
Cached {
|
||||||
|
token,
|
||||||
|
value: f(self.value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn take_value(self) -> (Cached<C, ()>, V) {
|
pub fn take_value(self) -> (Cached<C, ()>, V) {
|
||||||
(
|
(
|
||||||
Cached {
|
Cached {
|
||||||
|
|||||||
+69
-62
@@ -93,7 +93,7 @@ pub type ScramKeys = tokio_postgres::config::ScramKeys<32>;
|
|||||||
/// Eventually, `tokio_postgres` will be replaced with something better.
|
/// Eventually, `tokio_postgres` will be replaced with something better.
|
||||||
/// Newtype allows us to implement methods on top of it.
|
/// Newtype allows us to implement methods on top of it.
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct ConnCfg(Box<tokio_postgres::Config>);
|
pub struct ConnCfg(tokio_postgres::Config);
|
||||||
|
|
||||||
/// Creation and initialization routines.
|
/// Creation and initialization routines.
|
||||||
impl ConnCfg {
|
impl ConnCfg {
|
||||||
@@ -103,12 +103,8 @@ impl ConnCfg {
|
|||||||
|
|
||||||
/// Reuse password or auth keys from the other config.
|
/// Reuse password or auth keys from the other config.
|
||||||
pub fn reuse_password(&mut self, other: Self) {
|
pub fn reuse_password(&mut self, other: Self) {
|
||||||
if let Some(password) = other.get_password() {
|
if let Some(password) = other.get_auth() {
|
||||||
self.password(password);
|
self.auth(password);
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(keys) = other.get_auth_keys() {
|
|
||||||
self.auth_keys(keys);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,48 +120,64 @@ impl ConnCfg {
|
|||||||
|
|
||||||
/// Apply startup message params to the connection config.
|
/// Apply startup message params to the connection config.
|
||||||
pub fn set_startup_params(&mut self, params: &StartupMessageParams) {
|
pub fn set_startup_params(&mut self, params: &StartupMessageParams) {
|
||||||
// Only set `user` if it's not present in the config.
|
let mut client_encoding = false;
|
||||||
// Link auth flow takes username from the console's response.
|
for (k, v) in params.iter() {
|
||||||
if let (None, Some(user)) = (self.get_user(), params.get("user")) {
|
match k {
|
||||||
self.user(user);
|
"user" => {
|
||||||
}
|
// Only set `user` if it's not present in the config.
|
||||||
|
// Link auth flow takes username from the console's response.
|
||||||
// Only set `dbname` if it's not present in the config.
|
if self.get_user().is_none() {
|
||||||
// Link auth flow takes dbname from the console's response.
|
self.user(v);
|
||||||
if let (None, Some(dbname)) = (self.get_dbname(), params.get("database")) {
|
}
|
||||||
self.dbname(dbname);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't add `options` if they were only used for specifying a project.
|
|
||||||
// Connection pools don't support `options`, because they affect backend startup.
|
|
||||||
if let Some(options) = filtered_options(params) {
|
|
||||||
self.options(&options);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(app_name) = params.get("application_name") {
|
|
||||||
self.application_name(app_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This is especially ugly...
|
|
||||||
if let Some(replication) = params.get("replication") {
|
|
||||||
use tokio_postgres::config::ReplicationMode;
|
|
||||||
match replication {
|
|
||||||
"true" | "on" | "yes" | "1" => {
|
|
||||||
self.replication_mode(ReplicationMode::Physical);
|
|
||||||
}
|
}
|
||||||
"database" => {
|
"database" => {
|
||||||
self.replication_mode(ReplicationMode::Logical);
|
// Only set `dbname` if it's not present in the config.
|
||||||
|
// Link auth flow takes dbname from the console's response.
|
||||||
|
if self.get_dbname().is_none() {
|
||||||
|
self.dbname(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"options" => {
|
||||||
|
// Don't add `options` if they were only used for specifying a project.
|
||||||
|
// Connection pools don't support `options`, because they affect backend startup.
|
||||||
|
if let Some(options) = filtered_options(v) {
|
||||||
|
self.options(&options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the special ones in tokio-postgres that we don't want being set by the user
|
||||||
|
"dbname" => {}
|
||||||
|
"password" => {}
|
||||||
|
"sslmode" => {}
|
||||||
|
"host" => {}
|
||||||
|
"port" => {}
|
||||||
|
"connect_timeout" => {}
|
||||||
|
"keepalives" => {}
|
||||||
|
"keepalives_idle" => {}
|
||||||
|
"keepalives_interval" => {}
|
||||||
|
"keepalives_retries" => {}
|
||||||
|
"target_session_attrs" => {}
|
||||||
|
"channel_binding" => {}
|
||||||
|
"max_backend_message_size" => {}
|
||||||
|
|
||||||
|
"client_encoding" => {
|
||||||
|
client_encoding = true;
|
||||||
|
// only error should be from bad null bytes,
|
||||||
|
// but we've already checked for those.
|
||||||
|
_ = self.param("client_encoding", v);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
// only error should be from bad null bytes,
|
||||||
|
// but we've already checked for those.
|
||||||
|
_ = self.param(k, v);
|
||||||
}
|
}
|
||||||
_other => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !client_encoding {
|
||||||
// TODO: extend the list of the forwarded startup parameters.
|
// for compatibility since we removed it from tokio-postgres
|
||||||
// Currently, tokio-postgres doesn't allow us to pass
|
self.param("client_encoding", "UTF8").unwrap();
|
||||||
// arbitrary parameters, but the ones above are a good start.
|
}
|
||||||
//
|
|
||||||
// This and the reverse params problem can be better addressed
|
|
||||||
// in a bespoke connection machinery (a new library for that sake).
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,10 +350,9 @@ impl ConnCfg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve `options` from a startup message, dropping all proxy-secific flags.
|
/// Retrieve `options` from a startup message, dropping all proxy-secific flags.
|
||||||
fn filtered_options(params: &StartupMessageParams) -> Option<String> {
|
fn filtered_options(options: &str) -> Option<String> {
|
||||||
#[allow(unstable_name_collisions)]
|
#[allow(unstable_name_collisions)]
|
||||||
let options: String = params
|
let options: String = StartupMessageParams::parse_options_raw(options)
|
||||||
.options_raw()?
|
|
||||||
.filter(|opt| parse_endpoint_param(opt).is_none() && neon_option(opt).is_none())
|
.filter(|opt| parse_endpoint_param(opt).is_none() && neon_option(opt).is_none())
|
||||||
.intersperse(" ") // TODO: use impl from std once it's stabilized
|
.intersperse(" ") // TODO: use impl from std once it's stabilized
|
||||||
.collect();
|
.collect();
|
||||||
@@ -413,27 +424,23 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_filtered_options() {
|
fn test_filtered_options() {
|
||||||
// Empty options is unlikely to be useful anyway.
|
// Empty options is unlikely to be useful anyway.
|
||||||
let params = StartupMessageParams::new([("options", "")]);
|
assert_eq!(filtered_options(""), None);
|
||||||
assert_eq!(filtered_options(¶ms), None);
|
|
||||||
|
|
||||||
// It's likely that clients will only use options to specify endpoint/project.
|
// It's likely that clients will only use options to specify endpoint/project.
|
||||||
let params = StartupMessageParams::new([("options", "project=foo")]);
|
let params = "project=foo";
|
||||||
assert_eq!(filtered_options(¶ms), None);
|
assert_eq!(filtered_options(params), None);
|
||||||
|
|
||||||
// Same, because unescaped whitespaces are no-op.
|
// Same, because unescaped whitespaces are no-op.
|
||||||
let params = StartupMessageParams::new([("options", " project=foo ")]);
|
let params = " project=foo ";
|
||||||
assert_eq!(filtered_options(¶ms).as_deref(), None);
|
assert_eq!(filtered_options(params), None);
|
||||||
|
|
||||||
let params = StartupMessageParams::new([("options", r"\ project=foo \ ")]);
|
let params = r"\ project=foo \ ";
|
||||||
assert_eq!(filtered_options(¶ms).as_deref(), Some(r"\ \ "));
|
assert_eq!(filtered_options(params).as_deref(), Some(r"\ \ "));
|
||||||
|
|
||||||
let params = StartupMessageParams::new([("options", "project = foo")]);
|
let params = "project = foo";
|
||||||
assert_eq!(filtered_options(¶ms).as_deref(), Some("project = foo"));
|
assert_eq!(filtered_options(params).as_deref(), Some("project = foo"));
|
||||||
|
|
||||||
let params = StartupMessageParams::new([(
|
let params = "project = foo neon_endpoint_type:read_write neon_lsn:0/2";
|
||||||
"options",
|
assert_eq!(filtered_options(params).as_deref(), Some("project = foo"));
|
||||||
"project = foo neon_endpoint_type:read_write neon_lsn:0/2",
|
|
||||||
)]);
|
|
||||||
assert_eq!(filtered_options(¶ms).as_deref(), Some("project = foo"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::fmt::{self, Display};
|
|||||||
use crate::auth::IpPattern;
|
use crate::auth::IpPattern;
|
||||||
|
|
||||||
use crate::intern::{BranchIdInt, EndpointIdInt, ProjectIdInt};
|
use crate::intern::{BranchIdInt, EndpointIdInt, ProjectIdInt};
|
||||||
use crate::proxy::retry::ShouldRetry;
|
use crate::proxy::retry::CouldRetry;
|
||||||
|
|
||||||
/// Generic error response with human-readable description.
|
/// Generic error response with human-readable description.
|
||||||
/// Note that we can't always present it to user as is.
|
/// Note that we can't always present it to user as is.
|
||||||
@@ -64,45 +64,47 @@ impl Display for ConsoleError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShouldRetry for ConsoleError {
|
impl CouldRetry for ConsoleError {
|
||||||
fn could_retry(&self) -> bool {
|
fn could_retry(&self) -> bool {
|
||||||
if self.status.is_none() || self.status.as_ref().unwrap().details.retry_info.is_none() {
|
// If the error message does not have a status,
|
||||||
// retry some temporary failures because the compute was in a bad state
|
// the error is unknown and probably should not retry automatically
|
||||||
// (bad request can be returned when the endpoint was in transition)
|
let Some(status) = &self.status else {
|
||||||
return match &self {
|
return false;
|
||||||
ConsoleError {
|
};
|
||||||
http_status_code: http::StatusCode::BAD_REQUEST,
|
|
||||||
..
|
// retry if the retry info is set.
|
||||||
} => true,
|
if status.details.retry_info.is_some() {
|
||||||
// don't retry when quotas are exceeded
|
return true;
|
||||||
ConsoleError {
|
|
||||||
http_status_code: http::StatusCode::UNPROCESSABLE_ENTITY,
|
|
||||||
ref error,
|
|
||||||
..
|
|
||||||
} => !error.contains("compute time quota of non-primary branches is exceeded"),
|
|
||||||
// locked can be returned when the endpoint was in transition
|
|
||||||
// or when quotas are exceeded. don't retry when quotas are exceeded
|
|
||||||
ConsoleError {
|
|
||||||
http_status_code: http::StatusCode::LOCKED,
|
|
||||||
ref error,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
!error.contains("quota exceeded")
|
|
||||||
&& !error.contains("the limit for current plan reached")
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// retry if the response has a retry delay
|
// if no retry info set, attempt to use the error code to guess the retry state.
|
||||||
if let Some(retry_info) = self
|
let reason = status
|
||||||
.status
|
.details
|
||||||
.as_ref()
|
.error_info
|
||||||
.and_then(|s| s.details.retry_info.as_ref())
|
.map_or(Reason::Unknown, |e| e.reason);
|
||||||
{
|
match reason {
|
||||||
retry_info.retry_delay_ms > 0
|
// not a transitive error
|
||||||
} else {
|
Reason::RoleProtected => false,
|
||||||
false
|
// on retry, it will still not be found
|
||||||
|
Reason::ResourceNotFound
|
||||||
|
| Reason::ProjectNotFound
|
||||||
|
| Reason::EndpointNotFound
|
||||||
|
| Reason::BranchNotFound => false,
|
||||||
|
// we were asked to go away
|
||||||
|
Reason::RateLimitExceeded
|
||||||
|
| Reason::NonDefaultBranchComputeTimeExceeded
|
||||||
|
| Reason::ActiveTimeQuotaExceeded
|
||||||
|
| Reason::ComputeTimeQuotaExceeded
|
||||||
|
| Reason::WrittenDataQuotaExceeded
|
||||||
|
| Reason::DataTransferQuotaExceeded
|
||||||
|
| Reason::LogicalSizeQuotaExceeded => false,
|
||||||
|
// transitive error. control plane is currently busy
|
||||||
|
// but might be ready soon
|
||||||
|
Reason::RunningOperations => true,
|
||||||
|
Reason::ConcurrencyLimitReached => true,
|
||||||
|
Reason::LockAlreadyTaken => true,
|
||||||
|
// unknown error. better not retry it.
|
||||||
|
Reason::Unknown => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +123,7 @@ pub struct Details {
|
|||||||
pub user_facing_message: Option<UserFacingMessage>,
|
pub user_facing_message: Option<UserFacingMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Copy, Clone, Debug, Deserialize)]
|
||||||
pub struct ErrorInfo {
|
pub struct ErrorInfo {
|
||||||
pub reason: Reason,
|
pub reason: Reason,
|
||||||
// Schema could also have `metadata` field, but it's not structured. Skip it for now.
|
// Schema could also have `metadata` field, but it's not structured. Skip it for now.
|
||||||
@@ -129,30 +131,59 @@ pub struct ErrorInfo {
|
|||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize, Default)]
|
#[derive(Clone, Copy, Debug, Deserialize, Default)]
|
||||||
pub enum Reason {
|
pub enum Reason {
|
||||||
|
/// RoleProtected indicates that the role is protected and the attempted operation is not permitted on protected roles.
|
||||||
#[serde(rename = "ROLE_PROTECTED")]
|
#[serde(rename = "ROLE_PROTECTED")]
|
||||||
RoleProtected,
|
RoleProtected,
|
||||||
|
/// ResourceNotFound indicates that a resource (project, endpoint, branch, etc.) wasn't found,
|
||||||
|
/// usually due to the provided ID not being correct or because the subject doesn't have enough permissions to
|
||||||
|
/// access the requested resource.
|
||||||
|
/// Prefer a more specific reason if possible, e.g., ProjectNotFound, EndpointNotFound, etc.
|
||||||
#[serde(rename = "RESOURCE_NOT_FOUND")]
|
#[serde(rename = "RESOURCE_NOT_FOUND")]
|
||||||
ResourceNotFound,
|
ResourceNotFound,
|
||||||
|
/// ProjectNotFound indicates that the project wasn't found, usually due to the provided ID not being correct,
|
||||||
|
/// or that the subject doesn't have enough permissions to access the requested project.
|
||||||
#[serde(rename = "PROJECT_NOT_FOUND")]
|
#[serde(rename = "PROJECT_NOT_FOUND")]
|
||||||
ProjectNotFound,
|
ProjectNotFound,
|
||||||
|
/// EndpointNotFound indicates that the endpoint wasn't found, usually due to the provided ID not being correct,
|
||||||
|
/// or that the subject doesn't have enough permissions to access the requested endpoint.
|
||||||
#[serde(rename = "ENDPOINT_NOT_FOUND")]
|
#[serde(rename = "ENDPOINT_NOT_FOUND")]
|
||||||
EndpointNotFound,
|
EndpointNotFound,
|
||||||
|
/// BranchNotFound indicates that the branch wasn't found, usually due to the provided ID not being correct,
|
||||||
|
/// or that the subject doesn't have enough permissions to access the requested branch.
|
||||||
#[serde(rename = "BRANCH_NOT_FOUND")]
|
#[serde(rename = "BRANCH_NOT_FOUND")]
|
||||||
BranchNotFound,
|
BranchNotFound,
|
||||||
|
/// RateLimitExceeded indicates that the rate limit for the operation has been exceeded.
|
||||||
#[serde(rename = "RATE_LIMIT_EXCEEDED")]
|
#[serde(rename = "RATE_LIMIT_EXCEEDED")]
|
||||||
RateLimitExceeded,
|
RateLimitExceeded,
|
||||||
|
/// NonDefaultBranchComputeTimeExceeded indicates that the compute time quota of non-default branches has been
|
||||||
|
/// exceeded.
|
||||||
#[serde(rename = "NON_PRIMARY_BRANCH_COMPUTE_TIME_EXCEEDED")]
|
#[serde(rename = "NON_PRIMARY_BRANCH_COMPUTE_TIME_EXCEEDED")]
|
||||||
NonPrimaryBranchComputeTimeExceeded,
|
NonDefaultBranchComputeTimeExceeded,
|
||||||
|
/// ActiveTimeQuotaExceeded indicates that the active time quota was exceeded.
|
||||||
#[serde(rename = "ACTIVE_TIME_QUOTA_EXCEEDED")]
|
#[serde(rename = "ACTIVE_TIME_QUOTA_EXCEEDED")]
|
||||||
ActiveTimeQuotaExceeded,
|
ActiveTimeQuotaExceeded,
|
||||||
|
/// ComputeTimeQuotaExceeded indicates that the compute time quota was exceeded.
|
||||||
#[serde(rename = "COMPUTE_TIME_QUOTA_EXCEEDED")]
|
#[serde(rename = "COMPUTE_TIME_QUOTA_EXCEEDED")]
|
||||||
ComputeTimeQuotaExceeded,
|
ComputeTimeQuotaExceeded,
|
||||||
|
/// WrittenDataQuotaExceeded indicates that the written data quota was exceeded.
|
||||||
#[serde(rename = "WRITTEN_DATA_QUOTA_EXCEEDED")]
|
#[serde(rename = "WRITTEN_DATA_QUOTA_EXCEEDED")]
|
||||||
WrittenDataQuotaExceeded,
|
WrittenDataQuotaExceeded,
|
||||||
|
/// DataTransferQuotaExceeded indicates that the data transfer quota was exceeded.
|
||||||
#[serde(rename = "DATA_TRANSFER_QUOTA_EXCEEDED")]
|
#[serde(rename = "DATA_TRANSFER_QUOTA_EXCEEDED")]
|
||||||
DataTransferQuotaExceeded,
|
DataTransferQuotaExceeded,
|
||||||
|
/// LogicalSizeQuotaExceeded indicates that the logical size quota was exceeded.
|
||||||
#[serde(rename = "LOGICAL_SIZE_QUOTA_EXCEEDED")]
|
#[serde(rename = "LOGICAL_SIZE_QUOTA_EXCEEDED")]
|
||||||
LogicalSizeQuotaExceeded,
|
LogicalSizeQuotaExceeded,
|
||||||
|
/// RunningOperations indicates that the project already has some running operations
|
||||||
|
/// and scheduling of new ones is prohibited.
|
||||||
|
#[serde(rename = "RUNNING_OPERATIONS")]
|
||||||
|
RunningOperations,
|
||||||
|
/// ConcurrencyLimitReached indicates that the concurrency limit for an action was reached.
|
||||||
|
#[serde(rename = "CONCURRENCY_LIMIT_REACHED")]
|
||||||
|
ConcurrencyLimitReached,
|
||||||
|
/// LockAlreadyTaken indicates that the we attempted to take a lock that was already taken.
|
||||||
|
#[serde(rename = "LOCK_ALREADY_TAKEN")]
|
||||||
|
LockAlreadyTaken,
|
||||||
#[default]
|
#[default]
|
||||||
#[serde(other)]
|
#[serde(other)]
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -170,7 +201,7 @@ impl Reason {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Copy, Clone, Debug, Deserialize)]
|
||||||
pub struct RetryInfo {
|
pub struct RetryInfo {
|
||||||
pub retry_delay_ms: u64,
|
pub retry_delay_ms: u64,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ use crate::{
|
|||||||
IpPattern,
|
IpPattern,
|
||||||
},
|
},
|
||||||
cache::{endpoints::EndpointsCache, project_info::ProjectInfoCacheImpl, Cached, TimedLru},
|
cache::{endpoints::EndpointsCache, project_info::ProjectInfoCacheImpl, Cached, TimedLru},
|
||||||
compute,
|
compute::{self, ConnCfg},
|
||||||
config::{CacheOptions, EndpointCacheConfig, ProjectInfoCacheOptions},
|
config::{CacheOptions, EndpointCacheConfig, ProjectInfoCacheOptions},
|
||||||
context::RequestMonitoring,
|
context::RequestMonitoring,
|
||||||
error::ReportableError,
|
error::ReportableError,
|
||||||
intern::ProjectIdInt,
|
intern::ProjectIdInt,
|
||||||
metrics::ApiLockMetrics,
|
metrics::ApiLockMetrics,
|
||||||
rate_limiter::{DynamicLimiter, Outcome, RateLimiterConfig, Token},
|
rate_limiter::{DynamicLimiter, Outcome, RateLimiterConfig, Token},
|
||||||
scram, EndpointCacheKey,
|
scram, EndpointCacheKey, Host,
|
||||||
};
|
};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use std::{hash::Hash, sync::Arc, time::Duration};
|
use std::{hash::Hash, sync::Arc, time::Duration};
|
||||||
@@ -25,9 +25,9 @@ use tracing::info;
|
|||||||
|
|
||||||
pub mod errors {
|
pub mod errors {
|
||||||
use crate::{
|
use crate::{
|
||||||
console::messages::{self, ConsoleError},
|
console::messages::{self, ConsoleError, Reason},
|
||||||
error::{io_error, ReportableError, UserFacingError},
|
error::{io_error, ReportableError, UserFacingError},
|
||||||
proxy::retry::ShouldRetry,
|
proxy::retry::CouldRetry,
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -76,21 +76,22 @@ pub mod errors {
|
|||||||
ApiError::Console(e) => {
|
ApiError::Console(e) => {
|
||||||
use crate::error::ErrorKind::*;
|
use crate::error::ErrorKind::*;
|
||||||
match e.get_reason() {
|
match e.get_reason() {
|
||||||
crate::console::messages::Reason::RoleProtected => User,
|
Reason::RoleProtected => User,
|
||||||
crate::console::messages::Reason::ResourceNotFound => User,
|
Reason::ResourceNotFound => User,
|
||||||
crate::console::messages::Reason::ProjectNotFound => User,
|
Reason::ProjectNotFound => User,
|
||||||
crate::console::messages::Reason::EndpointNotFound => User,
|
Reason::EndpointNotFound => User,
|
||||||
crate::console::messages::Reason::BranchNotFound => User,
|
Reason::BranchNotFound => User,
|
||||||
crate::console::messages::Reason::RateLimitExceeded => ServiceRateLimit,
|
Reason::RateLimitExceeded => ServiceRateLimit,
|
||||||
crate::console::messages::Reason::NonPrimaryBranchComputeTimeExceeded => {
|
Reason::NonDefaultBranchComputeTimeExceeded => User,
|
||||||
User
|
Reason::ActiveTimeQuotaExceeded => User,
|
||||||
}
|
Reason::ComputeTimeQuotaExceeded => User,
|
||||||
crate::console::messages::Reason::ActiveTimeQuotaExceeded => User,
|
Reason::WrittenDataQuotaExceeded => User,
|
||||||
crate::console::messages::Reason::ComputeTimeQuotaExceeded => User,
|
Reason::DataTransferQuotaExceeded => User,
|
||||||
crate::console::messages::Reason::WrittenDataQuotaExceeded => User,
|
Reason::LogicalSizeQuotaExceeded => User,
|
||||||
crate::console::messages::Reason::DataTransferQuotaExceeded => User,
|
Reason::ConcurrencyLimitReached => ControlPlane,
|
||||||
crate::console::messages::Reason::LogicalSizeQuotaExceeded => User,
|
Reason::LockAlreadyTaken => ControlPlane,
|
||||||
crate::console::messages::Reason::Unknown => match &e {
|
Reason::RunningOperations => ControlPlane,
|
||||||
|
Reason::Unknown => match &e {
|
||||||
ConsoleError {
|
ConsoleError {
|
||||||
http_status_code:
|
http_status_code:
|
||||||
http::StatusCode::NOT_FOUND | http::StatusCode::NOT_ACCEPTABLE,
|
http::StatusCode::NOT_FOUND | http::StatusCode::NOT_ACCEPTABLE,
|
||||||
@@ -128,7 +129,7 @@ pub mod errors {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShouldRetry for ApiError {
|
impl CouldRetry for ApiError {
|
||||||
fn could_retry(&self) -> bool {
|
fn could_retry(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
// retry some transport errors
|
// retry some transport errors
|
||||||
@@ -239,6 +240,17 @@ pub mod errors {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CouldRetry for WakeComputeError {
|
||||||
|
fn could_retry(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
WakeComputeError::BadComputeAddress(_) => false,
|
||||||
|
WakeComputeError::ApiError(e) => e.could_retry(),
|
||||||
|
WakeComputeError::TooManyConnections => false,
|
||||||
|
WakeComputeError::TooManyConnectionAttempts(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Auth secret which is managed by the cloud.
|
/// Auth secret which is managed by the cloud.
|
||||||
@@ -277,6 +289,33 @@ pub struct NodeInfo {
|
|||||||
pub allow_self_signed_compute: bool,
|
pub allow_self_signed_compute: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cached info for establishing a connection to a compute node.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct NodeCachedInfo {
|
||||||
|
pub host: Host,
|
||||||
|
pub port: u16,
|
||||||
|
|
||||||
|
/// Labels for proxy's metrics.
|
||||||
|
pub aux: MetricsAuxInfo,
|
||||||
|
|
||||||
|
/// Whether we should accept self-signed certificates (for testing)
|
||||||
|
pub allow_self_signed_compute: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeCachedInfo {
|
||||||
|
pub fn into_node_info(self) -> NodeInfo {
|
||||||
|
let mut config = ConnCfg::default();
|
||||||
|
config.ssl_mode(tokio_postgres::config::SslMode::Disable);
|
||||||
|
config.host(&self.host);
|
||||||
|
config.port(self.port);
|
||||||
|
NodeInfo {
|
||||||
|
config,
|
||||||
|
aux: self.aux,
|
||||||
|
allow_self_signed_compute: self.allow_self_signed_compute,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl NodeInfo {
|
impl NodeInfo {
|
||||||
pub async fn connect(
|
pub async fn connect(
|
||||||
&self,
|
&self,
|
||||||
@@ -305,8 +344,8 @@ impl NodeInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type NodeInfoCache = TimedLru<EndpointCacheKey, NodeInfo>;
|
pub type NodeInfoCache = TimedLru<EndpointCacheKey, NodeCachedInfo>;
|
||||||
pub type CachedNodeInfo = Cached<&'static NodeInfoCache>;
|
pub type CachedNodeInfo = Cached<&'static NodeInfoCache, NodeInfo>;
|
||||||
pub type CachedRoleSecret = Cached<&'static ProjectInfoCacheImpl, Option<AuthSecret>>;
|
pub type CachedRoleSecret = Cached<&'static ProjectInfoCacheImpl, Option<AuthSecret>>;
|
||||||
pub type CachedAllowedIps = Cached<&'static ProjectInfoCacheImpl, Arc<Vec<IpPattern>>>;
|
pub type CachedAllowedIps = Cached<&'static ProjectInfoCacheImpl, Arc<Vec<IpPattern>>>;
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,20 @@ use super::{
|
|||||||
super::messages::{ConsoleError, GetRoleSecret, WakeCompute},
|
super::messages::{ConsoleError, GetRoleSecret, WakeCompute},
|
||||||
errors::{ApiError, GetAuthInfoError, WakeComputeError},
|
errors::{ApiError, GetAuthInfoError, WakeComputeError},
|
||||||
ApiCaches, ApiLocks, AuthInfo, AuthSecret, CachedAllowedIps, CachedNodeInfo, CachedRoleSecret,
|
ApiCaches, ApiLocks, AuthInfo, AuthSecret, CachedAllowedIps, CachedNodeInfo, CachedRoleSecret,
|
||||||
NodeInfo,
|
NodeCachedInfo,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::backend::ComputeUserInfo,
|
auth::backend::ComputeUserInfo,
|
||||||
compute,
|
|
||||||
console::messages::ColdStartInfo,
|
console::messages::ColdStartInfo,
|
||||||
http,
|
http,
|
||||||
metrics::{CacheOutcome, Metrics},
|
metrics::{CacheOutcome, Metrics},
|
||||||
rate_limiter::EndpointRateLimiter,
|
rate_limiter::EndpointRateLimiter,
|
||||||
scram, EndpointCacheKey,
|
scram, EndpointCacheKey, Host,
|
||||||
};
|
};
|
||||||
use crate::{cache::Cached, context::RequestMonitoring};
|
use crate::{cache::Cached, context::RequestMonitoring};
|
||||||
use futures::TryFutureExt;
|
use futures::TryFutureExt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use tokio_postgres::config::SslMode;
|
|
||||||
use tracing::{error, info, info_span, warn, Instrument};
|
use tracing::{error, info, info_span, warn, Instrument};
|
||||||
|
|
||||||
pub struct Api {
|
pub struct Api {
|
||||||
@@ -132,7 +130,7 @@ impl Api {
|
|||||||
&self,
|
&self,
|
||||||
ctx: &mut RequestMonitoring,
|
ctx: &mut RequestMonitoring,
|
||||||
user_info: &ComputeUserInfo,
|
user_info: &ComputeUserInfo,
|
||||||
) -> Result<NodeInfo, WakeComputeError> {
|
) -> Result<NodeCachedInfo, WakeComputeError> {
|
||||||
let request_id = ctx.session_id.to_string();
|
let request_id = ctx.session_id.to_string();
|
||||||
let application_name = ctx.console_application_name();
|
let application_name = ctx.console_application_name();
|
||||||
async {
|
async {
|
||||||
@@ -167,15 +165,11 @@ impl Api {
|
|||||||
None => return Err(WakeComputeError::BadComputeAddress(body.address)),
|
None => return Err(WakeComputeError::BadComputeAddress(body.address)),
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
};
|
};
|
||||||
|
let host = Host(host.into());
|
||||||
|
|
||||||
// Don't set anything but host and port! This config will be cached.
|
let node = NodeCachedInfo {
|
||||||
// We'll set username and such later using the startup message.
|
host,
|
||||||
// TODO: add more type safety (in progress).
|
port,
|
||||||
let mut config = compute::ConnCfg::new();
|
|
||||||
config.host(host).port(port).ssl_mode(SslMode::Disable); // TLS is not configured on compute nodes.
|
|
||||||
|
|
||||||
let node = NodeInfo {
|
|
||||||
config,
|
|
||||||
aux: body.aux,
|
aux: body.aux,
|
||||||
allow_self_signed_compute: false,
|
allow_self_signed_compute: false,
|
||||||
};
|
};
|
||||||
@@ -278,9 +272,9 @@ impl super::Api for Api {
|
|||||||
// The connection info remains the same during that period of time,
|
// The connection info remains the same during that period of time,
|
||||||
// which means that we might cache it to reduce the load and latency.
|
// which means that we might cache it to reduce the load and latency.
|
||||||
if let Some(cached) = self.caches.node_info.get(&key) {
|
if let Some(cached) = self.caches.node_info.get(&key) {
|
||||||
info!(key = &*key, "found cached compute node info");
|
info!(key = display(&key), "found cached compute node info");
|
||||||
ctx.set_project(cached.aux.clone());
|
ctx.set_project(cached.aux.clone());
|
||||||
return Ok(cached);
|
return Ok(cached.map(NodeCachedInfo::into_node_info));
|
||||||
}
|
}
|
||||||
|
|
||||||
let permit = self.locks.get_permit(&key).await?;
|
let permit = self.locks.get_permit(&key).await?;
|
||||||
@@ -289,9 +283,9 @@ impl super::Api for Api {
|
|||||||
// double check
|
// double check
|
||||||
if permit.should_check_cache() {
|
if permit.should_check_cache() {
|
||||||
if let Some(cached) = self.caches.node_info.get(&key) {
|
if let Some(cached) = self.caches.node_info.get(&key) {
|
||||||
info!(key = &*key, "found cached compute node info");
|
info!(key = display(&key), "found cached compute node info");
|
||||||
ctx.set_project(cached.aux.clone());
|
ctx.set_project(cached.aux.clone());
|
||||||
return Ok(cached);
|
return Ok(cached.map(NodeCachedInfo::into_node_info));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +294,7 @@ impl super::Api for Api {
|
|||||||
.wake_compute_endpoint_rate_limiter
|
.wake_compute_endpoint_rate_limiter
|
||||||
.check(user_info.endpoint.normalize_intern(), 1)
|
.check(user_info.endpoint.normalize_intern(), 1)
|
||||||
{
|
{
|
||||||
info!(key = &*key, "found cached compute node info");
|
info!(key = display(&key), "found cached compute node info");
|
||||||
return Err(WakeComputeError::TooManyConnections);
|
return Err(WakeComputeError::TooManyConnections);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,9 +308,12 @@ impl super::Api for Api {
|
|||||||
let (_, mut cached) = self.caches.node_info.insert(key.clone(), node);
|
let (_, mut cached) = self.caches.node_info.insert(key.clone(), node);
|
||||||
cached.aux.cold_start_info = cold_start_info;
|
cached.aux.cold_start_info = cold_start_info;
|
||||||
|
|
||||||
info!(key = &*key, "created a cache entry for compute node info");
|
info!(
|
||||||
|
key = display(&key),
|
||||||
|
"created a cache entry for compute node info"
|
||||||
|
);
|
||||||
|
|
||||||
Ok(cached)
|
Ok(cached.map(NodeCachedInfo::into_node_info))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -157,8 +157,16 @@ smol_str_wrapper!(BranchId);
|
|||||||
// 90% of project strings are 23 characters or less.
|
// 90% of project strings are 23 characters or less.
|
||||||
smol_str_wrapper!(ProjectId);
|
smol_str_wrapper!(ProjectId);
|
||||||
|
|
||||||
// will usually equal endpoint ID
|
#[derive(PartialEq, Eq, Hash, Debug, Clone)]
|
||||||
smol_str_wrapper!(EndpointCacheKey);
|
pub struct EndpointCacheKey {
|
||||||
|
pub id: EndpointIdInt,
|
||||||
|
pub extra: Box<str>,
|
||||||
|
}
|
||||||
|
impl std::fmt::Display for EndpointCacheKey {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}{}", &self.id, &self.extra)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
smol_str_wrapper!(DbName);
|
smol_str_wrapper!(DbName);
|
||||||
|
|
||||||
|
|||||||
+21
-9
@@ -8,7 +8,9 @@ pub mod passthrough;
|
|||||||
pub mod retry;
|
pub mod retry;
|
||||||
pub mod wake_compute;
|
pub mod wake_compute;
|
||||||
pub use copy_bidirectional::copy_bidirectional_client_compute;
|
pub use copy_bidirectional::copy_bidirectional_client_compute;
|
||||||
|
pub use copy_bidirectional::ErrorSource;
|
||||||
|
|
||||||
|
use crate::intern::EndpointIdInt;
|
||||||
use crate::{
|
use crate::{
|
||||||
auth,
|
auth,
|
||||||
cancellation::{self, CancellationHandlerMain, CancellationHandlerMainInternal},
|
cancellation::{self, CancellationHandlerMain, CancellationHandlerMainInternal},
|
||||||
@@ -148,8 +150,11 @@ pub async fn task_main(
|
|||||||
ctx.log_connect();
|
ctx.log_connect();
|
||||||
match p.proxy_pass().instrument(span.clone()).await {
|
match p.proxy_pass().instrument(span.clone()).await {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(e) => {
|
Err(ErrorSource::Client(e)) => {
|
||||||
error!(parent: &span, "per-client task finished with an error: {e:#}");
|
error!(parent: &span, "per-client task finished with an IO error from the client: {e:#}");
|
||||||
|
}
|
||||||
|
Err(ErrorSource::Compute(e)) => {
|
||||||
|
error!(parent: &span, "per-client task finished with an IO error from the compute: {e:#}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -400,13 +405,20 @@ impl NeonOptions {
|
|||||||
Self(options)
|
Self(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cache_key(&self, prefix: &str) -> EndpointCacheKey {
|
pub fn get_cache_key(&self, endpoint: EndpointIdInt) -> EndpointCacheKey {
|
||||||
// prefix + format!(" {k}:{v}")
|
EndpointCacheKey {
|
||||||
// kinda jank because SmolStr is immutable
|
id: endpoint,
|
||||||
std::iter::once(prefix)
|
extra: self.get_cache_key_extras(),
|
||||||
.chain(self.0.iter().flat_map(|(k, v)| [" ", &**k, ":", &**v]))
|
}
|
||||||
.collect::<SmolStr>()
|
}
|
||||||
.into()
|
|
||||||
|
pub fn get_cache_key_extras(&self) -> Box<str> {
|
||||||
|
let mut extras = String::new();
|
||||||
|
for (k, v) in &self.0 {
|
||||||
|
use std::fmt::Write;
|
||||||
|
write!(&mut extras, " {k}:{v}").unwrap();
|
||||||
|
}
|
||||||
|
extras.into_boxed_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <https://swagger.io/docs/specification/serialization/> DeepObject format
|
/// <https://swagger.io/docs/specification/serialization/> DeepObject format
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use crate::{
|
|||||||
error::ReportableError,
|
error::ReportableError,
|
||||||
metrics::{ConnectOutcome, ConnectionFailureKind, Metrics, RetriesMetricGroup, RetryType},
|
metrics::{ConnectOutcome, ConnectionFailureKind, Metrics, RetriesMetricGroup, RetryType},
|
||||||
proxy::{
|
proxy::{
|
||||||
retry::{retry_after, ShouldRetry},
|
retry::{retry_after, should_retry, CouldRetry},
|
||||||
wake_compute::wake_compute,
|
wake_compute::wake_compute,
|
||||||
},
|
},
|
||||||
Host,
|
Host,
|
||||||
@@ -17,6 +17,8 @@ use pq_proto::StartupMessageParams;
|
|||||||
use tokio::time;
|
use tokio::time;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
|
use super::retry::ShouldRetryWakeCompute;
|
||||||
|
|
||||||
const CONNECT_TIMEOUT: time::Duration = time::Duration::from_secs(2);
|
const CONNECT_TIMEOUT: time::Duration = time::Duration::from_secs(2);
|
||||||
|
|
||||||
/// If we couldn't connect, a cached connection info might be to blame
|
/// If we couldn't connect, a cached connection info might be to blame
|
||||||
@@ -45,7 +47,7 @@ pub trait ConnectMechanism {
|
|||||||
async fn connect_once(
|
async fn connect_once(
|
||||||
&self,
|
&self,
|
||||||
ctx: &mut RequestMonitoring,
|
ctx: &mut RequestMonitoring,
|
||||||
node_info: &console::CachedNodeInfo,
|
node_info: &NodeInfo,
|
||||||
timeout: time::Duration,
|
timeout: time::Duration,
|
||||||
) -> Result<Self::Connection, Self::ConnectError>;
|
) -> Result<Self::Connection, Self::ConnectError>;
|
||||||
|
|
||||||
@@ -80,7 +82,7 @@ impl ConnectMechanism for TcpMechanism<'_> {
|
|||||||
async fn connect_once(
|
async fn connect_once(
|
||||||
&self,
|
&self,
|
||||||
ctx: &mut RequestMonitoring,
|
ctx: &mut RequestMonitoring,
|
||||||
node_info: &console::CachedNodeInfo,
|
node_info: &NodeInfo,
|
||||||
timeout: time::Duration,
|
timeout: time::Duration,
|
||||||
) -> Result<PostgresConnection, Self::Error> {
|
) -> Result<PostgresConnection, Self::Error> {
|
||||||
let host = node_info.config.get_host()?;
|
let host = node_info.config.get_host()?;
|
||||||
@@ -104,7 +106,7 @@ pub async fn connect_to_compute<M: ConnectMechanism, B: ComputeConnectBackend>(
|
|||||||
connect_to_compute_retry_config: RetryConfig,
|
connect_to_compute_retry_config: RetryConfig,
|
||||||
) -> Result<M::Connection, M::Error>
|
) -> Result<M::Connection, M::Error>
|
||||||
where
|
where
|
||||||
M::ConnectError: ShouldRetry + std::fmt::Debug,
|
M::ConnectError: CouldRetry + ShouldRetryWakeCompute + std::fmt::Debug,
|
||||||
M::Error: From<WakeComputeError>,
|
M::Error: From<WakeComputeError>,
|
||||||
{
|
{
|
||||||
let mut num_retries = 0;
|
let mut num_retries = 0;
|
||||||
@@ -139,10 +141,10 @@ where
|
|||||||
|
|
||||||
error!(error = ?err, "could not connect to compute node");
|
error!(error = ?err, "could not connect to compute node");
|
||||||
|
|
||||||
let node_info = if !node_info.cached() || !err.should_retry_database_address() {
|
let node_info = if !node_info.cached() || !err.should_retry_wake_compute() {
|
||||||
// If we just recieved this from cplane and dodn't get it from cache, we shouldn't retry.
|
// If we just recieved this from cplane and dodn't get it from cache, we shouldn't retry.
|
||||||
// Do not need to retrieve a new node_info, just return the old one.
|
// Do not need to retrieve a new node_info, just return the old one.
|
||||||
if !err.should_retry(num_retries, connect_to_compute_retry_config) {
|
if should_retry(&err, num_retries, connect_to_compute_retry_config) {
|
||||||
Metrics::get().proxy.retries_metric.observe(
|
Metrics::get().proxy.retries_metric.observe(
|
||||||
RetriesMetricGroup {
|
RetriesMetricGroup {
|
||||||
outcome: ConnectOutcome::Failed,
|
outcome: ConnectOutcome::Failed,
|
||||||
@@ -188,9 +190,8 @@ where
|
|||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let retriable = e.should_retry(num_retries, connect_to_compute_retry_config);
|
if !should_retry(&e, num_retries, connect_to_compute_retry_config) {
|
||||||
if !retriable {
|
error!(error = ?e, num_retries, retriable = false, "couldn't connect to compute node");
|
||||||
error!(error = ?e, num_retries, retriable, "couldn't connect to compute node");
|
|
||||||
Metrics::get().proxy.retries_metric.observe(
|
Metrics::get().proxy.retries_metric.observe(
|
||||||
RetriesMetricGroup {
|
RetriesMetricGroup {
|
||||||
outcome: ConnectOutcome::Failed,
|
outcome: ConnectOutcome::Failed,
|
||||||
@@ -200,9 +201,10 @@ where
|
|||||||
);
|
);
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
warn!(error = ?e, num_retries, retriable, "couldn't connect to compute node");
|
|
||||||
|
warn!(error = ?e, num_retries, retriable = true, "couldn't connect to compute node");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
let wait_duration = retry_after(num_retries, connect_to_compute_retry_config);
|
let wait_duration = retry_after(num_retries, connect_to_compute_retry_config);
|
||||||
num_retries += 1;
|
num_retries += 1;
|
||||||
|
|||||||
@@ -13,12 +13,39 @@ enum TransferState {
|
|||||||
Done(u64),
|
Done(u64),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ErrorDirection {
|
||||||
|
Read(io::Error),
|
||||||
|
Write(io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorSource {
|
||||||
|
fn from_client(err: ErrorDirection) -> ErrorSource {
|
||||||
|
match err {
|
||||||
|
ErrorDirection::Read(client) => Self::Client(client),
|
||||||
|
ErrorDirection::Write(compute) => Self::Compute(compute),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn from_compute(err: ErrorDirection) -> ErrorSource {
|
||||||
|
match err {
|
||||||
|
ErrorDirection::Write(client) => Self::Client(client),
|
||||||
|
ErrorDirection::Read(compute) => Self::Compute(compute),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ErrorSource {
|
||||||
|
Client(io::Error),
|
||||||
|
Compute(io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
fn transfer_one_direction<A, B>(
|
fn transfer_one_direction<A, B>(
|
||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
state: &mut TransferState,
|
state: &mut TransferState,
|
||||||
r: &mut A,
|
r: &mut A,
|
||||||
w: &mut B,
|
w: &mut B,
|
||||||
) -> Poll<io::Result<u64>>
|
) -> Poll<Result<u64, ErrorDirection>>
|
||||||
where
|
where
|
||||||
A: AsyncRead + AsyncWrite + Unpin + ?Sized,
|
A: AsyncRead + AsyncWrite + Unpin + ?Sized,
|
||||||
B: AsyncRead + AsyncWrite + Unpin + ?Sized,
|
B: AsyncRead + AsyncWrite + Unpin + ?Sized,
|
||||||
@@ -32,7 +59,7 @@ where
|
|||||||
*state = TransferState::ShuttingDown(count);
|
*state = TransferState::ShuttingDown(count);
|
||||||
}
|
}
|
||||||
TransferState::ShuttingDown(count) => {
|
TransferState::ShuttingDown(count) => {
|
||||||
ready!(w.as_mut().poll_shutdown(cx))?;
|
ready!(w.as_mut().poll_shutdown(cx)).map_err(ErrorDirection::Write)?;
|
||||||
*state = TransferState::Done(*count);
|
*state = TransferState::Done(*count);
|
||||||
}
|
}
|
||||||
TransferState::Done(count) => return Poll::Ready(Ok(*count)),
|
TransferState::Done(count) => return Poll::Ready(Ok(*count)),
|
||||||
@@ -44,7 +71,7 @@ where
|
|||||||
pub async fn copy_bidirectional_client_compute<Client, Compute>(
|
pub async fn copy_bidirectional_client_compute<Client, Compute>(
|
||||||
client: &mut Client,
|
client: &mut Client,
|
||||||
compute: &mut Compute,
|
compute: &mut Compute,
|
||||||
) -> Result<(u64, u64), std::io::Error>
|
) -> Result<(u64, u64), ErrorSource>
|
||||||
where
|
where
|
||||||
Client: AsyncRead + AsyncWrite + Unpin + ?Sized,
|
Client: AsyncRead + AsyncWrite + Unpin + ?Sized,
|
||||||
Compute: AsyncRead + AsyncWrite + Unpin + ?Sized,
|
Compute: AsyncRead + AsyncWrite + Unpin + ?Sized,
|
||||||
@@ -54,9 +81,11 @@ where
|
|||||||
|
|
||||||
poll_fn(|cx| {
|
poll_fn(|cx| {
|
||||||
let mut client_to_compute_result =
|
let mut client_to_compute_result =
|
||||||
transfer_one_direction(cx, &mut client_to_compute, client, compute)?;
|
transfer_one_direction(cx, &mut client_to_compute, client, compute)
|
||||||
|
.map_err(ErrorSource::from_client)?;
|
||||||
let mut compute_to_client_result =
|
let mut compute_to_client_result =
|
||||||
transfer_one_direction(cx, &mut compute_to_client, compute, client)?;
|
transfer_one_direction(cx, &mut compute_to_client, compute, client)
|
||||||
|
.map_err(ErrorSource::from_compute)?;
|
||||||
|
|
||||||
// Early termination checks from compute to client.
|
// Early termination checks from compute to client.
|
||||||
if let TransferState::Done(_) = compute_to_client {
|
if let TransferState::Done(_) = compute_to_client {
|
||||||
@@ -65,18 +94,20 @@ where
|
|||||||
// Initiate shutdown
|
// Initiate shutdown
|
||||||
client_to_compute = TransferState::ShuttingDown(buf.amt);
|
client_to_compute = TransferState::ShuttingDown(buf.amt);
|
||||||
client_to_compute_result =
|
client_to_compute_result =
|
||||||
transfer_one_direction(cx, &mut client_to_compute, client, compute)?;
|
transfer_one_direction(cx, &mut client_to_compute, client, compute)
|
||||||
|
.map_err(ErrorSource::from_client)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Early termination checks from compute to client.
|
// Early termination checks from client to compute.
|
||||||
if let TransferState::Done(_) = client_to_compute {
|
if let TransferState::Done(_) = client_to_compute {
|
||||||
if let TransferState::Running(buf) = &compute_to_client {
|
if let TransferState::Running(buf) = &compute_to_client {
|
||||||
info!("Client is done, terminate compute");
|
info!("Client is done, terminate compute");
|
||||||
// Initiate shutdown
|
// Initiate shutdown
|
||||||
compute_to_client = TransferState::ShuttingDown(buf.amt);
|
compute_to_client = TransferState::ShuttingDown(buf.amt);
|
||||||
compute_to_client_result =
|
compute_to_client_result =
|
||||||
transfer_one_direction(cx, &mut compute_to_client, client, compute)?;
|
transfer_one_direction(cx, &mut compute_to_client, compute, client)
|
||||||
|
.map_err(ErrorSource::from_compute)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +169,7 @@ impl CopyBuffer {
|
|||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
mut reader: Pin<&mut R>,
|
mut reader: Pin<&mut R>,
|
||||||
mut writer: Pin<&mut W>,
|
mut writer: Pin<&mut W>,
|
||||||
) -> Poll<io::Result<usize>>
|
) -> Poll<Result<usize, ErrorDirection>>
|
||||||
where
|
where
|
||||||
R: AsyncRead + ?Sized,
|
R: AsyncRead + ?Sized,
|
||||||
W: AsyncWrite + ?Sized,
|
W: AsyncWrite + ?Sized,
|
||||||
@@ -149,11 +180,11 @@ impl CopyBuffer {
|
|||||||
// Top up the buffer towards full if we can read a bit more
|
// Top up the buffer towards full if we can read a bit more
|
||||||
// data - this should improve the chances of a large write
|
// data - this should improve the chances of a large write
|
||||||
if !me.read_done && me.cap < me.buf.len() {
|
if !me.read_done && me.cap < me.buf.len() {
|
||||||
ready!(me.poll_fill_buf(cx, reader.as_mut()))?;
|
ready!(me.poll_fill_buf(cx, reader.as_mut())).map_err(ErrorDirection::Read)?;
|
||||||
}
|
}
|
||||||
Poll::Pending
|
Poll::Pending
|
||||||
}
|
}
|
||||||
res => res,
|
res => res.map_err(ErrorDirection::Write),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +193,7 @@ impl CopyBuffer {
|
|||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
mut reader: Pin<&mut R>,
|
mut reader: Pin<&mut R>,
|
||||||
mut writer: Pin<&mut W>,
|
mut writer: Pin<&mut W>,
|
||||||
) -> Poll<io::Result<u64>>
|
) -> Poll<Result<u64, ErrorDirection>>
|
||||||
where
|
where
|
||||||
R: AsyncRead + ?Sized,
|
R: AsyncRead + ?Sized,
|
||||||
W: AsyncWrite + ?Sized,
|
W: AsyncWrite + ?Sized,
|
||||||
@@ -176,12 +207,13 @@ impl CopyBuffer {
|
|||||||
|
|
||||||
match self.poll_fill_buf(cx, reader.as_mut()) {
|
match self.poll_fill_buf(cx, reader.as_mut()) {
|
||||||
Poll::Ready(Ok(())) => (),
|
Poll::Ready(Ok(())) => (),
|
||||||
Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
|
Poll::Ready(Err(err)) => return Poll::Ready(Err(ErrorDirection::Read(err))),
|
||||||
Poll::Pending => {
|
Poll::Pending => {
|
||||||
// Try flushing when the reader has no progress to avoid deadlock
|
// Try flushing when the reader has no progress to avoid deadlock
|
||||||
// when the reader depends on buffered writer.
|
// when the reader depends on buffered writer.
|
||||||
if self.need_flush {
|
if self.need_flush {
|
||||||
ready!(writer.as_mut().poll_flush(cx))?;
|
ready!(writer.as_mut().poll_flush(cx))
|
||||||
|
.map_err(ErrorDirection::Write)?;
|
||||||
self.need_flush = false;
|
self.need_flush = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,10 +226,10 @@ impl CopyBuffer {
|
|||||||
while self.pos < self.cap {
|
while self.pos < self.cap {
|
||||||
let i = ready!(self.poll_write_buf(cx, reader.as_mut(), writer.as_mut()))?;
|
let i = ready!(self.poll_write_buf(cx, reader.as_mut(), writer.as_mut()))?;
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
return Poll::Ready(Err(io::Error::new(
|
return Poll::Ready(Err(ErrorDirection::Write(io::Error::new(
|
||||||
io::ErrorKind::WriteZero,
|
io::ErrorKind::WriteZero,
|
||||||
"write zero byte into writer",
|
"write zero byte into writer",
|
||||||
)));
|
))));
|
||||||
} else {
|
} else {
|
||||||
self.pos += i;
|
self.pos += i;
|
||||||
self.amt += i as u64;
|
self.amt += i as u64;
|
||||||
@@ -216,7 +248,7 @@ impl CopyBuffer {
|
|||||||
// If we've written all the data and we've seen EOF, flush out the
|
// If we've written all the data and we've seen EOF, flush out the
|
||||||
// data and finish the transfer.
|
// data and finish the transfer.
|
||||||
if self.pos == self.cap && self.read_done {
|
if self.pos == self.cap && self.read_done {
|
||||||
ready!(writer.as_mut().poll_flush(cx))?;
|
ready!(writer.as_mut().poll_flush(cx)).map_err(ErrorDirection::Write)?;
|
||||||
return Poll::Ready(Ok(self.amt));
|
return Poll::Ready(Ok(self.amt));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ use tokio::io::{AsyncRead, AsyncWrite};
|
|||||||
use tracing::info;
|
use tracing::info;
|
||||||
use utils::measured_stream::MeasuredStream;
|
use utils::measured_stream::MeasuredStream;
|
||||||
|
|
||||||
|
use super::copy_bidirectional::ErrorSource;
|
||||||
|
|
||||||
/// Forward bytes in both directions (client <-> compute).
|
/// Forward bytes in both directions (client <-> compute).
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub async fn proxy_pass(
|
pub async fn proxy_pass(
|
||||||
client: impl AsyncRead + AsyncWrite + Unpin,
|
client: impl AsyncRead + AsyncWrite + Unpin,
|
||||||
compute: impl AsyncRead + AsyncWrite + Unpin,
|
compute: impl AsyncRead + AsyncWrite + Unpin,
|
||||||
aux: MetricsAuxInfo,
|
aux: MetricsAuxInfo,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<(), ErrorSource> {
|
||||||
let usage = USAGE_METRICS.register(Ids {
|
let usage = USAGE_METRICS.register(Ids {
|
||||||
endpoint_id: aux.endpoint_id,
|
endpoint_id: aux.endpoint_id,
|
||||||
branch_id: aux.branch_id,
|
branch_id: aux.branch_id,
|
||||||
@@ -66,9 +68,11 @@ pub struct ProxyPassthrough<P, S> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<P, S: AsyncRead + AsyncWrite + Unpin> ProxyPassthrough<P, S> {
|
impl<P, S: AsyncRead + AsyncWrite + Unpin> ProxyPassthrough<P, S> {
|
||||||
pub async fn proxy_pass(self) -> anyhow::Result<()> {
|
pub async fn proxy_pass(self) -> Result<(), ErrorSource> {
|
||||||
let res = proxy_pass(self.client, self.compute.stream, self.aux).await;
|
let res = proxy_pass(self.client, self.compute.stream, self.aux).await;
|
||||||
self.compute.cancel_closure.try_cancel_query().await?;
|
if let Err(err) = self.compute.cancel_closure.try_cancel_query().await {
|
||||||
|
tracing::error!(?err, "could not cancel the query in the database");
|
||||||
|
}
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-23
@@ -2,20 +2,22 @@ use crate::{compute, config::RetryConfig};
|
|||||||
use std::{error::Error, io};
|
use std::{error::Error, io};
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
|
|
||||||
pub trait ShouldRetry {
|
pub trait CouldRetry {
|
||||||
|
/// Returns true if the error could be retried
|
||||||
fn could_retry(&self) -> bool;
|
fn could_retry(&self) -> bool;
|
||||||
fn should_retry(&self, num_retries: u32, config: RetryConfig) -> bool {
|
|
||||||
match self {
|
|
||||||
_ if num_retries >= config.max_retries => false,
|
|
||||||
err => err.could_retry(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn should_retry_database_address(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShouldRetry for io::Error {
|
pub trait ShouldRetryWakeCompute {
|
||||||
|
/// Returns true if we need to invalidate the cache for this node.
|
||||||
|
/// If false, we can continue retrying with the current node cache.
|
||||||
|
fn should_retry_wake_compute(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn should_retry(err: &impl CouldRetry, num_retries: u32, config: RetryConfig) -> bool {
|
||||||
|
num_retries < config.max_retries && err.could_retry()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CouldRetry for io::Error {
|
||||||
fn could_retry(&self) -> bool {
|
fn could_retry(&self) -> bool {
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
matches!(
|
matches!(
|
||||||
@@ -25,7 +27,7 @@ impl ShouldRetry for io::Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShouldRetry for tokio_postgres::error::DbError {
|
impl CouldRetry for tokio_postgres::error::DbError {
|
||||||
fn could_retry(&self) -> bool {
|
fn could_retry(&self) -> bool {
|
||||||
use tokio_postgres::error::SqlState;
|
use tokio_postgres::error::SqlState;
|
||||||
matches!(
|
matches!(
|
||||||
@@ -36,7 +38,9 @@ impl ShouldRetry for tokio_postgres::error::DbError {
|
|||||||
| &SqlState::SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION,
|
| &SqlState::SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn should_retry_database_address(&self) -> bool {
|
}
|
||||||
|
impl ShouldRetryWakeCompute for tokio_postgres::error::DbError {
|
||||||
|
fn should_retry_wake_compute(&self) -> bool {
|
||||||
use tokio_postgres::error::SqlState;
|
use tokio_postgres::error::SqlState;
|
||||||
// Here are errors that happens after the user successfully authenticated to the database.
|
// Here are errors that happens after the user successfully authenticated to the database.
|
||||||
// TODO: there are pgbouncer errors that should be retried, but they are not listed here.
|
// TODO: there are pgbouncer errors that should be retried, but they are not listed here.
|
||||||
@@ -53,7 +57,7 @@ impl ShouldRetry for tokio_postgres::error::DbError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShouldRetry for tokio_postgres::Error {
|
impl CouldRetry for tokio_postgres::Error {
|
||||||
fn could_retry(&self) -> bool {
|
fn could_retry(&self) -> bool {
|
||||||
if let Some(io_err) = self.source().and_then(|x| x.downcast_ref()) {
|
if let Some(io_err) = self.source().and_then(|x| x.downcast_ref()) {
|
||||||
io::Error::could_retry(io_err)
|
io::Error::could_retry(io_err)
|
||||||
@@ -63,29 +67,33 @@ impl ShouldRetry for tokio_postgres::Error {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn should_retry_database_address(&self) -> bool {
|
}
|
||||||
if let Some(io_err) = self.source().and_then(|x| x.downcast_ref()) {
|
impl ShouldRetryWakeCompute for tokio_postgres::Error {
|
||||||
io::Error::should_retry_database_address(io_err)
|
fn should_retry_wake_compute(&self) -> bool {
|
||||||
} else if let Some(db_err) = self.source().and_then(|x| x.downcast_ref()) {
|
if let Some(db_err) = self.source().and_then(|x| x.downcast_ref()) {
|
||||||
tokio_postgres::error::DbError::should_retry_database_address(db_err)
|
tokio_postgres::error::DbError::should_retry_wake_compute(db_err)
|
||||||
} else {
|
} else {
|
||||||
|
// likely an IO error. Possible the compute has shutdown and the
|
||||||
|
// cache is stale.
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShouldRetry for compute::ConnectionError {
|
impl CouldRetry for compute::ConnectionError {
|
||||||
fn could_retry(&self) -> bool {
|
fn could_retry(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
compute::ConnectionError::Postgres(err) => err.could_retry(),
|
compute::ConnectionError::Postgres(err) => err.could_retry(),
|
||||||
compute::ConnectionError::CouldNotConnect(err) => err.could_retry(),
|
compute::ConnectionError::CouldNotConnect(err) => err.could_retry(),
|
||||||
|
compute::ConnectionError::WakeComputeError(err) => err.could_retry(),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn should_retry_database_address(&self) -> bool {
|
}
|
||||||
|
impl ShouldRetryWakeCompute for compute::ConnectionError {
|
||||||
|
fn should_retry_wake_compute(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
compute::ConnectionError::Postgres(err) => err.should_retry_database_address(),
|
compute::ConnectionError::Postgres(err) => err.should_retry_wake_compute(),
|
||||||
compute::ConnectionError::CouldNotConnect(err) => err.should_retry_database_address(),
|
|
||||||
// the cache entry was not checked for validity
|
// the cache entry was not checked for validity
|
||||||
compute::ConnectionError::TooManyConnectionAttempts(_) => false,
|
compute::ConnectionError::TooManyConnectionAttempts(_) => false,
|
||||||
_ => true,
|
_ => true,
|
||||||
|
|||||||
+33
-13
@@ -5,21 +5,23 @@ mod mitm;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use super::connect_compute::ConnectMechanism;
|
use super::connect_compute::ConnectMechanism;
|
||||||
use super::retry::ShouldRetry;
|
use super::retry::CouldRetry;
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::auth::backend::{
|
use crate::auth::backend::{
|
||||||
ComputeCredentialKeys, ComputeCredentials, ComputeUserInfo, MaybeOwned, TestBackend,
|
ComputeCredentialKeys, ComputeCredentials, ComputeUserInfo, MaybeOwned, TestBackend,
|
||||||
};
|
};
|
||||||
use crate::config::{CertResolver, RetryConfig};
|
use crate::config::{CertResolver, RetryConfig};
|
||||||
use crate::console::caches::NodeInfoCache;
|
use crate::console::caches::NodeInfoCache;
|
||||||
use crate::console::messages::{ConsoleError, MetricsAuxInfo};
|
use crate::console::messages::{ConsoleError, Details, MetricsAuxInfo, Status};
|
||||||
use crate::console::provider::{CachedAllowedIps, CachedRoleSecret, ConsoleBackend};
|
use crate::console::provider::{
|
||||||
use crate::console::{self, CachedNodeInfo, NodeInfo};
|
CachedAllowedIps, CachedRoleSecret, ConsoleBackend, NodeCachedInfo,
|
||||||
|
};
|
||||||
|
use crate::console::{self, CachedNodeInfo};
|
||||||
use crate::error::ErrorKind;
|
use crate::error::ErrorKind;
|
||||||
use crate::proxy::retry::retry_after;
|
|
||||||
use crate::{http, sasl, scram, BranchId, EndpointId, ProjectId};
|
use crate::{http, sasl, scram, BranchId, EndpointId, ProjectId};
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use retry::{retry_after, ShouldRetryWakeCompute};
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
use rustls::pki_types;
|
use rustls::pki_types;
|
||||||
use tokio_postgres::config::SslMode;
|
use tokio_postgres::config::SslMode;
|
||||||
@@ -438,11 +440,16 @@ impl std::fmt::Display for TestConnectError {
|
|||||||
|
|
||||||
impl std::error::Error for TestConnectError {}
|
impl std::error::Error for TestConnectError {}
|
||||||
|
|
||||||
impl ShouldRetry for TestConnectError {
|
impl CouldRetry for TestConnectError {
|
||||||
fn could_retry(&self) -> bool {
|
fn could_retry(&self) -> bool {
|
||||||
self.retryable
|
self.retryable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl ShouldRetryWakeCompute for TestConnectError {
|
||||||
|
fn should_retry_wake_compute(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ConnectMechanism for TestConnectMechanism {
|
impl ConnectMechanism for TestConnectMechanism {
|
||||||
@@ -453,7 +460,7 @@ impl ConnectMechanism for TestConnectMechanism {
|
|||||||
async fn connect_once(
|
async fn connect_once(
|
||||||
&self,
|
&self,
|
||||||
_ctx: &mut RequestMonitoring,
|
_ctx: &mut RequestMonitoring,
|
||||||
_node_info: &console::CachedNodeInfo,
|
_node_info: &console::NodeInfo,
|
||||||
_timeout: std::time::Duration,
|
_timeout: std::time::Duration,
|
||||||
) -> Result<Self::Connection, Self::ConnectError> {
|
) -> Result<Self::Connection, Self::ConnectError> {
|
||||||
let mut counter = self.counter.lock().unwrap();
|
let mut counter = self.counter.lock().unwrap();
|
||||||
@@ -485,7 +492,7 @@ impl TestBackend for TestConnectMechanism {
|
|||||||
ConnectAction::Wake => Ok(helper_create_cached_node_info(self.cache)),
|
ConnectAction::Wake => Ok(helper_create_cached_node_info(self.cache)),
|
||||||
ConnectAction::WakeFail => {
|
ConnectAction::WakeFail => {
|
||||||
let err = console::errors::ApiError::Console(ConsoleError {
|
let err = console::errors::ApiError::Console(ConsoleError {
|
||||||
http_status_code: http::StatusCode::FORBIDDEN,
|
http_status_code: http::StatusCode::BAD_REQUEST,
|
||||||
error: "TEST".into(),
|
error: "TEST".into(),
|
||||||
status: None,
|
status: None,
|
||||||
});
|
});
|
||||||
@@ -496,7 +503,15 @@ impl TestBackend for TestConnectMechanism {
|
|||||||
let err = console::errors::ApiError::Console(ConsoleError {
|
let err = console::errors::ApiError::Console(ConsoleError {
|
||||||
http_status_code: http::StatusCode::BAD_REQUEST,
|
http_status_code: http::StatusCode::BAD_REQUEST,
|
||||||
error: "TEST".into(),
|
error: "TEST".into(),
|
||||||
status: None,
|
status: Some(Status {
|
||||||
|
code: "error".into(),
|
||||||
|
message: "error".into(),
|
||||||
|
details: Details {
|
||||||
|
error_info: None,
|
||||||
|
retry_info: Some(console::messages::RetryInfo { retry_delay_ms: 1 }),
|
||||||
|
user_facing_message: None,
|
||||||
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
assert!(err.could_retry());
|
assert!(err.could_retry());
|
||||||
Err(console::errors::WakeComputeError::ApiError(err))
|
Err(console::errors::WakeComputeError::ApiError(err))
|
||||||
@@ -517,8 +532,9 @@ impl TestBackend for TestConnectMechanism {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn helper_create_cached_node_info(cache: &'static NodeInfoCache) -> CachedNodeInfo {
|
fn helper_create_cached_node_info(cache: &'static NodeInfoCache) -> CachedNodeInfo {
|
||||||
let node = NodeInfo {
|
let node = NodeCachedInfo {
|
||||||
config: compute::ConnCfg::new(),
|
host: "localhost".into(),
|
||||||
|
port: 5432,
|
||||||
aux: MetricsAuxInfo {
|
aux: MetricsAuxInfo {
|
||||||
endpoint_id: (&EndpointId::from("endpoint")).into(),
|
endpoint_id: (&EndpointId::from("endpoint")).into(),
|
||||||
project_id: (&ProjectId::from("project")).into(),
|
project_id: (&ProjectId::from("project")).into(),
|
||||||
@@ -527,8 +543,12 @@ fn helper_create_cached_node_info(cache: &'static NodeInfoCache) -> CachedNodeIn
|
|||||||
},
|
},
|
||||||
allow_self_signed_compute: false,
|
allow_self_signed_compute: false,
|
||||||
};
|
};
|
||||||
let (_, node) = cache.insert("key".into(), node);
|
let key = EndpointCacheKey {
|
||||||
node
|
id: node.aux.endpoint_id,
|
||||||
|
extra: "".into(),
|
||||||
|
};
|
||||||
|
let (_, node) = cache.insert(key, node);
|
||||||
|
node.map(NodeCachedInfo::into_node_info)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn helper_create_connect_info(
|
fn helper_create_connect_info(
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
use crate::config::RetryConfig;
|
use crate::config::RetryConfig;
|
||||||
use crate::console::messages::ConsoleError;
|
use crate::console::messages::{ConsoleError, Reason};
|
||||||
use crate::console::{errors::WakeComputeError, provider::CachedNodeInfo};
|
use crate::console::{errors::WakeComputeError, provider::CachedNodeInfo};
|
||||||
use crate::context::RequestMonitoring;
|
use crate::context::RequestMonitoring;
|
||||||
use crate::metrics::{
|
use crate::metrics::{
|
||||||
ConnectOutcome, ConnectionFailuresBreakdownGroup, Metrics, RetriesMetricGroup, RetryType,
|
ConnectOutcome, ConnectionFailuresBreakdownGroup, Metrics, RetriesMetricGroup, RetryType,
|
||||||
WakeupFailureKind,
|
WakeupFailureKind,
|
||||||
};
|
};
|
||||||
use crate::proxy::retry::retry_after;
|
use crate::proxy::retry::{retry_after, should_retry};
|
||||||
use hyper1::StatusCode;
|
use hyper1::StatusCode;
|
||||||
use std::ops::ControlFlow;
|
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use super::connect_compute::ComputeConnectBackend;
|
use super::connect_compute::ComputeConnectBackend;
|
||||||
use super::retry::ShouldRetry;
|
|
||||||
|
|
||||||
pub async fn wake_compute<B: ComputeConnectBackend>(
|
pub async fn wake_compute<B: ComputeConnectBackend>(
|
||||||
num_retries: &mut u32,
|
num_retries: &mut u32,
|
||||||
@@ -22,9 +20,8 @@ pub async fn wake_compute<B: ComputeConnectBackend>(
|
|||||||
) -> Result<CachedNodeInfo, WakeComputeError> {
|
) -> Result<CachedNodeInfo, WakeComputeError> {
|
||||||
let retry_type = RetryType::WakeCompute;
|
let retry_type = RetryType::WakeCompute;
|
||||||
loop {
|
loop {
|
||||||
let wake_res = api.wake_compute(ctx).await;
|
match api.wake_compute(ctx).await {
|
||||||
match handle_try_wake(wake_res, *num_retries, config) {
|
Err(e) if !should_retry(&e, *num_retries, config) => {
|
||||||
Err(e) => {
|
|
||||||
error!(error = ?e, num_retries, retriable = false, "couldn't wake compute node");
|
error!(error = ?e, num_retries, retriable = false, "couldn't wake compute node");
|
||||||
report_error(&e, false);
|
report_error(&e, false);
|
||||||
Metrics::get().proxy.retries_metric.observe(
|
Metrics::get().proxy.retries_metric.observe(
|
||||||
@@ -36,11 +33,11 @@ pub async fn wake_compute<B: ComputeConnectBackend>(
|
|||||||
);
|
);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
Ok(ControlFlow::Continue(e)) => {
|
Err(e) => {
|
||||||
warn!(error = ?e, num_retries, retriable = true, "couldn't wake compute node");
|
warn!(error = ?e, num_retries, retriable = true, "couldn't wake compute node");
|
||||||
report_error(&e, true);
|
report_error(&e, true);
|
||||||
}
|
}
|
||||||
Ok(ControlFlow::Break(n)) => {
|
Ok(n) => {
|
||||||
Metrics::get().proxy.retries_metric.observe(
|
Metrics::get().proxy.retries_metric.observe(
|
||||||
RetriesMetricGroup {
|
RetriesMetricGroup {
|
||||||
outcome: ConnectOutcome::Success,
|
outcome: ConnectOutcome::Success,
|
||||||
@@ -63,70 +60,28 @@ pub async fn wake_compute<B: ComputeConnectBackend>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to wake up the compute node.
|
|
||||||
/// * Returns Ok(Continue(e)) if there was an error waking but retries are acceptable
|
|
||||||
/// * Returns Ok(Break(node)) if the wakeup succeeded
|
|
||||||
/// * Returns Err(e) if there was an error
|
|
||||||
pub fn handle_try_wake(
|
|
||||||
result: Result<CachedNodeInfo, WakeComputeError>,
|
|
||||||
num_retries: u32,
|
|
||||||
config: RetryConfig,
|
|
||||||
) -> Result<ControlFlow<CachedNodeInfo, WakeComputeError>, WakeComputeError> {
|
|
||||||
match result {
|
|
||||||
Err(err) => match &err {
|
|
||||||
WakeComputeError::ApiError(api) if api.should_retry(num_retries, config) => {
|
|
||||||
Ok(ControlFlow::Continue(err))
|
|
||||||
}
|
|
||||||
_ => Err(err),
|
|
||||||
},
|
|
||||||
// Ready to try again.
|
|
||||||
Ok(new) => Ok(ControlFlow::Break(new)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn report_error(e: &WakeComputeError, retry: bool) {
|
fn report_error(e: &WakeComputeError, retry: bool) {
|
||||||
use crate::console::errors::ApiError;
|
use crate::console::errors::ApiError;
|
||||||
let kind = match e {
|
let kind = match e {
|
||||||
WakeComputeError::BadComputeAddress(_) => WakeupFailureKind::BadComputeAddress,
|
WakeComputeError::BadComputeAddress(_) => WakeupFailureKind::BadComputeAddress,
|
||||||
WakeComputeError::ApiError(ApiError::Transport(_)) => WakeupFailureKind::ApiTransportError,
|
WakeComputeError::ApiError(ApiError::Transport(_)) => WakeupFailureKind::ApiTransportError,
|
||||||
WakeComputeError::ApiError(ApiError::Console(e)) => match e.get_reason() {
|
WakeComputeError::ApiError(ApiError::Console(e)) => match e.get_reason() {
|
||||||
crate::console::messages::Reason::RoleProtected => {
|
Reason::RoleProtected => WakeupFailureKind::ApiConsoleBadRequest,
|
||||||
WakeupFailureKind::ApiConsoleBadRequest
|
Reason::ResourceNotFound => WakeupFailureKind::ApiConsoleBadRequest,
|
||||||
}
|
Reason::ProjectNotFound => WakeupFailureKind::ApiConsoleBadRequest,
|
||||||
crate::console::messages::Reason::ResourceNotFound => {
|
Reason::EndpointNotFound => WakeupFailureKind::ApiConsoleBadRequest,
|
||||||
WakeupFailureKind::ApiConsoleBadRequest
|
Reason::BranchNotFound => WakeupFailureKind::ApiConsoleBadRequest,
|
||||||
}
|
Reason::RateLimitExceeded => WakeupFailureKind::ApiConsoleLocked,
|
||||||
crate::console::messages::Reason::ProjectNotFound => {
|
Reason::NonDefaultBranchComputeTimeExceeded => WakeupFailureKind::QuotaExceeded,
|
||||||
WakeupFailureKind::ApiConsoleBadRequest
|
Reason::ActiveTimeQuotaExceeded => WakeupFailureKind::QuotaExceeded,
|
||||||
}
|
Reason::ComputeTimeQuotaExceeded => WakeupFailureKind::QuotaExceeded,
|
||||||
crate::console::messages::Reason::EndpointNotFound => {
|
Reason::WrittenDataQuotaExceeded => WakeupFailureKind::QuotaExceeded,
|
||||||
WakeupFailureKind::ApiConsoleBadRequest
|
Reason::DataTransferQuotaExceeded => WakeupFailureKind::QuotaExceeded,
|
||||||
}
|
Reason::LogicalSizeQuotaExceeded => WakeupFailureKind::QuotaExceeded,
|
||||||
crate::console::messages::Reason::BranchNotFound => {
|
Reason::ConcurrencyLimitReached => WakeupFailureKind::ApiConsoleLocked,
|
||||||
WakeupFailureKind::ApiConsoleBadRequest
|
Reason::LockAlreadyTaken => WakeupFailureKind::ApiConsoleLocked,
|
||||||
}
|
Reason::RunningOperations => WakeupFailureKind::ApiConsoleLocked,
|
||||||
crate::console::messages::Reason::RateLimitExceeded => {
|
Reason::Unknown => match e {
|
||||||
WakeupFailureKind::ApiConsoleLocked
|
|
||||||
}
|
|
||||||
crate::console::messages::Reason::NonPrimaryBranchComputeTimeExceeded => {
|
|
||||||
WakeupFailureKind::QuotaExceeded
|
|
||||||
}
|
|
||||||
crate::console::messages::Reason::ActiveTimeQuotaExceeded => {
|
|
||||||
WakeupFailureKind::QuotaExceeded
|
|
||||||
}
|
|
||||||
crate::console::messages::Reason::ComputeTimeQuotaExceeded => {
|
|
||||||
WakeupFailureKind::QuotaExceeded
|
|
||||||
}
|
|
||||||
crate::console::messages::Reason::WrittenDataQuotaExceeded => {
|
|
||||||
WakeupFailureKind::QuotaExceeded
|
|
||||||
}
|
|
||||||
crate::console::messages::Reason::DataTransferQuotaExceeded => {
|
|
||||||
WakeupFailureKind::QuotaExceeded
|
|
||||||
}
|
|
||||||
crate::console::messages::Reason::LogicalSizeQuotaExceeded => {
|
|
||||||
WakeupFailureKind::QuotaExceeded
|
|
||||||
}
|
|
||||||
crate::console::messages::Reason::Unknown => match e {
|
|
||||||
ConsoleError {
|
ConsoleError {
|
||||||
http_status_code: StatusCode::LOCKED,
|
http_status_code: StatusCode::LOCKED,
|
||||||
ref error,
|
ref error,
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ use crate::{
|
|||||||
errors::{GetAuthInfoError, WakeComputeError},
|
errors::{GetAuthInfoError, WakeComputeError},
|
||||||
locks::ApiLocks,
|
locks::ApiLocks,
|
||||||
provider::ApiLockError,
|
provider::ApiLockError,
|
||||||
CachedNodeInfo,
|
NodeInfo,
|
||||||
},
|
},
|
||||||
context::RequestMonitoring,
|
context::RequestMonitoring,
|
||||||
error::{ErrorKind, ReportableError, UserFacingError},
|
error::{ErrorKind, ReportableError, UserFacingError},
|
||||||
intern::EndpointIdInt,
|
intern::EndpointIdInt,
|
||||||
proxy::{connect_compute::ConnectMechanism, retry::ShouldRetry},
|
proxy::{
|
||||||
|
connect_compute::ConnectMechanism,
|
||||||
|
retry::{CouldRetry, ShouldRetryWakeCompute},
|
||||||
|
},
|
||||||
rate_limiter::EndpointRateLimiter,
|
rate_limiter::EndpointRateLimiter,
|
||||||
Host,
|
Host,
|
||||||
};
|
};
|
||||||
@@ -179,7 +182,7 @@ impl UserFacingError for HttpConnError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShouldRetry for HttpConnError {
|
impl CouldRetry for HttpConnError {
|
||||||
fn could_retry(&self) -> bool {
|
fn could_retry(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
HttpConnError::ConnectionError(e) => e.could_retry(),
|
HttpConnError::ConnectionError(e) => e.could_retry(),
|
||||||
@@ -190,9 +193,11 @@ impl ShouldRetry for HttpConnError {
|
|||||||
HttpConnError::TooManyConnectionAttempts(_) => false,
|
HttpConnError::TooManyConnectionAttempts(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn should_retry_database_address(&self) -> bool {
|
}
|
||||||
|
impl ShouldRetryWakeCompute for HttpConnError {
|
||||||
|
fn should_retry_wake_compute(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
HttpConnError::ConnectionError(e) => e.should_retry_database_address(),
|
HttpConnError::ConnectionError(e) => e.should_retry_wake_compute(),
|
||||||
// we never checked cache validity
|
// we never checked cache validity
|
||||||
HttpConnError::TooManyConnectionAttempts(_) => false,
|
HttpConnError::TooManyConnectionAttempts(_) => false,
|
||||||
_ => true,
|
_ => true,
|
||||||
@@ -218,7 +223,7 @@ impl ConnectMechanism for TokioMechanism {
|
|||||||
async fn connect_once(
|
async fn connect_once(
|
||||||
&self,
|
&self,
|
||||||
ctx: &mut RequestMonitoring,
|
ctx: &mut RequestMonitoring,
|
||||||
node_info: &CachedNodeInfo,
|
node_info: &NodeInfo,
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
) -> Result<Self::Connection, Self::ConnectError> {
|
) -> Result<Self::Connection, Self::ConnectError> {
|
||||||
let host = node_info.config.get_host()?;
|
let host = node_info.config.get_host()?;
|
||||||
@@ -231,6 +236,10 @@ impl ConnectMechanism for TokioMechanism {
|
|||||||
.dbname(&self.conn_info.dbname)
|
.dbname(&self.conn_info.dbname)
|
||||||
.connect_timeout(timeout);
|
.connect_timeout(timeout);
|
||||||
|
|
||||||
|
config
|
||||||
|
.param("client_encoding", "UTF8")
|
||||||
|
.expect("client encoding UTF8 is always valid");
|
||||||
|
|
||||||
let pause = ctx.latency_timer.pause(crate::metrics::Waiting::Compute);
|
let pause = ctx.latency_timer.pause(crate::metrics::Waiting::Compute);
|
||||||
let res = config.connect(tokio_postgres::NoTls).await;
|
let res = config.connect(tokio_postgres::NoTls).await;
|
||||||
drop(pause);
|
drop(pause);
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ impl fmt::Display for ConnInfo {
|
|||||||
self.user_info.user,
|
self.user_info.user,
|
||||||
self.user_info.endpoint,
|
self.user_info.endpoint,
|
||||||
self.dbname,
|
self.dbname,
|
||||||
self.user_info.options.get_cache_key("")
|
self.user_info.options.get_cache_key_extras()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ fn get_conn_info(
|
|||||||
options = Some(NeonOptions::parse_options_raw(&value));
|
options = Some(NeonOptions::parse_options_raw(&value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ctx.set_db_options(params.freeze());
|
||||||
|
|
||||||
let user_info = ComputeUserInfo {
|
let user_info = ComputeUserInfo {
|
||||||
endpoint,
|
endpoint,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::proxy::ErrorSource;
|
||||||
use crate::{
|
use crate::{
|
||||||
cancellation::CancellationHandlerMain,
|
cancellation::CancellationHandlerMain,
|
||||||
config::ProxyConfig,
|
config::ProxyConfig,
|
||||||
@@ -7,6 +8,7 @@ use crate::{
|
|||||||
proxy::{handle_client, ClientMode},
|
proxy::{handle_client, ClientMode},
|
||||||
rate_limiter::EndpointRateLimiter,
|
rate_limiter::EndpointRateLimiter,
|
||||||
};
|
};
|
||||||
|
use anyhow::Context as _;
|
||||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||||
use framed_websockets::{Frame, OpCode, WebSocketServer};
|
use framed_websockets::{Frame, OpCode, WebSocketServer};
|
||||||
use futures::{Sink, Stream};
|
use futures::{Sink, Stream};
|
||||||
@@ -165,7 +167,11 @@ pub async fn serve_websocket(
|
|||||||
Ok(Some(p)) => {
|
Ok(Some(p)) => {
|
||||||
ctx.set_success();
|
ctx.set_success();
|
||||||
ctx.log_connect();
|
ctx.log_connect();
|
||||||
p.proxy_pass().await
|
match p.proxy_pass().await {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(ErrorSource::Client(err)) => Err(err).context("client"),
|
||||||
|
Err(ErrorSource::Compute(err)) => Err(err).context("compute"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ use utils::pid_file;
|
|||||||
|
|
||||||
use metrics::set_build_info_metric;
|
use metrics::set_build_info_metric;
|
||||||
use safekeeper::defaults::{
|
use safekeeper::defaults::{
|
||||||
DEFAULT_HEARTBEAT_TIMEOUT, DEFAULT_HTTP_LISTEN_ADDR, DEFAULT_MAX_OFFLOADER_LAG_BYTES,
|
DEFAULT_CONTROL_FILE_SAVE_INTERVAL, DEFAULT_HEARTBEAT_TIMEOUT, DEFAULT_HTTP_LISTEN_ADDR,
|
||||||
DEFAULT_PARTIAL_BACKUP_TIMEOUT, DEFAULT_PG_LISTEN_ADDR,
|
DEFAULT_MAX_OFFLOADER_LAG_BYTES, DEFAULT_PARTIAL_BACKUP_TIMEOUT, DEFAULT_PG_LISTEN_ADDR,
|
||||||
};
|
};
|
||||||
use safekeeper::http;
|
use safekeeper::http;
|
||||||
use safekeeper::wal_service;
|
use safekeeper::wal_service;
|
||||||
@@ -172,6 +172,7 @@ struct Args {
|
|||||||
walsenders_keep_horizon: bool,
|
walsenders_keep_horizon: bool,
|
||||||
/// Enable partial backup. If disabled, safekeeper will not upload partial
|
/// Enable partial backup. If disabled, safekeeper will not upload partial
|
||||||
/// segments to remote storage.
|
/// segments to remote storage.
|
||||||
|
/// TODO: now partial backup is always enabled, remove this flag.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
partial_backup_enabled: bool,
|
partial_backup_enabled: bool,
|
||||||
/// Controls how long backup will wait until uploading the partial segment.
|
/// Controls how long backup will wait until uploading the partial segment.
|
||||||
@@ -181,6 +182,15 @@ struct Args {
|
|||||||
/// be used in tests.
|
/// be used in tests.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
disable_periodic_broker_push: bool,
|
disable_periodic_broker_push: bool,
|
||||||
|
/// Enable automatic switching to offloaded state.
|
||||||
|
#[arg(long)]
|
||||||
|
enable_offload: bool,
|
||||||
|
/// Delete local WAL files after offloading. When disabled, they will be left on disk.
|
||||||
|
#[arg(long)]
|
||||||
|
delete_offloaded_wal: bool,
|
||||||
|
/// Pending updates to control file will be automatically saved after this interval.
|
||||||
|
#[arg(long, value_parser = humantime::parse_duration, default_value = DEFAULT_CONTROL_FILE_SAVE_INTERVAL)]
|
||||||
|
control_file_save_interval: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Like PathBufValueParser, but allows empty string.
|
// Like PathBufValueParser, but allows empty string.
|
||||||
@@ -328,9 +338,12 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
sk_auth_token,
|
sk_auth_token,
|
||||||
current_thread_runtime: args.current_thread_runtime,
|
current_thread_runtime: args.current_thread_runtime,
|
||||||
walsenders_keep_horizon: args.walsenders_keep_horizon,
|
walsenders_keep_horizon: args.walsenders_keep_horizon,
|
||||||
partial_backup_enabled: args.partial_backup_enabled,
|
partial_backup_enabled: true,
|
||||||
partial_backup_timeout: args.partial_backup_timeout,
|
partial_backup_timeout: args.partial_backup_timeout,
|
||||||
disable_periodic_broker_push: args.disable_periodic_broker_push,
|
disable_periodic_broker_push: args.disable_periodic_broker_push,
|
||||||
|
enable_offload: args.enable_offload,
|
||||||
|
delete_offloaded_wal: args.delete_offloaded_wal,
|
||||||
|
control_file_save_interval: args.control_file_save_interval,
|
||||||
};
|
};
|
||||||
|
|
||||||
// initialize sentry if SENTRY_DSN is provided
|
// initialize sentry if SENTRY_DSN is provided
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ impl FileStorage {
|
|||||||
conf: &SafeKeeperConf,
|
conf: &SafeKeeperConf,
|
||||||
state: TimelinePersistentState,
|
state: TimelinePersistentState,
|
||||||
) -> Result<FileStorage> {
|
) -> Result<FileStorage> {
|
||||||
|
// we don't support creating new timelines in offloaded state
|
||||||
|
assert!(matches!(state.eviction_state, EvictionState::Present));
|
||||||
|
|
||||||
let store = FileStorage {
|
let store = FileStorage {
|
||||||
timeline_dir,
|
timeline_dir,
|
||||||
no_sync: conf.no_sync,
|
no_sync: conf.no_sync,
|
||||||
@@ -103,7 +106,7 @@ impl FileStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Load control file from given directory.
|
/// Load control file from given directory.
|
||||||
pub fn load_control_file_from_dir(timeline_dir: &Utf8Path) -> Result<TimelinePersistentState> {
|
fn load_control_file_from_dir(timeline_dir: &Utf8Path) -> Result<TimelinePersistentState> {
|
||||||
let path = timeline_dir.join(CONTROL_FILE_NAME);
|
let path = timeline_dir.join(CONTROL_FILE_NAME);
|
||||||
Self::load_control_file(path)
|
Self::load_control_file(path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use crate::{
|
|||||||
control_file::{FileStorage, Storage},
|
control_file::{FileStorage, Storage},
|
||||||
pull_timeline::{create_temp_timeline_dir, load_temp_timeline, validate_temp_timeline},
|
pull_timeline::{create_temp_timeline_dir, load_temp_timeline, validate_temp_timeline},
|
||||||
state::TimelinePersistentState,
|
state::TimelinePersistentState,
|
||||||
timeline::{FullAccessTimeline, Timeline, TimelineError},
|
timeline::{Timeline, TimelineError, WalResidentTimeline},
|
||||||
wal_backup::copy_s3_segments,
|
wal_backup::copy_s3_segments,
|
||||||
wal_storage::{wal_file_paths, WalReader},
|
wal_storage::{wal_file_paths, WalReader},
|
||||||
GlobalTimelines,
|
GlobalTimelines,
|
||||||
@@ -46,7 +46,7 @@ pub async fn handle_request(request: Request) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let source_tli = request.source.full_access_guard().await?;
|
let source_tli = request.source.wal_residence_guard().await?;
|
||||||
|
|
||||||
let conf = &GlobalTimelines::get_global_config();
|
let conf = &GlobalTimelines::get_global_config();
|
||||||
let ttid = request.destination_ttid;
|
let ttid = request.destination_ttid;
|
||||||
@@ -159,7 +159,7 @@ pub async fn handle_request(request: Request) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn copy_disk_segments(
|
async fn copy_disk_segments(
|
||||||
tli: &FullAccessTimeline,
|
tli: &WalResidentTimeline,
|
||||||
wal_seg_size: usize,
|
wal_seg_size: usize,
|
||||||
start_lsn: Lsn,
|
start_lsn: Lsn,
|
||||||
end_lsn: Lsn,
|
end_lsn: Lsn,
|
||||||
@@ -183,7 +183,7 @@ async fn copy_disk_segments(
|
|||||||
let copy_end = copy_end - segment_start;
|
let copy_end = copy_end - segment_start;
|
||||||
|
|
||||||
let wal_file_path = {
|
let wal_file_path = {
|
||||||
let (normal, partial) = wal_file_paths(tli_dir_path, segment, wal_seg_size)?;
|
let (normal, partial) = wal_file_paths(tli_dir_path, segment, wal_seg_size);
|
||||||
|
|
||||||
if segment == last_segment {
|
if segment == last_segment {
|
||||||
partial
|
partial
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ use crate::send_wal::WalSenderState;
|
|||||||
use crate::state::TimelineMemState;
|
use crate::state::TimelineMemState;
|
||||||
use crate::state::TimelinePersistentState;
|
use crate::state::TimelinePersistentState;
|
||||||
use crate::timeline::get_timeline_dir;
|
use crate::timeline::get_timeline_dir;
|
||||||
use crate::timeline::FullAccessTimeline;
|
use crate::timeline::WalResidentTimeline;
|
||||||
|
use crate::timeline_manager;
|
||||||
use crate::GlobalTimelines;
|
use crate::GlobalTimelines;
|
||||||
use crate::SafeKeeperConf;
|
use crate::SafeKeeperConf;
|
||||||
|
|
||||||
@@ -168,6 +169,7 @@ pub struct Memory {
|
|||||||
pub last_removed_segno: XLogSegNo,
|
pub last_removed_segno: XLogSegNo,
|
||||||
pub epoch_start_lsn: Lsn,
|
pub epoch_start_lsn: Lsn,
|
||||||
pub mem_state: TimelineMemState,
|
pub mem_state: TimelineMemState,
|
||||||
|
pub mgr_status: timeline_manager::Status,
|
||||||
|
|
||||||
// PhysicalStorage state.
|
// PhysicalStorage state.
|
||||||
pub write_lsn: Lsn,
|
pub write_lsn: Lsn,
|
||||||
@@ -326,7 +328,7 @@ pub struct TimelineDigest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn calculate_digest(
|
pub async fn calculate_digest(
|
||||||
tli: &FullAccessTimeline,
|
tli: &WalResidentTimeline,
|
||||||
request: TimelineDigestRequest,
|
request: TimelineDigestRequest,
|
||||||
) -> Result<TimelineDigest> {
|
) -> Result<TimelineDigest> {
|
||||||
if request.from_lsn > request.until_lsn {
|
if request.from_lsn > request.until_lsn {
|
||||||
|
|||||||
@@ -214,10 +214,10 @@ async fn timeline_snapshot_handler(request: Request<Body>) -> Result<Response<Bo
|
|||||||
let tli = GlobalTimelines::get(ttid).map_err(ApiError::from)?;
|
let tli = GlobalTimelines::get(ttid).map_err(ApiError::from)?;
|
||||||
// Note: with evicted timelines it should work better then de-evict them and
|
// Note: with evicted timelines it should work better then de-evict them and
|
||||||
// stream; probably start_snapshot would copy partial s3 file to dest path
|
// stream; probably start_snapshot would copy partial s3 file to dest path
|
||||||
// and stream control file, or return FullAccessTimeline if timeline is not
|
// and stream control file, or return WalResidentTimeline if timeline is not
|
||||||
// evicted.
|
// evicted.
|
||||||
let tli = tli
|
let tli = tli
|
||||||
.full_access_guard()
|
.wal_residence_guard()
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::InternalServerError)?;
|
.map_err(ApiError::InternalServerError)?;
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ async fn timeline_digest_handler(request: Request<Body>) -> Result<Response<Body
|
|||||||
|
|
||||||
let tli = GlobalTimelines::get(ttid).map_err(ApiError::from)?;
|
let tli = GlobalTimelines::get(ttid).map_err(ApiError::from)?;
|
||||||
let tli = tli
|
let tli = tli
|
||||||
.full_access_guard()
|
.wal_residence_guard()
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::InternalServerError)?;
|
.map_err(ApiError::InternalServerError)?;
|
||||||
|
|
||||||
@@ -306,7 +306,7 @@ async fn timeline_checkpoint_handler(request: Request<Body>) -> Result<Response<
|
|||||||
tli.write_shared_state()
|
tli.write_shared_state()
|
||||||
.await
|
.await
|
||||||
.sk
|
.sk
|
||||||
.state
|
.state_mut()
|
||||||
.flush()
|
.flush()
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::InternalServerError)?;
|
.map_err(ApiError::InternalServerError)?;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use crate::safekeeper::{
|
|||||||
};
|
};
|
||||||
use crate::safekeeper::{Term, TermHistory, TermLsn};
|
use crate::safekeeper::{Term, TermHistory, TermLsn};
|
||||||
use crate::state::TimelinePersistentState;
|
use crate::state::TimelinePersistentState;
|
||||||
use crate::timeline::FullAccessTimeline;
|
use crate::timeline::WalResidentTimeline;
|
||||||
use crate::GlobalTimelines;
|
use crate::GlobalTimelines;
|
||||||
use postgres_backend::PostgresBackend;
|
use postgres_backend::PostgresBackend;
|
||||||
use postgres_ffi::encode_logical_message;
|
use postgres_ffi::encode_logical_message;
|
||||||
@@ -102,7 +102,7 @@ pub async fn handle_json_ctrl<IO: AsyncRead + AsyncWrite + Unpin>(
|
|||||||
async fn prepare_safekeeper(
|
async fn prepare_safekeeper(
|
||||||
ttid: TenantTimelineId,
|
ttid: TenantTimelineId,
|
||||||
pg_version: u32,
|
pg_version: u32,
|
||||||
) -> anyhow::Result<FullAccessTimeline> {
|
) -> anyhow::Result<WalResidentTimeline> {
|
||||||
let tli = GlobalTimelines::create(
|
let tli = GlobalTimelines::create(
|
||||||
ttid,
|
ttid,
|
||||||
ServerInfo {
|
ServerInfo {
|
||||||
@@ -115,11 +115,11 @@ async fn prepare_safekeeper(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tli.full_access_guard().await
|
tli.wal_residence_guard().await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_proposer_elected(
|
async fn send_proposer_elected(
|
||||||
tli: &FullAccessTimeline,
|
tli: &WalResidentTimeline,
|
||||||
term: Term,
|
term: Term,
|
||||||
lsn: Lsn,
|
lsn: Lsn,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
@@ -151,7 +151,7 @@ pub struct InsertedWAL {
|
|||||||
/// Extend local WAL with new LogicalMessage record. To do that,
|
/// Extend local WAL with new LogicalMessage record. To do that,
|
||||||
/// create AppendRequest with new WAL and pass it to safekeeper.
|
/// create AppendRequest with new WAL and pass it to safekeeper.
|
||||||
pub async fn append_logical_message(
|
pub async fn append_logical_message(
|
||||||
tli: &FullAccessTimeline,
|
tli: &WalResidentTimeline,
|
||||||
msg: &AppendLogicalMessage,
|
msg: &AppendLogicalMessage,
|
||||||
) -> anyhow::Result<InsertedWAL> {
|
) -> anyhow::Result<InsertedWAL> {
|
||||||
let wal_data = encode_logical_message(&msg.lm_prefix, &msg.lm_message);
|
let wal_data = encode_logical_message(&msg.lm_prefix, &msg.lm_message);
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ pub mod safekeeper;
|
|||||||
pub mod send_wal;
|
pub mod send_wal;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod timeline;
|
pub mod timeline;
|
||||||
|
pub mod timeline_eviction;
|
||||||
|
pub mod timeline_guard;
|
||||||
pub mod timeline_manager;
|
pub mod timeline_manager;
|
||||||
pub mod timelines_set;
|
pub mod timelines_set;
|
||||||
pub mod wal_backup;
|
pub mod wal_backup;
|
||||||
@@ -49,6 +51,7 @@ pub mod defaults {
|
|||||||
pub const DEFAULT_HEARTBEAT_TIMEOUT: &str = "5000ms";
|
pub const DEFAULT_HEARTBEAT_TIMEOUT: &str = "5000ms";
|
||||||
pub const DEFAULT_MAX_OFFLOADER_LAG_BYTES: u64 = 128 * (1 << 20);
|
pub const DEFAULT_MAX_OFFLOADER_LAG_BYTES: u64 = 128 * (1 << 20);
|
||||||
pub const DEFAULT_PARTIAL_BACKUP_TIMEOUT: &str = "15m";
|
pub const DEFAULT_PARTIAL_BACKUP_TIMEOUT: &str = "15m";
|
||||||
|
pub const DEFAULT_CONTROL_FILE_SAVE_INTERVAL: &str = "300s";
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -85,6 +88,9 @@ pub struct SafeKeeperConf {
|
|||||||
pub partial_backup_enabled: bool,
|
pub partial_backup_enabled: bool,
|
||||||
pub partial_backup_timeout: Duration,
|
pub partial_backup_timeout: Duration,
|
||||||
pub disable_periodic_broker_push: bool,
|
pub disable_periodic_broker_push: bool,
|
||||||
|
pub enable_offload: bool,
|
||||||
|
pub delete_offloaded_wal: bool,
|
||||||
|
pub control_file_save_interval: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SafeKeeperConf {
|
impl SafeKeeperConf {
|
||||||
@@ -124,6 +130,9 @@ impl SafeKeeperConf {
|
|||||||
partial_backup_enabled: false,
|
partial_backup_enabled: false,
|
||||||
partial_backup_timeout: Duration::from_secs(0),
|
partial_backup_timeout: Duration::from_secs(0),
|
||||||
disable_periodic_broker_push: false,
|
disable_periodic_broker_push: false,
|
||||||
|
enable_offload: false,
|
||||||
|
delete_offloaded_wal: false,
|
||||||
|
control_file_save_interval: Duration::from_secs(1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ use std::{
|
|||||||
time::{Instant, SystemTime},
|
time::{Instant, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
use ::metrics::{register_histogram, GaugeVec, Histogram, IntGauge, DISK_WRITE_SECONDS_BUCKETS};
|
use ::metrics::{register_histogram, GaugeVec, Histogram, IntGauge, DISK_FSYNC_SECONDS_BUCKETS};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use metrics::{
|
use metrics::{
|
||||||
core::{AtomicU64, Collector, Desc, GenericCounter, GenericGaugeVec, Opts},
|
core::{AtomicU64, Collector, Desc, GenericCounter, GenericGaugeVec, Opts},
|
||||||
proto::MetricFamily,
|
proto::MetricFamily,
|
||||||
register_int_counter, register_int_counter_pair, register_int_counter_pair_vec,
|
register_histogram_vec, register_int_counter, register_int_counter_pair,
|
||||||
register_int_counter_vec, Gauge, IntCounter, IntCounterPair, IntCounterPairVec, IntCounterVec,
|
register_int_counter_pair_vec, register_int_counter_vec, Gauge, HistogramVec, IntCounter,
|
||||||
IntGaugeVec,
|
IntCounterPair, IntCounterPairVec, IntCounterVec, IntGaugeVec,
|
||||||
};
|
};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ pub static WRITE_WAL_SECONDS: Lazy<Histogram> = Lazy::new(|| {
|
|||||||
register_histogram!(
|
register_histogram!(
|
||||||
"safekeeper_write_wal_seconds",
|
"safekeeper_write_wal_seconds",
|
||||||
"Seconds spent writing and syncing WAL to a disk in a single request",
|
"Seconds spent writing and syncing WAL to a disk in a single request",
|
||||||
DISK_WRITE_SECONDS_BUCKETS.to_vec()
|
DISK_FSYNC_SECONDS_BUCKETS.to_vec()
|
||||||
)
|
)
|
||||||
.expect("Failed to register safekeeper_write_wal_seconds histogram")
|
.expect("Failed to register safekeeper_write_wal_seconds histogram")
|
||||||
});
|
});
|
||||||
@@ -56,7 +56,7 @@ pub static FLUSH_WAL_SECONDS: Lazy<Histogram> = Lazy::new(|| {
|
|||||||
register_histogram!(
|
register_histogram!(
|
||||||
"safekeeper_flush_wal_seconds",
|
"safekeeper_flush_wal_seconds",
|
||||||
"Seconds spent syncing WAL to a disk",
|
"Seconds spent syncing WAL to a disk",
|
||||||
DISK_WRITE_SECONDS_BUCKETS.to_vec()
|
DISK_FSYNC_SECONDS_BUCKETS.to_vec()
|
||||||
)
|
)
|
||||||
.expect("Failed to register safekeeper_flush_wal_seconds histogram")
|
.expect("Failed to register safekeeper_flush_wal_seconds histogram")
|
||||||
});
|
});
|
||||||
@@ -64,10 +64,26 @@ pub static PERSIST_CONTROL_FILE_SECONDS: Lazy<Histogram> = Lazy::new(|| {
|
|||||||
register_histogram!(
|
register_histogram!(
|
||||||
"safekeeper_persist_control_file_seconds",
|
"safekeeper_persist_control_file_seconds",
|
||||||
"Seconds to persist and sync control file",
|
"Seconds to persist and sync control file",
|
||||||
DISK_WRITE_SECONDS_BUCKETS.to_vec()
|
DISK_FSYNC_SECONDS_BUCKETS.to_vec()
|
||||||
)
|
)
|
||||||
.expect("Failed to register safekeeper_persist_control_file_seconds histogram vec")
|
.expect("Failed to register safekeeper_persist_control_file_seconds histogram vec")
|
||||||
});
|
});
|
||||||
|
pub static WAL_STORAGE_OPERATION_SECONDS: Lazy<HistogramVec> = Lazy::new(|| {
|
||||||
|
register_histogram_vec!(
|
||||||
|
"safekeeper_wal_storage_operation_seconds",
|
||||||
|
"Seconds spent on WAL storage operations",
|
||||||
|
&["operation"]
|
||||||
|
)
|
||||||
|
.expect("Failed to register safekeeper_wal_storage_operation_seconds histogram vec")
|
||||||
|
});
|
||||||
|
pub static MISC_OPERATION_SECONDS: Lazy<HistogramVec> = Lazy::new(|| {
|
||||||
|
register_histogram_vec!(
|
||||||
|
"safekeeper_misc_operation_seconds",
|
||||||
|
"Seconds spent on miscellaneous operations",
|
||||||
|
&["operation"]
|
||||||
|
)
|
||||||
|
.expect("Failed to register safekeeper_misc_operation_seconds histogram vec")
|
||||||
|
});
|
||||||
pub static PG_IO_BYTES: Lazy<IntCounterVec> = Lazy::new(|| {
|
pub static PG_IO_BYTES: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||||
register_int_counter_vec!(
|
register_int_counter_vec!(
|
||||||
"safekeeper_pg_io_bytes_total",
|
"safekeeper_pg_io_bytes_total",
|
||||||
@@ -126,7 +142,7 @@ pub static BROKER_PUSH_ALL_UPDATES_SECONDS: Lazy<Histogram> = Lazy::new(|| {
|
|||||||
register_histogram!(
|
register_histogram!(
|
||||||
"safekeeper_broker_push_update_seconds",
|
"safekeeper_broker_push_update_seconds",
|
||||||
"Seconds to push all timeline updates to the broker",
|
"Seconds to push all timeline updates to the broker",
|
||||||
DISK_WRITE_SECONDS_BUCKETS.to_vec()
|
DISK_FSYNC_SECONDS_BUCKETS.to_vec()
|
||||||
)
|
)
|
||||||
.expect("Failed to register safekeeper_broker_push_update_seconds histogram vec")
|
.expect("Failed to register safekeeper_broker_push_update_seconds histogram vec")
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ use crate::{
|
|||||||
routes::TimelineStatus,
|
routes::TimelineStatus,
|
||||||
},
|
},
|
||||||
safekeeper::Term,
|
safekeeper::Term,
|
||||||
timeline::{get_tenant_dir, get_timeline_dir, FullAccessTimeline, Timeline, TimelineError},
|
timeline::{get_tenant_dir, get_timeline_dir, Timeline, TimelineError, WalResidentTimeline},
|
||||||
wal_storage::{self, open_wal_file, Storage},
|
wal_storage::{self, open_wal_file, Storage},
|
||||||
GlobalTimelines, SafeKeeperConf,
|
GlobalTimelines, SafeKeeperConf,
|
||||||
};
|
};
|
||||||
@@ -46,7 +46,7 @@ use utils::{
|
|||||||
|
|
||||||
/// Stream tar archive of timeline to tx.
|
/// Stream tar archive of timeline to tx.
|
||||||
#[instrument(name = "snapshot", skip_all, fields(ttid = %tli.ttid))]
|
#[instrument(name = "snapshot", skip_all, fields(ttid = %tli.ttid))]
|
||||||
pub async fn stream_snapshot(tli: FullAccessTimeline, tx: mpsc::Sender<Result<Bytes>>) {
|
pub async fn stream_snapshot(tli: WalResidentTimeline, tx: mpsc::Sender<Result<Bytes>>) {
|
||||||
if let Err(e) = stream_snapshot_guts(tli, tx.clone()).await {
|
if let Err(e) = stream_snapshot_guts(tli, tx.clone()).await {
|
||||||
// Error type/contents don't matter as they won't can't reach the client
|
// Error type/contents don't matter as they won't can't reach the client
|
||||||
// (hyper likely doesn't do anything with it), but http stream will be
|
// (hyper likely doesn't do anything with it), but http stream will be
|
||||||
@@ -66,7 +66,7 @@ pub struct SnapshotContext {
|
|||||||
pub flush_lsn: Lsn,
|
pub flush_lsn: Lsn,
|
||||||
pub wal_seg_size: usize,
|
pub wal_seg_size: usize,
|
||||||
// used to remove WAL hold off in Drop.
|
// used to remove WAL hold off in Drop.
|
||||||
pub tli: FullAccessTimeline,
|
pub tli: WalResidentTimeline,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for SnapshotContext {
|
impl Drop for SnapshotContext {
|
||||||
@@ -80,7 +80,7 @@ impl Drop for SnapshotContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stream_snapshot_guts(
|
pub async fn stream_snapshot_guts(
|
||||||
tli: FullAccessTimeline,
|
tli: WalResidentTimeline,
|
||||||
tx: mpsc::Sender<Result<Bytes>>,
|
tx: mpsc::Sender<Result<Bytes>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// tokio-tar wants Write implementor, but we have mpsc tx <Result<Bytes>>;
|
// tokio-tar wants Write implementor, but we have mpsc tx <Result<Bytes>>;
|
||||||
@@ -135,7 +135,7 @@ pub async fn stream_snapshot_guts(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FullAccessTimeline {
|
impl WalResidentTimeline {
|
||||||
/// Start streaming tar archive with timeline:
|
/// Start streaming tar archive with timeline:
|
||||||
/// 1) stream control file under lock;
|
/// 1) stream control file under lock;
|
||||||
/// 2) hold off WAL removal;
|
/// 2) hold off WAL removal;
|
||||||
@@ -160,6 +160,7 @@ impl FullAccessTimeline {
|
|||||||
ar: &mut tokio_tar::Builder<W>,
|
ar: &mut tokio_tar::Builder<W>,
|
||||||
) -> Result<SnapshotContext> {
|
) -> Result<SnapshotContext> {
|
||||||
let mut shared_state = self.write_shared_state().await;
|
let mut shared_state = self.write_shared_state().await;
|
||||||
|
let wal_seg_size = shared_state.get_wal_seg_size();
|
||||||
|
|
||||||
let cf_path = self.get_timeline_dir().join(CONTROL_FILE_NAME);
|
let cf_path = self.get_timeline_dir().join(CONTROL_FILE_NAME);
|
||||||
let mut cf = File::open(cf_path).await?;
|
let mut cf = File::open(cf_path).await?;
|
||||||
@@ -173,19 +174,19 @@ impl FullAccessTimeline {
|
|||||||
// lock and setting `wal_removal_on_hold` later, it guarantees that WAL
|
// lock and setting `wal_removal_on_hold` later, it guarantees that WAL
|
||||||
// won't be removed until we're done.
|
// won't be removed until we're done.
|
||||||
let from_lsn = min(
|
let from_lsn = min(
|
||||||
shared_state.sk.state.remote_consistent_lsn,
|
shared_state.sk.state().remote_consistent_lsn,
|
||||||
shared_state.sk.state.backup_lsn,
|
shared_state.sk.state().backup_lsn,
|
||||||
);
|
);
|
||||||
if from_lsn == Lsn::INVALID {
|
if from_lsn == Lsn::INVALID {
|
||||||
// this is possible if snapshot is called before handling first
|
// this is possible if snapshot is called before handling first
|
||||||
// elected message
|
// elected message
|
||||||
bail!("snapshot is called on uninitialized timeline");
|
bail!("snapshot is called on uninitialized timeline");
|
||||||
}
|
}
|
||||||
let from_segno = from_lsn.segment_number(shared_state.get_wal_seg_size());
|
let from_segno = from_lsn.segment_number(wal_seg_size);
|
||||||
let term = shared_state.sk.get_term();
|
let term = shared_state.sk.state().acceptor_state.term;
|
||||||
let last_log_term = shared_state.sk.get_last_log_term();
|
let last_log_term = shared_state.sk.last_log_term();
|
||||||
let flush_lsn = shared_state.sk.flush_lsn();
|
let flush_lsn = shared_state.sk.flush_lsn();
|
||||||
let upto_segno = flush_lsn.segment_number(shared_state.get_wal_seg_size());
|
let upto_segno = flush_lsn.segment_number(wal_seg_size);
|
||||||
// have some limit on max number of segments as a sanity check
|
// have some limit on max number of segments as a sanity check
|
||||||
const MAX_ALLOWED_SEGS: u64 = 1000;
|
const MAX_ALLOWED_SEGS: u64 = 1000;
|
||||||
let num_segs = upto_segno - from_segno + 1;
|
let num_segs = upto_segno - from_segno + 1;
|
||||||
@@ -206,14 +207,18 @@ impl FullAccessTimeline {
|
|||||||
}
|
}
|
||||||
shared_state.wal_removal_on_hold = true;
|
shared_state.wal_removal_on_hold = true;
|
||||||
|
|
||||||
|
// Drop shared_state to release the lock, before calling wal_residence_guard().
|
||||||
|
drop(shared_state);
|
||||||
|
|
||||||
|
let tli_copy = self.wal_residence_guard().await?;
|
||||||
let bctx = SnapshotContext {
|
let bctx = SnapshotContext {
|
||||||
from_segno,
|
from_segno,
|
||||||
upto_segno,
|
upto_segno,
|
||||||
term,
|
term,
|
||||||
last_log_term,
|
last_log_term,
|
||||||
flush_lsn,
|
flush_lsn,
|
||||||
wal_seg_size: shared_state.get_wal_seg_size(),
|
wal_seg_size,
|
||||||
tli: self.clone(),
|
tli: tli_copy,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(bctx)
|
Ok(bctx)
|
||||||
@@ -225,8 +230,8 @@ impl FullAccessTimeline {
|
|||||||
/// forget this if snapshotting fails mid the way.
|
/// forget this if snapshotting fails mid the way.
|
||||||
pub async fn finish_snapshot(&self, bctx: &SnapshotContext) -> Result<()> {
|
pub async fn finish_snapshot(&self, bctx: &SnapshotContext) -> Result<()> {
|
||||||
let shared_state = self.read_shared_state().await;
|
let shared_state = self.read_shared_state().await;
|
||||||
let term = shared_state.sk.get_term();
|
let term = shared_state.sk.state().acceptor_state.term;
|
||||||
let last_log_term = shared_state.sk.get_last_log_term();
|
let last_log_term = shared_state.sk.last_log_term();
|
||||||
// There are some cases to relax this check (e.g. last_log_term might
|
// There are some cases to relax this check (e.g. last_log_term might
|
||||||
// change, but as long as older history is strictly part of new that's
|
// change, but as long as older history is strictly part of new that's
|
||||||
// fine), but there is no need to do it.
|
// fine), but there is no need to do it.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::handler::SafekeeperPostgresHandler;
|
|||||||
use crate::safekeeper::AcceptorProposerMessage;
|
use crate::safekeeper::AcceptorProposerMessage;
|
||||||
use crate::safekeeper::ProposerAcceptorMessage;
|
use crate::safekeeper::ProposerAcceptorMessage;
|
||||||
use crate::safekeeper::ServerInfo;
|
use crate::safekeeper::ServerInfo;
|
||||||
use crate::timeline::FullAccessTimeline;
|
use crate::timeline::WalResidentTimeline;
|
||||||
use crate::wal_service::ConnectionId;
|
use crate::wal_service::ConnectionId;
|
||||||
use crate::GlobalTimelines;
|
use crate::GlobalTimelines;
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
@@ -213,7 +213,7 @@ impl SafekeeperPostgresHandler {
|
|||||||
&mut self,
|
&mut self,
|
||||||
pgb: &mut PostgresBackend<IO>,
|
pgb: &mut PostgresBackend<IO>,
|
||||||
) -> Result<(), QueryError> {
|
) -> Result<(), QueryError> {
|
||||||
let mut tli: Option<FullAccessTimeline> = None;
|
let mut tli: Option<WalResidentTimeline> = None;
|
||||||
if let Err(end) = self.handle_start_wal_push_guts(pgb, &mut tli).await {
|
if let Err(end) = self.handle_start_wal_push_guts(pgb, &mut tli).await {
|
||||||
// Log the result and probably send it to the client, closing the stream.
|
// Log the result and probably send it to the client, closing the stream.
|
||||||
let handle_end_fut = pgb.handle_copy_stream_end(end);
|
let handle_end_fut = pgb.handle_copy_stream_end(end);
|
||||||
@@ -233,7 +233,7 @@ impl SafekeeperPostgresHandler {
|
|||||||
pub async fn handle_start_wal_push_guts<IO: AsyncRead + AsyncWrite + Unpin>(
|
pub async fn handle_start_wal_push_guts<IO: AsyncRead + AsyncWrite + Unpin>(
|
||||||
&mut self,
|
&mut self,
|
||||||
pgb: &mut PostgresBackend<IO>,
|
pgb: &mut PostgresBackend<IO>,
|
||||||
tli: &mut Option<FullAccessTimeline>,
|
tli: &mut Option<WalResidentTimeline>,
|
||||||
) -> Result<(), CopyStreamHandlerEnd> {
|
) -> Result<(), CopyStreamHandlerEnd> {
|
||||||
// Notify the libpq client that it's allowed to send `CopyData` messages
|
// Notify the libpq client that it's allowed to send `CopyData` messages
|
||||||
pgb.write_message(&BeMessage::CopyBothResponse).await?;
|
pgb.write_message(&BeMessage::CopyBothResponse).await?;
|
||||||
@@ -269,11 +269,11 @@ impl SafekeeperPostgresHandler {
|
|||||||
.get_walreceivers()
|
.get_walreceivers()
|
||||||
.pageserver_feedback_tx
|
.pageserver_feedback_tx
|
||||||
.subscribe();
|
.subscribe();
|
||||||
*tli = Some(timeline.clone());
|
*tli = Some(timeline.wal_residence_guard().await?);
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
// todo: add read|write .context to these errors
|
// todo: add read|write .context to these errors
|
||||||
r = network_reader.run(msg_tx, msg_rx, reply_tx, timeline.clone(), next_msg) => r,
|
r = network_reader.run(msg_tx, msg_rx, reply_tx, timeline, next_msg) => r,
|
||||||
r = network_write(pgb, reply_rx, pageserver_feedback_rx) => r,
|
r = network_write(pgb, reply_rx, pageserver_feedback_rx) => r,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -323,7 +323,7 @@ struct NetworkReader<'a, IO> {
|
|||||||
impl<'a, IO: AsyncRead + AsyncWrite + Unpin> NetworkReader<'a, IO> {
|
impl<'a, IO: AsyncRead + AsyncWrite + Unpin> NetworkReader<'a, IO> {
|
||||||
async fn read_first_message(
|
async fn read_first_message(
|
||||||
&mut self,
|
&mut self,
|
||||||
) -> Result<(FullAccessTimeline, ProposerAcceptorMessage), CopyStreamHandlerEnd> {
|
) -> Result<(WalResidentTimeline, ProposerAcceptorMessage), CopyStreamHandlerEnd> {
|
||||||
// Receive information about server to create timeline, if not yet.
|
// Receive information about server to create timeline, if not yet.
|
||||||
let next_msg = read_message(self.pgb_reader).await?;
|
let next_msg = read_message(self.pgb_reader).await?;
|
||||||
let tli = match next_msg {
|
let tli = match next_msg {
|
||||||
@@ -340,7 +340,7 @@ impl<'a, IO: AsyncRead + AsyncWrite + Unpin> NetworkReader<'a, IO> {
|
|||||||
let tli =
|
let tli =
|
||||||
GlobalTimelines::create(self.ttid, server_info, Lsn::INVALID, Lsn::INVALID)
|
GlobalTimelines::create(self.ttid, server_info, Lsn::INVALID, Lsn::INVALID)
|
||||||
.await?;
|
.await?;
|
||||||
tli.full_access_guard().await?
|
tli.wal_residence_guard().await?
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(CopyStreamHandlerEnd::Other(anyhow::anyhow!(
|
return Err(CopyStreamHandlerEnd::Other(anyhow::anyhow!(
|
||||||
@@ -356,7 +356,7 @@ impl<'a, IO: AsyncRead + AsyncWrite + Unpin> NetworkReader<'a, IO> {
|
|||||||
msg_tx: Sender<ProposerAcceptorMessage>,
|
msg_tx: Sender<ProposerAcceptorMessage>,
|
||||||
msg_rx: Receiver<ProposerAcceptorMessage>,
|
msg_rx: Receiver<ProposerAcceptorMessage>,
|
||||||
reply_tx: Sender<AcceptorProposerMessage>,
|
reply_tx: Sender<AcceptorProposerMessage>,
|
||||||
tli: FullAccessTimeline,
|
tli: WalResidentTimeline,
|
||||||
next_msg: ProposerAcceptorMessage,
|
next_msg: ProposerAcceptorMessage,
|
||||||
) -> Result<(), CopyStreamHandlerEnd> {
|
) -> Result<(), CopyStreamHandlerEnd> {
|
||||||
*self.acceptor_handle = Some(WalAcceptor::spawn(
|
*self.acceptor_handle = Some(WalAcceptor::spawn(
|
||||||
@@ -451,7 +451,7 @@ const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
|
|||||||
/// replies to reply_tx; reading from socket and writing to disk in parallel is
|
/// replies to reply_tx; reading from socket and writing to disk in parallel is
|
||||||
/// beneficial for performance, this struct provides writing to disk part.
|
/// beneficial for performance, this struct provides writing to disk part.
|
||||||
pub struct WalAcceptor {
|
pub struct WalAcceptor {
|
||||||
tli: FullAccessTimeline,
|
tli: WalResidentTimeline,
|
||||||
msg_rx: Receiver<ProposerAcceptorMessage>,
|
msg_rx: Receiver<ProposerAcceptorMessage>,
|
||||||
reply_tx: Sender<AcceptorProposerMessage>,
|
reply_tx: Sender<AcceptorProposerMessage>,
|
||||||
conn_id: Option<ConnectionId>,
|
conn_id: Option<ConnectionId>,
|
||||||
@@ -464,7 +464,7 @@ impl WalAcceptor {
|
|||||||
///
|
///
|
||||||
/// conn_id None means WalAcceptor is used by recovery initiated at this safekeeper.
|
/// conn_id None means WalAcceptor is used by recovery initiated at this safekeeper.
|
||||||
pub fn spawn(
|
pub fn spawn(
|
||||||
tli: FullAccessTimeline,
|
tli: WalResidentTimeline,
|
||||||
msg_rx: Receiver<ProposerAcceptorMessage>,
|
msg_rx: Receiver<ProposerAcceptorMessage>,
|
||||||
reply_tx: Sender<AcceptorProposerMessage>,
|
reply_tx: Sender<AcceptorProposerMessage>,
|
||||||
conn_id: Option<ConnectionId>,
|
conn_id: Option<ConnectionId>,
|
||||||
|
|||||||
+17
-12
@@ -21,7 +21,7 @@ use utils::{id::NodeId, lsn::Lsn, postgres_client::wal_stream_connection_config}
|
|||||||
|
|
||||||
use crate::receive_wal::{WalAcceptor, REPLY_QUEUE_SIZE};
|
use crate::receive_wal::{WalAcceptor, REPLY_QUEUE_SIZE};
|
||||||
use crate::safekeeper::{AppendRequest, AppendRequestHeader};
|
use crate::safekeeper::{AppendRequest, AppendRequestHeader};
|
||||||
use crate::timeline::FullAccessTimeline;
|
use crate::timeline::WalResidentTimeline;
|
||||||
use crate::{
|
use crate::{
|
||||||
http::routes::TimelineStatus,
|
http::routes::TimelineStatus,
|
||||||
receive_wal::MSG_QUEUE_SIZE,
|
receive_wal::MSG_QUEUE_SIZE,
|
||||||
@@ -36,7 +36,7 @@ use crate::{
|
|||||||
/// Entrypoint for per timeline task which always runs, checking whether
|
/// Entrypoint for per timeline task which always runs, checking whether
|
||||||
/// recovery for this safekeeper is needed and starting it if so.
|
/// recovery for this safekeeper is needed and starting it if so.
|
||||||
#[instrument(name = "recovery task", skip_all, fields(ttid = %tli.ttid))]
|
#[instrument(name = "recovery task", skip_all, fields(ttid = %tli.ttid))]
|
||||||
pub async fn recovery_main(tli: FullAccessTimeline, conf: SafeKeeperConf) {
|
pub async fn recovery_main(tli: WalResidentTimeline, conf: SafeKeeperConf) {
|
||||||
info!("started");
|
info!("started");
|
||||||
|
|
||||||
let cancel = tli.cancel.clone();
|
let cancel = tli.cancel.clone();
|
||||||
@@ -66,12 +66,12 @@ pub async fn recovery_main(tli: FullAccessTimeline, conf: SafeKeeperConf) {
|
|||||||
/// depending on assembled quorum (e.g. classic picture 8 from Raft paper).
|
/// depending on assembled quorum (e.g. classic picture 8 from Raft paper).
|
||||||
/// Thus we don't try to predict it here.
|
/// Thus we don't try to predict it here.
|
||||||
async fn recovery_needed(
|
async fn recovery_needed(
|
||||||
tli: &FullAccessTimeline,
|
tli: &WalResidentTimeline,
|
||||||
heartbeat_timeout: Duration,
|
heartbeat_timeout: Duration,
|
||||||
) -> RecoveryNeededInfo {
|
) -> RecoveryNeededInfo {
|
||||||
let ss = tli.read_shared_state().await;
|
let ss = tli.read_shared_state().await;
|
||||||
let term = ss.sk.state.acceptor_state.term;
|
let term = ss.sk.state().acceptor_state.term;
|
||||||
let last_log_term = ss.sk.get_last_log_term();
|
let last_log_term = ss.sk.last_log_term();
|
||||||
let flush_lsn = ss.sk.flush_lsn();
|
let flush_lsn = ss.sk.flush_lsn();
|
||||||
// note that peers contain myself, but that's ok -- we are interested only in peers which are strictly ahead of us.
|
// note that peers contain myself, but that's ok -- we are interested only in peers which are strictly ahead of us.
|
||||||
let mut peers = ss.get_peers(heartbeat_timeout);
|
let mut peers = ss.get_peers(heartbeat_timeout);
|
||||||
@@ -195,7 +195,7 @@ impl From<&PeerInfo> for Donor {
|
|||||||
const CHECK_INTERVAL_MS: u64 = 2000;
|
const CHECK_INTERVAL_MS: u64 = 2000;
|
||||||
|
|
||||||
/// Check regularly whether we need to start recovery.
|
/// Check regularly whether we need to start recovery.
|
||||||
async fn recovery_main_loop(tli: FullAccessTimeline, conf: SafeKeeperConf) {
|
async fn recovery_main_loop(tli: WalResidentTimeline, conf: SafeKeeperConf) {
|
||||||
let check_duration = Duration::from_millis(CHECK_INTERVAL_MS);
|
let check_duration = Duration::from_millis(CHECK_INTERVAL_MS);
|
||||||
loop {
|
loop {
|
||||||
let recovery_needed_info = recovery_needed(&tli, conf.heartbeat_timeout).await;
|
let recovery_needed_info = recovery_needed(&tli, conf.heartbeat_timeout).await;
|
||||||
@@ -205,7 +205,12 @@ async fn recovery_main_loop(tli: FullAccessTimeline, conf: SafeKeeperConf) {
|
|||||||
"starting recovery from donor {}: {}",
|
"starting recovery from donor {}: {}",
|
||||||
donor.sk_id, recovery_needed_info
|
donor.sk_id, recovery_needed_info
|
||||||
);
|
);
|
||||||
match recover(tli.clone(), donor, &conf).await {
|
let res = tli.wal_residence_guard().await;
|
||||||
|
if let Err(e) = res {
|
||||||
|
warn!("failed to obtain guard: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match recover(res.unwrap(), donor, &conf).await {
|
||||||
// Note: 'write_wal rewrites WAL written before' error is
|
// Note: 'write_wal rewrites WAL written before' error is
|
||||||
// expected here and might happen if compute and recovery
|
// expected here and might happen if compute and recovery
|
||||||
// concurrently write the same data. Eventually compute
|
// concurrently write the same data. Eventually compute
|
||||||
@@ -228,7 +233,7 @@ async fn recovery_main_loop(tli: FullAccessTimeline, conf: SafeKeeperConf) {
|
|||||||
/// Recover from the specified donor. Returns message explaining normal finish
|
/// Recover from the specified donor. Returns message explaining normal finish
|
||||||
/// reason or error.
|
/// reason or error.
|
||||||
async fn recover(
|
async fn recover(
|
||||||
tli: FullAccessTimeline,
|
tli: WalResidentTimeline,
|
||||||
donor: &Donor,
|
donor: &Donor,
|
||||||
conf: &SafeKeeperConf,
|
conf: &SafeKeeperConf,
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
@@ -314,7 +319,7 @@ async fn recover(
|
|||||||
|
|
||||||
// Pull WAL from donor, assuming handshake is already done.
|
// Pull WAL from donor, assuming handshake is already done.
|
||||||
async fn recovery_stream(
|
async fn recovery_stream(
|
||||||
tli: FullAccessTimeline,
|
tli: WalResidentTimeline,
|
||||||
donor: &Donor,
|
donor: &Donor,
|
||||||
start_streaming_at: Lsn,
|
start_streaming_at: Lsn,
|
||||||
conf: &SafeKeeperConf,
|
conf: &SafeKeeperConf,
|
||||||
@@ -364,10 +369,10 @@ async fn recovery_stream(
|
|||||||
// As in normal walreceiver, do networking and writing to disk in parallel.
|
// As in normal walreceiver, do networking and writing to disk in parallel.
|
||||||
let (msg_tx, msg_rx) = channel(MSG_QUEUE_SIZE);
|
let (msg_tx, msg_rx) = channel(MSG_QUEUE_SIZE);
|
||||||
let (reply_tx, reply_rx) = channel(REPLY_QUEUE_SIZE);
|
let (reply_tx, reply_rx) = channel(REPLY_QUEUE_SIZE);
|
||||||
let wa = WalAcceptor::spawn(tli.clone(), msg_rx, reply_tx, None);
|
let wa = WalAcceptor::spawn(tli.wal_residence_guard().await?, msg_rx, reply_tx, None);
|
||||||
|
|
||||||
let res = tokio::select! {
|
let res = tokio::select! {
|
||||||
r = network_io(physical_stream, msg_tx, donor.clone(), tli.clone(), conf.clone()) => r,
|
r = network_io(physical_stream, msg_tx, donor.clone(), tli, conf.clone()) => r,
|
||||||
r = read_replies(reply_rx, donor.term) => r.map(|()| None),
|
r = read_replies(reply_rx, donor.term) => r.map(|()| None),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -398,7 +403,7 @@ async fn network_io(
|
|||||||
physical_stream: ReplicationStream,
|
physical_stream: ReplicationStream,
|
||||||
msg_tx: Sender<ProposerAcceptorMessage>,
|
msg_tx: Sender<ProposerAcceptorMessage>,
|
||||||
donor: Donor,
|
donor: Donor,
|
||||||
tli: FullAccessTimeline,
|
tli: WalResidentTimeline,
|
||||||
conf: SafeKeeperConf,
|
conf: SafeKeeperConf,
|
||||||
) -> anyhow::Result<Option<String>> {
|
) -> anyhow::Result<Option<String>> {
|
||||||
let mut physical_stream = pin!(physical_stream);
|
let mut physical_stream = pin!(physical_stream);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::timeline_manager::StateSnapshot;
|
|||||||
/// While it is safe to use inmem values for determining horizon,
|
/// While it is safe to use inmem values for determining horizon,
|
||||||
/// we use persistent to make possible normal states less surprising.
|
/// we use persistent to make possible normal states less surprising.
|
||||||
/// All segments covering LSNs before horizon_lsn can be removed.
|
/// All segments covering LSNs before horizon_lsn can be removed.
|
||||||
pub fn calc_horizon_lsn(state: &StateSnapshot, extra_horizon_lsn: Option<Lsn>) -> Lsn {
|
pub(crate) fn calc_horizon_lsn(state: &StateSnapshot, extra_horizon_lsn: Option<Lsn>) -> Lsn {
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
|
|
||||||
let mut horizon_lsn = min(
|
let mut horizon_lsn = min(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use storage_broker::proto::SafekeeperTimelineInfo;
|
|||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
use crate::control_file;
|
use crate::control_file;
|
||||||
|
use crate::metrics::MISC_OPERATION_SECONDS;
|
||||||
use crate::send_wal::HotStandbyFeedback;
|
use crate::send_wal::HotStandbyFeedback;
|
||||||
|
|
||||||
use crate::state::TimelineState;
|
use crate::state::TimelineState;
|
||||||
@@ -499,7 +500,11 @@ where
|
|||||||
/// Accepts a control file storage containing the safekeeper state.
|
/// Accepts a control file storage containing the safekeeper state.
|
||||||
/// State must be initialized, i.e. contain filled `tenant_id`, `timeline_id`
|
/// State must be initialized, i.e. contain filled `tenant_id`, `timeline_id`
|
||||||
/// and `server` (`wal_seg_size` inside it) fields.
|
/// and `server` (`wal_seg_size` inside it) fields.
|
||||||
pub fn new(state: CTRL, wal_store: WAL, node_id: NodeId) -> Result<SafeKeeper<CTRL, WAL>> {
|
pub fn new(
|
||||||
|
state: TimelineState<CTRL>,
|
||||||
|
wal_store: WAL,
|
||||||
|
node_id: NodeId,
|
||||||
|
) -> Result<SafeKeeper<CTRL, WAL>> {
|
||||||
if state.tenant_id == TenantId::from([0u8; 16])
|
if state.tenant_id == TenantId::from([0u8; 16])
|
||||||
|| state.timeline_id == TimelineId::from([0u8; 16])
|
|| state.timeline_id == TimelineId::from([0u8; 16])
|
||||||
{
|
{
|
||||||
@@ -512,7 +517,7 @@ where
|
|||||||
|
|
||||||
Ok(SafeKeeper {
|
Ok(SafeKeeper {
|
||||||
term_start_lsn: Lsn(0),
|
term_start_lsn: Lsn(0),
|
||||||
state: TimelineState::new(state),
|
state,
|
||||||
wal_store,
|
wal_store,
|
||||||
node_id,
|
node_id,
|
||||||
})
|
})
|
||||||
@@ -526,11 +531,6 @@ where
|
|||||||
.up_to(self.flush_lsn())
|
.up_to(self.flush_lsn())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get current term.
|
|
||||||
pub fn get_term(&self) -> Term {
|
|
||||||
self.state.acceptor_state.term
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_last_log_term(&self) -> Term {
|
pub fn get_last_log_term(&self) -> Term {
|
||||||
self.state
|
self.state
|
||||||
.acceptor_state
|
.acceptor_state
|
||||||
@@ -697,6 +697,10 @@ where
|
|||||||
&mut self,
|
&mut self,
|
||||||
msg: &ProposerElected,
|
msg: &ProposerElected,
|
||||||
) -> Result<Option<AcceptorProposerMessage>> {
|
) -> Result<Option<AcceptorProposerMessage>> {
|
||||||
|
let _timer = MISC_OPERATION_SECONDS
|
||||||
|
.with_label_values(&["handle_elected"])
|
||||||
|
.start_timer();
|
||||||
|
|
||||||
info!("received ProposerElected {:?}", msg);
|
info!("received ProposerElected {:?}", msg);
|
||||||
if self.state.acceptor_state.term < msg.term {
|
if self.state.acceptor_state.term < msg.term {
|
||||||
let mut state = self.state.start_change();
|
let mut state = self.state.start_change();
|
||||||
@@ -912,10 +916,8 @@ where
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update timeline state with peer safekeeper data.
|
/// Update commit_lsn from peer safekeeper data.
|
||||||
pub async fn record_safekeeper_info(&mut self, sk_info: &SafekeeperTimelineInfo) -> Result<()> {
|
pub async fn record_safekeeper_info(&mut self, sk_info: &SafekeeperTimelineInfo) -> Result<()> {
|
||||||
let mut sync_control_file = false;
|
|
||||||
|
|
||||||
if (Lsn(sk_info.commit_lsn) != Lsn::INVALID) && (sk_info.last_log_term != INVALID_TERM) {
|
if (Lsn(sk_info.commit_lsn) != Lsn::INVALID) && (sk_info.last_log_term != INVALID_TERM) {
|
||||||
// Note: the check is too restrictive, generally we can update local
|
// Note: the check is too restrictive, generally we can update local
|
||||||
// commit_lsn if our history matches (is part of) history of advanced
|
// commit_lsn if our history matches (is part of) history of advanced
|
||||||
@@ -924,29 +926,6 @@ where
|
|||||||
self.update_commit_lsn(Lsn(sk_info.commit_lsn)).await?;
|
self.update_commit_lsn(Lsn(sk_info.commit_lsn)).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.state.inmem.backup_lsn = max(Lsn(sk_info.backup_lsn), self.state.inmem.backup_lsn);
|
|
||||||
sync_control_file |= self.state.backup_lsn + (self.state.server.wal_seg_size as u64)
|
|
||||||
< self.state.inmem.backup_lsn;
|
|
||||||
|
|
||||||
self.state.inmem.remote_consistent_lsn = max(
|
|
||||||
Lsn(sk_info.remote_consistent_lsn),
|
|
||||||
self.state.inmem.remote_consistent_lsn,
|
|
||||||
);
|
|
||||||
sync_control_file |= self.state.remote_consistent_lsn
|
|
||||||
+ (self.state.server.wal_seg_size as u64)
|
|
||||||
< self.state.inmem.remote_consistent_lsn;
|
|
||||||
|
|
||||||
self.state.inmem.peer_horizon_lsn = max(
|
|
||||||
Lsn(sk_info.peer_horizon_lsn),
|
|
||||||
self.state.inmem.peer_horizon_lsn,
|
|
||||||
);
|
|
||||||
sync_control_file |= self.state.peer_horizon_lsn + (self.state.server.wal_seg_size as u64)
|
|
||||||
< self.state.inmem.peer_horizon_lsn;
|
|
||||||
|
|
||||||
if sync_control_file {
|
|
||||||
self.state.flush().await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1039,7 +1018,7 @@ mod tests {
|
|||||||
persisted_state: test_sk_state(),
|
persisted_state: test_sk_state(),
|
||||||
};
|
};
|
||||||
let wal_store = DummyWalStore { lsn: Lsn(0) };
|
let wal_store = DummyWalStore { lsn: Lsn(0) };
|
||||||
let mut sk = SafeKeeper::new(storage, wal_store, NodeId(0)).unwrap();
|
let mut sk = SafeKeeper::new(TimelineState::new(storage), wal_store, NodeId(0)).unwrap();
|
||||||
|
|
||||||
// check voting for 1 is ok
|
// check voting for 1 is ok
|
||||||
let vote_request = ProposerAcceptorMessage::VoteRequest(VoteRequest { term: 1 });
|
let vote_request = ProposerAcceptorMessage::VoteRequest(VoteRequest { term: 1 });
|
||||||
@@ -1055,7 +1034,7 @@ mod tests {
|
|||||||
persisted_state: state,
|
persisted_state: state,
|
||||||
};
|
};
|
||||||
|
|
||||||
sk = SafeKeeper::new(storage, sk.wal_store, NodeId(0)).unwrap();
|
sk = SafeKeeper::new(TimelineState::new(storage), sk.wal_store, NodeId(0)).unwrap();
|
||||||
|
|
||||||
// and ensure voting second time for 1 is not ok
|
// and ensure voting second time for 1 is not ok
|
||||||
vote_resp = sk.process_msg(&vote_request).await;
|
vote_resp = sk.process_msg(&vote_request).await;
|
||||||
@@ -1072,7 +1051,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let wal_store = DummyWalStore { lsn: Lsn(0) };
|
let wal_store = DummyWalStore { lsn: Lsn(0) };
|
||||||
|
|
||||||
let mut sk = SafeKeeper::new(storage, wal_store, NodeId(0)).unwrap();
|
let mut sk = SafeKeeper::new(TimelineState::new(storage), wal_store, NodeId(0)).unwrap();
|
||||||
|
|
||||||
let mut ar_hdr = AppendRequestHeader {
|
let mut ar_hdr = AppendRequestHeader {
|
||||||
term: 1,
|
term: 1,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::handler::SafekeeperPostgresHandler;
|
|||||||
use crate::metrics::RECEIVED_PS_FEEDBACKS;
|
use crate::metrics::RECEIVED_PS_FEEDBACKS;
|
||||||
use crate::receive_wal::WalReceivers;
|
use crate::receive_wal::WalReceivers;
|
||||||
use crate::safekeeper::{Term, TermLsn};
|
use crate::safekeeper::{Term, TermLsn};
|
||||||
use crate::timeline::FullAccessTimeline;
|
use crate::timeline::WalResidentTimeline;
|
||||||
use crate::wal_service::ConnectionId;
|
use crate::wal_service::ConnectionId;
|
||||||
use crate::wal_storage::WalReader;
|
use crate::wal_storage::WalReader;
|
||||||
use crate::GlobalTimelines;
|
use crate::GlobalTimelines;
|
||||||
@@ -387,10 +387,10 @@ impl SafekeeperPostgresHandler {
|
|||||||
term: Option<Term>,
|
term: Option<Term>,
|
||||||
) -> Result<(), QueryError> {
|
) -> Result<(), QueryError> {
|
||||||
let tli = GlobalTimelines::get(self.ttid).map_err(|e| QueryError::Other(e.into()))?;
|
let tli = GlobalTimelines::get(self.ttid).map_err(|e| QueryError::Other(e.into()))?;
|
||||||
let full_access = tli.full_access_guard().await?;
|
let residence_guard = tli.wal_residence_guard().await?;
|
||||||
|
|
||||||
if let Err(end) = self
|
if let Err(end) = self
|
||||||
.handle_start_replication_guts(pgb, start_pos, term, full_access)
|
.handle_start_replication_guts(pgb, start_pos, term, residence_guard)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
let info = tli.get_safekeeper_info(&self.conf).await;
|
let info = tli.get_safekeeper_info(&self.conf).await;
|
||||||
@@ -407,7 +407,7 @@ impl SafekeeperPostgresHandler {
|
|||||||
pgb: &mut PostgresBackend<IO>,
|
pgb: &mut PostgresBackend<IO>,
|
||||||
start_pos: Lsn,
|
start_pos: Lsn,
|
||||||
term: Option<Term>,
|
term: Option<Term>,
|
||||||
tli: FullAccessTimeline,
|
tli: WalResidentTimeline,
|
||||||
) -> Result<(), CopyStreamHandlerEnd> {
|
) -> Result<(), CopyStreamHandlerEnd> {
|
||||||
let appname = self.appname.clone();
|
let appname = self.appname.clone();
|
||||||
|
|
||||||
@@ -458,7 +458,8 @@ impl SafekeeperPostgresHandler {
|
|||||||
|
|
||||||
let mut sender = WalSender {
|
let mut sender = WalSender {
|
||||||
pgb,
|
pgb,
|
||||||
tli: tli.clone(),
|
// should succeed since we're already holding another guard
|
||||||
|
tli: tli.wal_residence_guard().await?,
|
||||||
appname,
|
appname,
|
||||||
start_pos,
|
start_pos,
|
||||||
end_pos,
|
end_pos,
|
||||||
@@ -527,7 +528,7 @@ impl EndWatch {
|
|||||||
/// A half driving sending WAL.
|
/// A half driving sending WAL.
|
||||||
struct WalSender<'a, IO> {
|
struct WalSender<'a, IO> {
|
||||||
pgb: &'a mut PostgresBackend<IO>,
|
pgb: &'a mut PostgresBackend<IO>,
|
||||||
tli: FullAccessTimeline,
|
tli: WalResidentTimeline,
|
||||||
appname: Option<String>,
|
appname: Option<String>,
|
||||||
// Position since which we are sending next chunk.
|
// Position since which we are sending next chunk.
|
||||||
start_pos: Lsn,
|
start_pos: Lsn,
|
||||||
@@ -736,7 +737,7 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> WalSender<'_, IO> {
|
|||||||
struct ReplyReader<IO> {
|
struct ReplyReader<IO> {
|
||||||
reader: PostgresBackendReader<IO>,
|
reader: PostgresBackendReader<IO>,
|
||||||
ws_guard: Arc<WalSenderGuard>,
|
ws_guard: Arc<WalSenderGuard>,
|
||||||
tli: FullAccessTimeline,
|
tli: WalResidentTimeline,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<IO: AsyncRead + AsyncWrite + Unpin> ReplyReader<IO> {
|
impl<IO: AsyncRead + AsyncWrite + Unpin> ReplyReader<IO> {
|
||||||
|
|||||||
@@ -189,7 +189,12 @@ where
|
|||||||
|
|
||||||
/// Persist given state. c.f. start_change.
|
/// Persist given state. c.f. start_change.
|
||||||
pub async fn finish_change(&mut self, s: &TimelinePersistentState) -> Result<()> {
|
pub async fn finish_change(&mut self, s: &TimelinePersistentState) -> Result<()> {
|
||||||
self.pers.persist(s).await?;
|
if s.eq(&*self.pers) {
|
||||||
|
// nothing to do if state didn't change
|
||||||
|
} else {
|
||||||
|
self.pers.persist(s).await?;
|
||||||
|
}
|
||||||
|
|
||||||
// keep in memory values up to date
|
// keep in memory values up to date
|
||||||
self.inmem.commit_lsn = s.commit_lsn;
|
self.inmem.commit_lsn = s.commit_lsn;
|
||||||
self.inmem.backup_lsn = s.backup_lsn;
|
self.inmem.backup_lsn = s.backup_lsn;
|
||||||
|
|||||||
+414
-56
@@ -31,12 +31,15 @@ use crate::safekeeper::{
|
|||||||
INVALID_TERM,
|
INVALID_TERM,
|
||||||
};
|
};
|
||||||
use crate::send_wal::WalSenders;
|
use crate::send_wal::WalSenders;
|
||||||
use crate::state::{TimelineMemState, TimelinePersistentState};
|
use crate::state::{EvictionState, TimelineMemState, TimelinePersistentState, TimelineState};
|
||||||
|
use crate::timeline_guard::ResidenceGuard;
|
||||||
|
use crate::timeline_manager::{AtomicStatus, ManagerCtl};
|
||||||
use crate::timelines_set::TimelinesSet;
|
use crate::timelines_set::TimelinesSet;
|
||||||
use crate::wal_backup::{self};
|
use crate::wal_backup::{self};
|
||||||
|
use crate::wal_backup_partial::PartialRemoteSegment;
|
||||||
use crate::{control_file, safekeeper::UNKNOWN_SERVER_VERSION};
|
use crate::{control_file, safekeeper::UNKNOWN_SERVER_VERSION};
|
||||||
|
|
||||||
use crate::metrics::FullTimelineInfo;
|
use crate::metrics::{FullTimelineInfo, WalStorageMetrics, MISC_OPERATION_SECONDS};
|
||||||
use crate::wal_storage::{Storage as wal_storage_iface, WalReader};
|
use crate::wal_storage::{Storage as wal_storage_iface, WalReader};
|
||||||
use crate::{debug_dump, timeline_manager, wal_storage};
|
use crate::{debug_dump, timeline_manager, wal_storage};
|
||||||
use crate::{GlobalTimelines, SafeKeeperConf};
|
use crate::{GlobalTimelines, SafeKeeperConf};
|
||||||
@@ -132,8 +135,9 @@ impl<'a> DerefMut for WriteGuardSharedState<'a> {
|
|||||||
|
|
||||||
impl<'a> Drop for WriteGuardSharedState<'a> {
|
impl<'a> Drop for WriteGuardSharedState<'a> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let term_flush_lsn = TermLsn::from((self.guard.sk.get_term(), self.guard.sk.flush_lsn()));
|
let term_flush_lsn =
|
||||||
let commit_lsn = self.guard.sk.state.inmem.commit_lsn;
|
TermLsn::from((self.guard.sk.last_log_term(), self.guard.sk.flush_lsn()));
|
||||||
|
let commit_lsn = self.guard.sk.state().inmem.commit_lsn;
|
||||||
|
|
||||||
let _ = self.tli.term_flush_lsn_watch_tx.send_if_modified(|old| {
|
let _ = self.tli.term_flush_lsn_watch_tx.send_if_modified(|old| {
|
||||||
if *old != term_flush_lsn {
|
if *old != term_flush_lsn {
|
||||||
@@ -162,10 +166,150 @@ impl<'a> Drop for WriteGuardSharedState<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This structure is stored in shared state and represents the state of the timeline.
|
||||||
|
/// Usually it holds SafeKeeper, but it also supports offloaded timeline state. In this
|
||||||
|
/// case, SafeKeeper is not available (because WAL is not present on disk) and all
|
||||||
|
/// operations can be done only with control file.
|
||||||
|
pub enum StateSK {
|
||||||
|
Loaded(SafeKeeper<control_file::FileStorage, wal_storage::PhysicalStorage>),
|
||||||
|
Offloaded(Box<TimelineState<control_file::FileStorage>>),
|
||||||
|
// Not used, required for moving between states.
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StateSK {
|
||||||
|
pub fn flush_lsn(&self) -> Lsn {
|
||||||
|
match self {
|
||||||
|
StateSK::Loaded(sk) => sk.wal_store.flush_lsn(),
|
||||||
|
StateSK::Offloaded(state) => match state.eviction_state {
|
||||||
|
EvictionState::Offloaded(flush_lsn) => flush_lsn,
|
||||||
|
_ => panic!("StateSK::Offloaded mismatches with eviction_state from control_file"),
|
||||||
|
},
|
||||||
|
StateSK::Empty => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the control file's timeline state.
|
||||||
|
pub fn state(&self) -> &TimelineState<control_file::FileStorage> {
|
||||||
|
match self {
|
||||||
|
StateSK::Loaded(sk) => &sk.state,
|
||||||
|
StateSK::Offloaded(ref s) => s,
|
||||||
|
StateSK::Empty => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state_mut(&mut self) -> &mut TimelineState<control_file::FileStorage> {
|
||||||
|
match self {
|
||||||
|
StateSK::Loaded(sk) => &mut sk.state,
|
||||||
|
StateSK::Offloaded(ref mut s) => s,
|
||||||
|
StateSK::Empty => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last_log_term(&self) -> Term {
|
||||||
|
self.state()
|
||||||
|
.acceptor_state
|
||||||
|
.get_last_log_term(self.flush_lsn())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close open WAL files to release FDs.
|
||||||
|
fn close_wal_store(&mut self) {
|
||||||
|
if let StateSK::Loaded(sk) = self {
|
||||||
|
sk.wal_store.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update timeline state with peer safekeeper data.
|
||||||
|
pub async fn record_safekeeper_info(&mut self, sk_info: &SafekeeperTimelineInfo) -> Result<()> {
|
||||||
|
// update commit_lsn if safekeeper is loaded
|
||||||
|
match self {
|
||||||
|
StateSK::Loaded(sk) => sk.record_safekeeper_info(sk_info).await?,
|
||||||
|
StateSK::Offloaded(_) => {}
|
||||||
|
StateSK::Empty => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// update everything else, including remote_consistent_lsn and backup_lsn
|
||||||
|
let mut sync_control_file = false;
|
||||||
|
let state = self.state_mut();
|
||||||
|
let wal_seg_size = state.server.wal_seg_size as u64;
|
||||||
|
|
||||||
|
state.inmem.backup_lsn = max(Lsn(sk_info.backup_lsn), state.inmem.backup_lsn);
|
||||||
|
sync_control_file |= state.backup_lsn + wal_seg_size < state.inmem.backup_lsn;
|
||||||
|
|
||||||
|
state.inmem.remote_consistent_lsn = max(
|
||||||
|
Lsn(sk_info.remote_consistent_lsn),
|
||||||
|
state.inmem.remote_consistent_lsn,
|
||||||
|
);
|
||||||
|
sync_control_file |=
|
||||||
|
state.remote_consistent_lsn + wal_seg_size < state.inmem.remote_consistent_lsn;
|
||||||
|
|
||||||
|
state.inmem.peer_horizon_lsn =
|
||||||
|
max(Lsn(sk_info.peer_horizon_lsn), state.inmem.peer_horizon_lsn);
|
||||||
|
sync_control_file |= state.peer_horizon_lsn + wal_seg_size < state.inmem.peer_horizon_lsn;
|
||||||
|
|
||||||
|
if sync_control_file {
|
||||||
|
state.flush().await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Previously known as epoch_start_lsn. Needed only for reference in some APIs.
|
||||||
|
pub fn term_start_lsn(&self) -> Lsn {
|
||||||
|
match self {
|
||||||
|
StateSK::Loaded(sk) => sk.term_start_lsn,
|
||||||
|
StateSK::Offloaded(_) => Lsn(0),
|
||||||
|
StateSK::Empty => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used for metrics only.
|
||||||
|
pub fn wal_storage_metrics(&self) -> WalStorageMetrics {
|
||||||
|
match self {
|
||||||
|
StateSK::Loaded(sk) => sk.wal_store.get_metrics(),
|
||||||
|
StateSK::Offloaded(_) => WalStorageMetrics::default(),
|
||||||
|
StateSK::Empty => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns WAL storage internal LSNs for debug dump.
|
||||||
|
pub fn wal_storage_internal_state(&self) -> (Lsn, Lsn, Lsn, bool) {
|
||||||
|
match self {
|
||||||
|
StateSK::Loaded(sk) => sk.wal_store.internal_state(),
|
||||||
|
StateSK::Offloaded(_) => {
|
||||||
|
let flush_lsn = self.flush_lsn();
|
||||||
|
(flush_lsn, flush_lsn, flush_lsn, false)
|
||||||
|
}
|
||||||
|
StateSK::Empty => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access to SafeKeeper object. Panics if offloaded, should be good to use from WalResidentTimeline.
|
||||||
|
pub fn safekeeper(
|
||||||
|
&mut self,
|
||||||
|
) -> &mut SafeKeeper<control_file::FileStorage, wal_storage::PhysicalStorage> {
|
||||||
|
match self {
|
||||||
|
StateSK::Loaded(sk) => sk,
|
||||||
|
StateSK::Offloaded(_) => {
|
||||||
|
panic!("safekeeper is offloaded, cannot be used")
|
||||||
|
}
|
||||||
|
StateSK::Empty => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves control file's state structure out of the enum. Used to switch states.
|
||||||
|
fn take_state(self) -> TimelineState<control_file::FileStorage> {
|
||||||
|
match self {
|
||||||
|
StateSK::Loaded(sk) => sk.state,
|
||||||
|
StateSK::Offloaded(state) => *state,
|
||||||
|
StateSK::Empty => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Shared state associated with database instance
|
/// Shared state associated with database instance
|
||||||
pub struct SharedState {
|
pub struct SharedState {
|
||||||
/// Safekeeper object
|
/// Safekeeper object
|
||||||
pub(crate) sk: SafeKeeper<control_file::FileStorage, wal_storage::PhysicalStorage>,
|
pub(crate) sk: StateSK,
|
||||||
/// In memory list containing state of peers sent in latest messages from them.
|
/// In memory list containing state of peers sent in latest messages from them.
|
||||||
pub(crate) peers_info: PeersInfo,
|
pub(crate) peers_info: PeersInfo,
|
||||||
// True value hinders old WAL removal; this is used by snapshotting. We
|
// True value hinders old WAL removal; this is used by snapshotting. We
|
||||||
@@ -203,10 +347,10 @@ impl SharedState {
|
|||||||
control_file::FileStorage::create_new(timeline_dir.clone(), conf, state)?;
|
control_file::FileStorage::create_new(timeline_dir.clone(), conf, state)?;
|
||||||
let wal_store =
|
let wal_store =
|
||||||
wal_storage::PhysicalStorage::new(ttid, timeline_dir, conf, &control_store)?;
|
wal_storage::PhysicalStorage::new(ttid, timeline_dir, conf, &control_store)?;
|
||||||
let sk = SafeKeeper::new(control_store, wal_store, conf.my_id)?;
|
let sk = SafeKeeper::new(TimelineState::new(control_store), wal_store, conf.my_id)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
sk,
|
sk: StateSK::Loaded(sk),
|
||||||
peers_info: PeersInfo(vec![]),
|
peers_info: PeersInfo(vec![]),
|
||||||
wal_removal_on_hold: false,
|
wal_removal_on_hold: false,
|
||||||
})
|
})
|
||||||
@@ -220,18 +364,30 @@ impl SharedState {
|
|||||||
bail!(TimelineError::UninitializedWalSegSize(*ttid));
|
bail!(TimelineError::UninitializedWalSegSize(*ttid));
|
||||||
}
|
}
|
||||||
|
|
||||||
let wal_store =
|
let sk = match control_store.eviction_state {
|
||||||
wal_storage::PhysicalStorage::new(ttid, timeline_dir, conf, &control_store)?;
|
EvictionState::Present => {
|
||||||
|
let wal_store =
|
||||||
|
wal_storage::PhysicalStorage::new(ttid, timeline_dir, conf, &control_store)?;
|
||||||
|
StateSK::Loaded(SafeKeeper::new(
|
||||||
|
TimelineState::new(control_store),
|
||||||
|
wal_store,
|
||||||
|
conf.my_id,
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
EvictionState::Offloaded(_) => {
|
||||||
|
StateSK::Offloaded(Box::new(TimelineState::new(control_store)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
sk: SafeKeeper::new(control_store, wal_store, conf.my_id)?,
|
sk,
|
||||||
peers_info: PeersInfo(vec![]),
|
peers_info: PeersInfo(vec![]),
|
||||||
wal_removal_on_hold: false,
|
wal_removal_on_hold: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_wal_seg_size(&self) -> usize {
|
pub(crate) fn get_wal_seg_size(&self) -> usize {
|
||||||
self.sk.state.server.wal_seg_size as usize
|
self.sk.state().server.wal_seg_size as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_safekeeper_info(
|
fn get_safekeeper_info(
|
||||||
@@ -246,20 +402,20 @@ impl SharedState {
|
|||||||
tenant_id: ttid.tenant_id.as_ref().to_owned(),
|
tenant_id: ttid.tenant_id.as_ref().to_owned(),
|
||||||
timeline_id: ttid.timeline_id.as_ref().to_owned(),
|
timeline_id: ttid.timeline_id.as_ref().to_owned(),
|
||||||
}),
|
}),
|
||||||
term: self.sk.state.acceptor_state.term,
|
term: self.sk.state().acceptor_state.term,
|
||||||
last_log_term: self.sk.get_last_log_term(),
|
last_log_term: self.sk.last_log_term(),
|
||||||
flush_lsn: self.sk.flush_lsn().0,
|
flush_lsn: self.sk.flush_lsn().0,
|
||||||
// note: this value is not flushed to control file yet and can be lost
|
// note: this value is not flushed to control file yet and can be lost
|
||||||
commit_lsn: self.sk.state.inmem.commit_lsn.0,
|
commit_lsn: self.sk.state().inmem.commit_lsn.0,
|
||||||
remote_consistent_lsn: self.sk.state.inmem.remote_consistent_lsn.0,
|
remote_consistent_lsn: self.sk.state().inmem.remote_consistent_lsn.0,
|
||||||
peer_horizon_lsn: self.sk.state.inmem.peer_horizon_lsn.0,
|
peer_horizon_lsn: self.sk.state().inmem.peer_horizon_lsn.0,
|
||||||
safekeeper_connstr: conf
|
safekeeper_connstr: conf
|
||||||
.advertise_pg_addr
|
.advertise_pg_addr
|
||||||
.to_owned()
|
.to_owned()
|
||||||
.unwrap_or(conf.listen_pg_addr.clone()),
|
.unwrap_or(conf.listen_pg_addr.clone()),
|
||||||
http_connstr: conf.listen_http_addr.to_owned(),
|
http_connstr: conf.listen_http_addr.to_owned(),
|
||||||
backup_lsn: self.sk.state.inmem.backup_lsn.0,
|
backup_lsn: self.sk.state().inmem.backup_lsn.0,
|
||||||
local_start_lsn: self.sk.state.local_start_lsn.0,
|
local_start_lsn: self.sk.state().local_start_lsn.0,
|
||||||
availability_zone: conf.availability_zone.clone(),
|
availability_zone: conf.availability_zone.clone(),
|
||||||
standby_horizon: standby_apply_lsn.0,
|
standby_horizon: standby_apply_lsn.0,
|
||||||
}
|
}
|
||||||
@@ -335,6 +491,7 @@ pub struct Timeline {
|
|||||||
walsenders: Arc<WalSenders>,
|
walsenders: Arc<WalSenders>,
|
||||||
walreceivers: Arc<WalReceivers>,
|
walreceivers: Arc<WalReceivers>,
|
||||||
timeline_dir: Utf8PathBuf,
|
timeline_dir: Utf8PathBuf,
|
||||||
|
manager_ctl: ManagerCtl,
|
||||||
|
|
||||||
/// Delete/cancel will trigger this, background tasks should drop out as soon as it fires
|
/// Delete/cancel will trigger this, background tasks should drop out as soon as it fires
|
||||||
pub(crate) cancel: CancellationToken,
|
pub(crate) cancel: CancellationToken,
|
||||||
@@ -343,6 +500,7 @@ pub struct Timeline {
|
|||||||
pub(crate) broker_active: AtomicBool,
|
pub(crate) broker_active: AtomicBool,
|
||||||
pub(crate) wal_backup_active: AtomicBool,
|
pub(crate) wal_backup_active: AtomicBool,
|
||||||
pub(crate) last_removed_segno: AtomicU64,
|
pub(crate) last_removed_segno: AtomicU64,
|
||||||
|
pub(crate) mgr_status: AtomicStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Timeline {
|
impl Timeline {
|
||||||
@@ -352,9 +510,9 @@ impl Timeline {
|
|||||||
|
|
||||||
let shared_state = SharedState::restore(conf, &ttid)?;
|
let shared_state = SharedState::restore(conf, &ttid)?;
|
||||||
let (commit_lsn_watch_tx, commit_lsn_watch_rx) =
|
let (commit_lsn_watch_tx, commit_lsn_watch_rx) =
|
||||||
watch::channel(shared_state.sk.state.commit_lsn);
|
watch::channel(shared_state.sk.state().commit_lsn);
|
||||||
let (term_flush_lsn_watch_tx, term_flush_lsn_watch_rx) = watch::channel(TermLsn::from((
|
let (term_flush_lsn_watch_tx, term_flush_lsn_watch_rx) = watch::channel(TermLsn::from((
|
||||||
shared_state.sk.get_term(),
|
shared_state.sk.last_log_term(),
|
||||||
shared_state.sk.flush_lsn(),
|
shared_state.sk.flush_lsn(),
|
||||||
)));
|
)));
|
||||||
let (shared_state_version_tx, shared_state_version_rx) = watch::channel(0);
|
let (shared_state_version_tx, shared_state_version_rx) = watch::channel(0);
|
||||||
@@ -373,9 +531,11 @@ impl Timeline {
|
|||||||
walreceivers,
|
walreceivers,
|
||||||
cancel: CancellationToken::default(),
|
cancel: CancellationToken::default(),
|
||||||
timeline_dir: get_timeline_dir(conf, &ttid),
|
timeline_dir: get_timeline_dir(conf, &ttid),
|
||||||
|
manager_ctl: ManagerCtl::new(),
|
||||||
broker_active: AtomicBool::new(false),
|
broker_active: AtomicBool::new(false),
|
||||||
wal_backup_active: AtomicBool::new(false),
|
wal_backup_active: AtomicBool::new(false),
|
||||||
last_removed_segno: AtomicU64::new(0),
|
last_removed_segno: AtomicU64::new(0),
|
||||||
|
mgr_status: AtomicStatus::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,9 +569,11 @@ impl Timeline {
|
|||||||
walreceivers,
|
walreceivers,
|
||||||
cancel: CancellationToken::default(),
|
cancel: CancellationToken::default(),
|
||||||
timeline_dir: get_timeline_dir(conf, &ttid),
|
timeline_dir: get_timeline_dir(conf, &ttid),
|
||||||
|
manager_ctl: ManagerCtl::new(),
|
||||||
broker_active: AtomicBool::new(false),
|
broker_active: AtomicBool::new(false),
|
||||||
wal_backup_active: AtomicBool::new(false),
|
wal_backup_active: AtomicBool::new(false),
|
||||||
last_removed_segno: AtomicU64::new(0),
|
last_removed_segno: AtomicU64::new(0),
|
||||||
|
mgr_status: AtomicStatus::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,7 +604,7 @@ impl Timeline {
|
|||||||
fs::create_dir_all(&self.timeline_dir).await?;
|
fs::create_dir_all(&self.timeline_dir).await?;
|
||||||
|
|
||||||
// Write timeline to disk and start background tasks.
|
// Write timeline to disk and start background tasks.
|
||||||
if let Err(e) = shared_state.sk.state.flush().await {
|
if let Err(e) = shared_state.sk.state_mut().flush().await {
|
||||||
// Bootstrap failed, cancel timeline and remove timeline directory.
|
// Bootstrap failed, cancel timeline and remove timeline directory.
|
||||||
self.cancel(shared_state);
|
self.cancel(shared_state);
|
||||||
|
|
||||||
@@ -465,12 +627,16 @@ impl Timeline {
|
|||||||
conf: &SafeKeeperConf,
|
conf: &SafeKeeperConf,
|
||||||
broker_active_set: Arc<TimelinesSet>,
|
broker_active_set: Arc<TimelinesSet>,
|
||||||
) {
|
) {
|
||||||
|
let (tx, rx) = self.manager_ctl.bootstrap_manager();
|
||||||
|
|
||||||
// Start manager task which will monitor timeline state and update
|
// Start manager task which will monitor timeline state and update
|
||||||
// background tasks.
|
// background tasks.
|
||||||
tokio::spawn(timeline_manager::main_task(
|
tokio::spawn(timeline_manager::main_task(
|
||||||
self.clone(),
|
ManagerTimeline { tli: self.clone() },
|
||||||
conf.clone(),
|
conf.clone(),
|
||||||
broker_active_set,
|
broker_active_set,
|
||||||
|
tx,
|
||||||
|
rx,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,7 +673,7 @@ impl Timeline {
|
|||||||
self.cancel.cancel();
|
self.cancel.cancel();
|
||||||
// Close associated FDs. Nobody will be able to touch timeline data once
|
// Close associated FDs. Nobody will be able to touch timeline data once
|
||||||
// it is cancelled, so WAL storage won't be opened again.
|
// it is cancelled, so WAL storage won't be opened again.
|
||||||
shared_state.sk.wal_store.close();
|
shared_state.sk.close_wal_store();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns if timeline is cancelled.
|
/// Returns if timeline is cancelled.
|
||||||
@@ -547,12 +713,15 @@ impl Timeline {
|
|||||||
/// Returns state of the timeline.
|
/// Returns state of the timeline.
|
||||||
pub async fn get_state(&self) -> (TimelineMemState, TimelinePersistentState) {
|
pub async fn get_state(&self) -> (TimelineMemState, TimelinePersistentState) {
|
||||||
let state = self.read_shared_state().await;
|
let state = self.read_shared_state().await;
|
||||||
(state.sk.state.inmem.clone(), state.sk.state.clone())
|
(
|
||||||
|
state.sk.state().inmem.clone(),
|
||||||
|
TimelinePersistentState::clone(state.sk.state()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns latest backup_lsn.
|
/// Returns latest backup_lsn.
|
||||||
pub async fn get_wal_backup_lsn(&self) -> Lsn {
|
pub async fn get_wal_backup_lsn(&self) -> Lsn {
|
||||||
self.read_shared_state().await.sk.state.inmem.backup_lsn
|
self.read_shared_state().await.sk.state().inmem.backup_lsn
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets backup_lsn to the given value.
|
/// Sets backup_lsn to the given value.
|
||||||
@@ -562,7 +731,7 @@ impl Timeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut state = self.write_shared_state().await;
|
let mut state = self.write_shared_state().await;
|
||||||
state.sk.state.inmem.backup_lsn = max(state.sk.state.inmem.backup_lsn, backup_lsn);
|
state.sk.state_mut().inmem.backup_lsn = max(state.sk.state().inmem.backup_lsn, backup_lsn);
|
||||||
// we should check whether to shut down offloader, but this will be done
|
// we should check whether to shut down offloader, but this will be done
|
||||||
// soon by peer communication anyway.
|
// soon by peer communication anyway.
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -604,7 +773,7 @@ impl Timeline {
|
|||||||
|
|
||||||
/// Returns flush_lsn.
|
/// Returns flush_lsn.
|
||||||
pub async fn get_flush_lsn(&self) -> Lsn {
|
pub async fn get_flush_lsn(&self) -> Lsn {
|
||||||
self.read_shared_state().await.sk.wal_store.flush_lsn()
|
self.read_shared_state().await.sk.flush_lsn()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gather timeline data for metrics.
|
/// Gather timeline data for metrics.
|
||||||
@@ -623,11 +792,11 @@ impl Timeline {
|
|||||||
timeline_is_active: self.broker_active.load(Ordering::Relaxed),
|
timeline_is_active: self.broker_active.load(Ordering::Relaxed),
|
||||||
num_computes: self.walreceivers.get_num() as u32,
|
num_computes: self.walreceivers.get_num() as u32,
|
||||||
last_removed_segno: self.last_removed_segno.load(Ordering::Relaxed),
|
last_removed_segno: self.last_removed_segno.load(Ordering::Relaxed),
|
||||||
epoch_start_lsn: state.sk.term_start_lsn,
|
epoch_start_lsn: state.sk.term_start_lsn(),
|
||||||
mem_state: state.sk.state.inmem.clone(),
|
mem_state: state.sk.state().inmem.clone(),
|
||||||
persisted_state: state.sk.state.clone(),
|
persisted_state: TimelinePersistentState::clone(state.sk.state()),
|
||||||
flush_lsn: state.sk.wal_store.flush_lsn(),
|
flush_lsn: state.sk.flush_lsn(),
|
||||||
wal_storage: state.sk.wal_store.get_metrics(),
|
wal_storage: state.sk.wal_storage_metrics(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,7 +805,7 @@ impl Timeline {
|
|||||||
let state = self.read_shared_state().await;
|
let state = self.read_shared_state().await;
|
||||||
|
|
||||||
let (write_lsn, write_record_lsn, flush_lsn, file_open) =
|
let (write_lsn, write_record_lsn, flush_lsn, file_open) =
|
||||||
state.sk.wal_store.internal_state();
|
state.sk.wal_storage_internal_state();
|
||||||
|
|
||||||
debug_dump::Memory {
|
debug_dump::Memory {
|
||||||
is_cancelled: self.is_cancelled(),
|
is_cancelled: self.is_cancelled(),
|
||||||
@@ -646,8 +815,9 @@ impl Timeline {
|
|||||||
active: self.broker_active.load(Ordering::Relaxed),
|
active: self.broker_active.load(Ordering::Relaxed),
|
||||||
num_computes: self.walreceivers.get_num() as u32,
|
num_computes: self.walreceivers.get_num() as u32,
|
||||||
last_removed_segno: self.last_removed_segno.load(Ordering::Relaxed),
|
last_removed_segno: self.last_removed_segno.load(Ordering::Relaxed),
|
||||||
epoch_start_lsn: state.sk.term_start_lsn,
|
epoch_start_lsn: state.sk.term_start_lsn(),
|
||||||
mem_state: state.sk.state.inmem.clone(),
|
mem_state: state.sk.state().inmem.clone(),
|
||||||
|
mgr_status: self.mgr_status.get(),
|
||||||
write_lsn,
|
write_lsn,
|
||||||
write_record_lsn,
|
write_record_lsn,
|
||||||
flush_lsn,
|
flush_lsn,
|
||||||
@@ -661,34 +831,89 @@ impl Timeline {
|
|||||||
f: impl FnOnce(&mut TimelinePersistentState) -> Result<T>,
|
f: impl FnOnce(&mut TimelinePersistentState) -> Result<T>,
|
||||||
) -> Result<T> {
|
) -> Result<T> {
|
||||||
let mut state = self.write_shared_state().await;
|
let mut state = self.write_shared_state().await;
|
||||||
let mut persistent_state = state.sk.state.start_change();
|
let mut persistent_state = state.sk.state_mut().start_change();
|
||||||
// If f returns error, we abort the change and don't persist anything.
|
// If f returns error, we abort the change and don't persist anything.
|
||||||
let res = f(&mut persistent_state)?;
|
let res = f(&mut persistent_state)?;
|
||||||
// If persisting fails, we abort the change and return error.
|
// If persisting fails, we abort the change and return error.
|
||||||
state.sk.state.finish_change(&persistent_state).await?;
|
state
|
||||||
|
.sk
|
||||||
|
.state_mut()
|
||||||
|
.finish_change(&persistent_state)
|
||||||
|
.await?;
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the timeline guard for reading/writing WAL files.
|
/// Get the timeline guard for reading/writing WAL files.
|
||||||
/// TODO: if WAL files are not present on disk (evicted), they will be
|
/// If WAL files are not present on disk (evicted), they will be automatically
|
||||||
/// downloaded from S3. Also there will logic for preventing eviction
|
/// downloaded from remote storage. This is done in the manager task, which is
|
||||||
/// while someone is holding FullAccessTimeline guard.
|
/// responsible for issuing all guards.
|
||||||
pub async fn full_access_guard(self: &Arc<Self>) -> Result<FullAccessTimeline> {
|
///
|
||||||
|
/// NB: don't use this function from timeline_manager, it will deadlock.
|
||||||
|
/// NB: don't use this function while holding shared_state lock.
|
||||||
|
pub async fn wal_residence_guard(self: &Arc<Self>) -> Result<WalResidentTimeline> {
|
||||||
if self.is_cancelled() {
|
if self.is_cancelled() {
|
||||||
bail!(TimelineError::Cancelled(self.ttid));
|
bail!(TimelineError::Cancelled(self.ttid));
|
||||||
}
|
}
|
||||||
Ok(FullAccessTimeline { tli: self.clone() })
|
|
||||||
|
debug!("requesting WalResidentTimeline guard");
|
||||||
|
let started_at = Instant::now();
|
||||||
|
let status_before = self.mgr_status.get();
|
||||||
|
|
||||||
|
// Wait 30 seconds for the guard to be acquired. It can time out if someone is
|
||||||
|
// holding the lock (e.g. during `SafeKeeper::process_msg()`) or manager task
|
||||||
|
// is stuck.
|
||||||
|
let res = tokio::time::timeout_at(
|
||||||
|
started_at + Duration::from_secs(30),
|
||||||
|
self.manager_ctl.wal_residence_guard(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let guard = match res {
|
||||||
|
Ok(Ok(guard)) => {
|
||||||
|
let finished_at = Instant::now();
|
||||||
|
let elapsed = finished_at - started_at;
|
||||||
|
MISC_OPERATION_SECONDS
|
||||||
|
.with_label_values(&["wal_residence_guard"])
|
||||||
|
.observe(elapsed.as_secs_f64());
|
||||||
|
|
||||||
|
guard
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
warn!(
|
||||||
|
"error while acquiring WalResidentTimeline guard, statuses {:?} => {:?}",
|
||||||
|
status_before,
|
||||||
|
self.mgr_status.get()
|
||||||
|
);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
warn!(
|
||||||
|
"timeout while acquiring WalResidentTimeline guard, statuses {:?} => {:?}",
|
||||||
|
status_before,
|
||||||
|
self.mgr_status.get()
|
||||||
|
);
|
||||||
|
anyhow::bail!("timeout while acquiring WalResidentTimeline guard");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(WalResidentTimeline::new(self.clone(), guard))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is a guard that allows to read/write disk timeline state.
|
/// This is a guard that allows to read/write disk timeline state.
|
||||||
/// All tasks that are using the disk should use this guard.
|
/// All tasks that are trying to read/write WAL from disk should use this guard.
|
||||||
#[derive(Clone)]
|
pub struct WalResidentTimeline {
|
||||||
pub struct FullAccessTimeline {
|
|
||||||
pub tli: Arc<Timeline>,
|
pub tli: Arc<Timeline>,
|
||||||
|
_guard: ResidenceGuard,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for FullAccessTimeline {
|
impl WalResidentTimeline {
|
||||||
|
pub fn new(tli: Arc<Timeline>, _guard: ResidenceGuard) -> Self {
|
||||||
|
WalResidentTimeline { tli, _guard }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for WalResidentTimeline {
|
||||||
type Target = Arc<Timeline>;
|
type Target = Arc<Timeline>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
@@ -696,7 +921,7 @@ impl Deref for FullAccessTimeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FullAccessTimeline {
|
impl WalResidentTimeline {
|
||||||
/// Returns true if walsender should stop sending WAL to pageserver. We
|
/// Returns true if walsender should stop sending WAL to pageserver. We
|
||||||
/// terminate it if remote_consistent_lsn reached commit_lsn and there is no
|
/// terminate it if remote_consistent_lsn reached commit_lsn and there is no
|
||||||
/// computes. While there might be nothing to stream already, we learn about
|
/// computes. While there might be nothing to stream already, we learn about
|
||||||
@@ -708,8 +933,8 @@ impl FullAccessTimeline {
|
|||||||
}
|
}
|
||||||
let shared_state = self.read_shared_state().await;
|
let shared_state = self.read_shared_state().await;
|
||||||
if self.walreceivers.get_num() == 0 {
|
if self.walreceivers.get_num() == 0 {
|
||||||
return shared_state.sk.state.inmem.commit_lsn == Lsn(0) || // no data at all yet
|
return shared_state.sk.state().inmem.commit_lsn == Lsn(0) || // no data at all yet
|
||||||
reported_remote_consistent_lsn >= shared_state.sk.state.inmem.commit_lsn;
|
reported_remote_consistent_lsn >= shared_state.sk.state().inmem.commit_lsn;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -717,11 +942,11 @@ impl FullAccessTimeline {
|
|||||||
/// Ensure that current term is t, erroring otherwise, and lock the state.
|
/// Ensure that current term is t, erroring otherwise, and lock the state.
|
||||||
pub async fn acquire_term(&self, t: Term) -> Result<ReadGuardSharedState> {
|
pub async fn acquire_term(&self, t: Term) -> Result<ReadGuardSharedState> {
|
||||||
let ss = self.read_shared_state().await;
|
let ss = self.read_shared_state().await;
|
||||||
if ss.sk.state.acceptor_state.term != t {
|
if ss.sk.state().acceptor_state.term != t {
|
||||||
bail!(
|
bail!(
|
||||||
"failed to acquire term {}, current term {}",
|
"failed to acquire term {}, current term {}",
|
||||||
t,
|
t,
|
||||||
ss.sk.state.acceptor_state.term
|
ss.sk.state().acceptor_state.term
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(ss)
|
Ok(ss)
|
||||||
@@ -739,7 +964,7 @@ impl FullAccessTimeline {
|
|||||||
let mut rmsg: Option<AcceptorProposerMessage>;
|
let mut rmsg: Option<AcceptorProposerMessage>;
|
||||||
{
|
{
|
||||||
let mut shared_state = self.write_shared_state().await;
|
let mut shared_state = self.write_shared_state().await;
|
||||||
rmsg = shared_state.sk.process_msg(msg).await?;
|
rmsg = shared_state.sk.safekeeper().process_msg(msg).await?;
|
||||||
|
|
||||||
// if this is AppendResponse, fill in proper hot standby feedback.
|
// if this is AppendResponse, fill in proper hot standby feedback.
|
||||||
if let Some(AcceptorProposerMessage::AppendResponse(ref mut resp)) = rmsg {
|
if let Some(AcceptorProposerMessage::AppendResponse(ref mut resp)) = rmsg {
|
||||||
@@ -769,8 +994,141 @@ impl FullAccessTimeline {
|
|||||||
/// Update in memory remote consistent lsn.
|
/// Update in memory remote consistent lsn.
|
||||||
pub async fn update_remote_consistent_lsn(&self, candidate: Lsn) {
|
pub async fn update_remote_consistent_lsn(&self, candidate: Lsn) {
|
||||||
let mut shared_state = self.write_shared_state().await;
|
let mut shared_state = self.write_shared_state().await;
|
||||||
shared_state.sk.state.inmem.remote_consistent_lsn =
|
shared_state.sk.state_mut().inmem.remote_consistent_lsn = max(
|
||||||
max(shared_state.sk.state.inmem.remote_consistent_lsn, candidate);
|
shared_state.sk.state().inmem.remote_consistent_lsn,
|
||||||
|
candidate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This struct contains methods that are used by timeline manager task.
|
||||||
|
pub(crate) struct ManagerTimeline {
|
||||||
|
pub(crate) tli: Arc<Timeline>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for ManagerTimeline {
|
||||||
|
type Target = Arc<Timeline>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.tli
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ManagerTimeline {
|
||||||
|
pub(crate) fn timeline_dir(&self) -> &Utf8PathBuf {
|
||||||
|
&self.tli.timeline_dir
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manager requests this state on startup.
|
||||||
|
pub(crate) async fn bootstrap_mgr(&self) -> (bool, Option<PartialRemoteSegment>) {
|
||||||
|
let shared_state = self.read_shared_state().await;
|
||||||
|
let is_offloaded = matches!(
|
||||||
|
shared_state.sk.state().eviction_state,
|
||||||
|
EvictionState::Offloaded(_)
|
||||||
|
);
|
||||||
|
let partial_backup_uploaded = shared_state.sk.state().partial_backup.uploaded_segment();
|
||||||
|
|
||||||
|
(is_offloaded, partial_backup_uploaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to switch state Present->Offloaded.
|
||||||
|
pub(crate) async fn switch_to_offloaded(
|
||||||
|
&self,
|
||||||
|
partial: &PartialRemoteSegment,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut shared = self.write_shared_state().await;
|
||||||
|
|
||||||
|
// updating control file
|
||||||
|
let mut pstate = shared.sk.state_mut().start_change();
|
||||||
|
|
||||||
|
if !matches!(pstate.eviction_state, EvictionState::Present) {
|
||||||
|
bail!(
|
||||||
|
"cannot switch to offloaded state, current state is {:?}",
|
||||||
|
pstate.eviction_state
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if partial.flush_lsn != shared.sk.flush_lsn() {
|
||||||
|
bail!(
|
||||||
|
"flush_lsn mismatch in partial backup, expected {}, got {}",
|
||||||
|
shared.sk.flush_lsn(),
|
||||||
|
partial.flush_lsn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if partial.commit_lsn != pstate.commit_lsn {
|
||||||
|
bail!(
|
||||||
|
"commit_lsn mismatch in partial backup, expected {}, got {}",
|
||||||
|
pstate.commit_lsn,
|
||||||
|
partial.commit_lsn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if partial.term != shared.sk.last_log_term() {
|
||||||
|
bail!(
|
||||||
|
"term mismatch in partial backup, expected {}, got {}",
|
||||||
|
shared.sk.last_log_term(),
|
||||||
|
partial.term
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pstate.eviction_state = EvictionState::Offloaded(shared.sk.flush_lsn());
|
||||||
|
shared.sk.state_mut().finish_change(&pstate).await?;
|
||||||
|
// control file is now switched to Offloaded state
|
||||||
|
|
||||||
|
// now we can switch shared.sk to Offloaded, shouldn't fail
|
||||||
|
let prev_sk = std::mem::replace(&mut shared.sk, StateSK::Empty);
|
||||||
|
let cfile_state = prev_sk.take_state();
|
||||||
|
shared.sk = StateSK::Offloaded(Box::new(cfile_state));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to switch state Offloaded->Present.
|
||||||
|
pub(crate) async fn switch_to_present(&self) -> anyhow::Result<()> {
|
||||||
|
let conf = GlobalTimelines::get_global_config();
|
||||||
|
let mut shared = self.write_shared_state().await;
|
||||||
|
|
||||||
|
// trying to restore WAL storage
|
||||||
|
let wal_store = wal_storage::PhysicalStorage::new(
|
||||||
|
&self.ttid,
|
||||||
|
self.timeline_dir.clone(),
|
||||||
|
&conf,
|
||||||
|
shared.sk.state(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// updating control file
|
||||||
|
let mut pstate = shared.sk.state_mut().start_change();
|
||||||
|
|
||||||
|
if !matches!(pstate.eviction_state, EvictionState::Offloaded(_)) {
|
||||||
|
bail!(
|
||||||
|
"cannot switch to present state, current state is {:?}",
|
||||||
|
pstate.eviction_state
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if wal_store.flush_lsn() != shared.sk.flush_lsn() {
|
||||||
|
bail!(
|
||||||
|
"flush_lsn mismatch in restored WAL, expected {}, got {}",
|
||||||
|
shared.sk.flush_lsn(),
|
||||||
|
wal_store.flush_lsn()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pstate.eviction_state = EvictionState::Present;
|
||||||
|
shared.sk.state_mut().finish_change(&pstate).await?;
|
||||||
|
|
||||||
|
// now we can switch shared.sk to Present, shouldn't fail
|
||||||
|
let prev_sk = std::mem::replace(&mut shared.sk, StateSK::Empty);
|
||||||
|
let cfile_state = prev_sk.take_state();
|
||||||
|
shared.sk = StateSK::Loaded(SafeKeeper::new(cfile_state, wal_store, conf.my_id)?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update current manager state, useful for debugging manager deadlocks.
|
||||||
|
pub(crate) fn set_status(&self, status: timeline_manager::Status) {
|
||||||
|
self.mgr_status.store(status, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,13 +1142,13 @@ async fn delete_dir(path: &Utf8PathBuf) -> Result<bool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get a path to the tenant directory. If you just need to get a timeline directory,
|
/// Get a path to the tenant directory. If you just need to get a timeline directory,
|
||||||
/// use FullAccessTimeline::get_timeline_dir instead.
|
/// use WalResidentTimeline::get_timeline_dir instead.
|
||||||
pub(crate) fn get_tenant_dir(conf: &SafeKeeperConf, tenant_id: &TenantId) -> Utf8PathBuf {
|
pub(crate) fn get_tenant_dir(conf: &SafeKeeperConf, tenant_id: &TenantId) -> Utf8PathBuf {
|
||||||
conf.workdir.join(tenant_id.to_string())
|
conf.workdir.join(tenant_id.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a path to the timeline directory. If you need to read WAL files from disk,
|
/// Get a path to the timeline directory. If you need to read WAL files from disk,
|
||||||
/// use FullAccessTimeline::get_timeline_dir instead. This function does not check
|
/// use WalResidentTimeline::get_timeline_dir instead. This function does not check
|
||||||
/// timeline eviction status and WAL files might not be present on disk.
|
/// timeline eviction status and WAL files might not be present on disk.
|
||||||
pub(crate) fn get_timeline_dir(conf: &SafeKeeperConf, ttid: &TenantTimelineId) -> Utf8PathBuf {
|
pub(crate) fn get_timeline_dir(conf: &SafeKeeperConf, ttid: &TenantTimelineId) -> Utf8PathBuf {
|
||||||
get_tenant_dir(conf, &ttid.tenant_id).join(ttid.timeline_id.to_string())
|
get_tenant_dir(conf, &ttid.tenant_id).join(ttid.timeline_id.to_string())
|
||||||
|
|||||||
@@ -0,0 +1,366 @@
|
|||||||
|
//! Code related to evicting WAL files to remote storage. The actual upload is done by the
|
||||||
|
//! partial WAL backup code. This file has code to delete and re-download WAL files,
|
||||||
|
//! cross-validate with partial WAL backup if local file is still present.
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use camino::Utf8PathBuf;
|
||||||
|
use remote_storage::RemotePath;
|
||||||
|
use tokio::{
|
||||||
|
fs::File,
|
||||||
|
io::{AsyncRead, AsyncWriteExt},
|
||||||
|
};
|
||||||
|
use tracing::{debug, info, instrument, warn};
|
||||||
|
use utils::crashsafe::durable_rename;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
timeline_manager::{Manager, StateSnapshot},
|
||||||
|
wal_backup,
|
||||||
|
wal_backup_partial::{self, PartialRemoteSegment},
|
||||||
|
wal_storage::wal_file_paths,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
/// Returns true if the timeline is ready for eviction.
|
||||||
|
/// Current criteria:
|
||||||
|
/// - no active tasks
|
||||||
|
/// - control file is flushed (no next event scheduled)
|
||||||
|
/// - no WAL residence guards
|
||||||
|
/// - no pushes to the broker
|
||||||
|
/// - partial WAL backup is uploaded
|
||||||
|
pub(crate) fn ready_for_eviction(
|
||||||
|
&self,
|
||||||
|
next_event: &Option<tokio::time::Instant>,
|
||||||
|
state: &StateSnapshot,
|
||||||
|
) -> bool {
|
||||||
|
self.backup_task.is_none()
|
||||||
|
&& self.recovery_task.is_none()
|
||||||
|
&& self.wal_removal_task.is_none()
|
||||||
|
&& self.partial_backup_task.is_none()
|
||||||
|
&& self.partial_backup_uploaded.is_some()
|
||||||
|
&& next_event.is_none()
|
||||||
|
&& self.access_service.is_empty()
|
||||||
|
&& !self.tli_broker_active.get()
|
||||||
|
&& !wal_backup_partial::needs_uploading(state, &self.partial_backup_uploaded)
|
||||||
|
&& self
|
||||||
|
.partial_backup_uploaded
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.flush_lsn
|
||||||
|
.segment_number(self.wal_seg_size)
|
||||||
|
== self.last_removed_segno + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evict the timeline to remote storage.
|
||||||
|
#[instrument(name = "evict_timeline", skip_all)]
|
||||||
|
pub(crate) async fn evict_timeline(&mut self) {
|
||||||
|
assert!(!self.is_offloaded);
|
||||||
|
let partial_backup_uploaded = match &self.partial_backup_uploaded {
|
||||||
|
Some(p) => p.clone(),
|
||||||
|
None => {
|
||||||
|
warn!("no partial backup uploaded, skipping eviction");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("starting eviction, using {:?}", partial_backup_uploaded);
|
||||||
|
|
||||||
|
if let Err(e) = do_eviction(self, &partial_backup_uploaded).await {
|
||||||
|
warn!("failed to evict timeline: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("successfully evicted timeline");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore evicted timeline from remote storage.
|
||||||
|
#[instrument(name = "unevict_timeline", skip_all)]
|
||||||
|
pub(crate) async fn unevict_timeline(&mut self) {
|
||||||
|
assert!(self.is_offloaded);
|
||||||
|
let partial_backup_uploaded = match &self.partial_backup_uploaded {
|
||||||
|
Some(p) => p.clone(),
|
||||||
|
None => {
|
||||||
|
warn!("no partial backup uploaded, cannot unevict");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("starting uneviction, using {:?}", partial_backup_uploaded);
|
||||||
|
|
||||||
|
if let Err(e) = do_uneviction(self, &partial_backup_uploaded).await {
|
||||||
|
warn!("failed to unevict timeline: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("successfully restored evicted timeline");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that content matches the remote partial backup, if local segment exists.
|
||||||
|
/// Then change state in control file and in-memory. If `delete_offloaded_wal` is set,
|
||||||
|
/// delete the local segment.
|
||||||
|
async fn do_eviction(mgr: &mut Manager, partial: &PartialRemoteSegment) -> anyhow::Result<()> {
|
||||||
|
compare_local_segment_with_remote(mgr, partial).await?;
|
||||||
|
|
||||||
|
mgr.tli.switch_to_offloaded(partial).await?;
|
||||||
|
// switch manager state as soon as possible
|
||||||
|
mgr.is_offloaded = true;
|
||||||
|
|
||||||
|
if mgr.conf.delete_offloaded_wal {
|
||||||
|
delete_local_segment(mgr, partial).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that content matches the remote partial backup, if local segment exists.
|
||||||
|
/// Then download segment to local disk and change state in control file and in-memory.
|
||||||
|
async fn do_uneviction(mgr: &mut Manager, partial: &PartialRemoteSegment) -> anyhow::Result<()> {
|
||||||
|
// if the local segment is present, validate it
|
||||||
|
compare_local_segment_with_remote(mgr, partial).await?;
|
||||||
|
|
||||||
|
// atomically download the partial segment
|
||||||
|
redownload_partial_segment(mgr, partial).await?;
|
||||||
|
|
||||||
|
mgr.tli.switch_to_present().await?;
|
||||||
|
// switch manager state as soon as possible
|
||||||
|
mgr.is_offloaded = false;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete local WAL segment.
|
||||||
|
async fn delete_local_segment(mgr: &Manager, partial: &PartialRemoteSegment) -> anyhow::Result<()> {
|
||||||
|
let local_path = local_segment_path(mgr, partial);
|
||||||
|
|
||||||
|
info!("deleting WAL file to evict: {}", local_path);
|
||||||
|
tokio::fs::remove_file(&local_path).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Redownload partial segment from remote storage.
|
||||||
|
/// The segment is downloaded to a temporary file and then renamed to the final path.
|
||||||
|
async fn redownload_partial_segment(
|
||||||
|
mgr: &Manager,
|
||||||
|
partial: &PartialRemoteSegment,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let tmp_file = mgr.tli.timeline_dir().join("remote_partial.tmp");
|
||||||
|
let remote_segfile = remote_segment_path(mgr, partial)?;
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"redownloading partial segment: {} -> {}",
|
||||||
|
remote_segfile, tmp_file
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = wal_backup::read_object(&remote_segfile, 0).await?;
|
||||||
|
let mut file = File::create(&tmp_file).await?;
|
||||||
|
|
||||||
|
let actual_len = tokio::io::copy(&mut reader, &mut file).await?;
|
||||||
|
let expected_len = partial.flush_lsn.segment_offset(mgr.wal_seg_size);
|
||||||
|
|
||||||
|
if actual_len != expected_len as u64 {
|
||||||
|
anyhow::bail!(
|
||||||
|
"partial downloaded {} bytes, expected {}",
|
||||||
|
actual_len,
|
||||||
|
expected_len
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if actual_len > mgr.wal_seg_size as u64 {
|
||||||
|
anyhow::bail!(
|
||||||
|
"remote segment is too long: {} bytes, expected {}",
|
||||||
|
actual_len,
|
||||||
|
mgr.wal_seg_size
|
||||||
|
);
|
||||||
|
}
|
||||||
|
file.set_len(mgr.wal_seg_size as u64).await?;
|
||||||
|
file.flush().await?;
|
||||||
|
|
||||||
|
let final_path = local_segment_path(mgr, partial);
|
||||||
|
info!(
|
||||||
|
"downloaded {} bytes, renaming to {}",
|
||||||
|
final_path, final_path,
|
||||||
|
);
|
||||||
|
if let Err(e) = durable_rename(&tmp_file, &final_path, !mgr.conf.no_sync).await {
|
||||||
|
// Probably rename succeeded, but fsync of it failed. Remove
|
||||||
|
// the file then to avoid using it.
|
||||||
|
tokio::fs::remove_file(tmp_file)
|
||||||
|
.await
|
||||||
|
.or_else(utils::fs_ext::ignore_not_found)?;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare local WAL segment with partial WAL backup in remote storage.
|
||||||
|
/// If the local segment is not present, the function does nothing.
|
||||||
|
/// If the local segment is present, it compares the local segment with the remote one.
|
||||||
|
async fn compare_local_segment_with_remote(
|
||||||
|
mgr: &Manager,
|
||||||
|
partial: &PartialRemoteSegment,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let local_path = local_segment_path(mgr, partial);
|
||||||
|
|
||||||
|
match File::open(&local_path).await {
|
||||||
|
Ok(mut local_file) => do_validation(mgr, &mut local_file, mgr.wal_seg_size, partial)
|
||||||
|
.await
|
||||||
|
.context("validation failed"),
|
||||||
|
Err(_) => {
|
||||||
|
info!(
|
||||||
|
"local WAL file {} is not present, skipping validation",
|
||||||
|
local_path
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare opened local WAL segment with partial WAL backup in remote storage.
|
||||||
|
/// Validate full content of both files.
|
||||||
|
async fn do_validation(
|
||||||
|
mgr: &Manager,
|
||||||
|
file: &mut File,
|
||||||
|
wal_seg_size: usize,
|
||||||
|
partial: &PartialRemoteSegment,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let local_size = file.metadata().await?.len() as usize;
|
||||||
|
if local_size != wal_seg_size {
|
||||||
|
anyhow::bail!(
|
||||||
|
"local segment size is invalid: found {}, expected {}",
|
||||||
|
local_size,
|
||||||
|
wal_seg_size
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let remote_segfile = remote_segment_path(mgr, partial)?;
|
||||||
|
let mut remote_reader: std::pin::Pin<Box<dyn AsyncRead + Send + Sync>> =
|
||||||
|
wal_backup::read_object(&remote_segfile, 0).await?;
|
||||||
|
|
||||||
|
// remote segment should have bytes excatly up to `flush_lsn`
|
||||||
|
let expected_remote_size = partial.flush_lsn.segment_offset(mgr.wal_seg_size);
|
||||||
|
// let's compare the first `expected_remote_size` bytes
|
||||||
|
compare_n_bytes(&mut remote_reader, file, expected_remote_size).await?;
|
||||||
|
// and check that the remote segment ends here
|
||||||
|
check_end(&mut remote_reader).await?;
|
||||||
|
|
||||||
|
// if local segment is longer, the rest should be zeroes
|
||||||
|
read_n_zeroes(file, mgr.wal_seg_size - expected_remote_size).await?;
|
||||||
|
// and check that the local segment ends here
|
||||||
|
check_end(file).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_segment_path(mgr: &Manager, partial: &PartialRemoteSegment) -> Utf8PathBuf {
|
||||||
|
let flush_lsn = partial.flush_lsn;
|
||||||
|
let segno = flush_lsn.segment_number(mgr.wal_seg_size);
|
||||||
|
let (_, local_partial_segfile) =
|
||||||
|
wal_file_paths(mgr.tli.timeline_dir(), segno, mgr.wal_seg_size);
|
||||||
|
local_partial_segfile
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remote_segment_path(
|
||||||
|
mgr: &Manager,
|
||||||
|
partial: &PartialRemoteSegment,
|
||||||
|
) -> anyhow::Result<RemotePath> {
|
||||||
|
let remote_timeline_path = wal_backup::remote_timeline_path(&mgr.tli.ttid)?;
|
||||||
|
Ok(partial.remote_path(&remote_timeline_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare first `n` bytes of two readers. If the bytes differ, return an error.
|
||||||
|
/// If the readers are shorter than `n`, return an error.
|
||||||
|
async fn compare_n_bytes<R1, R2>(reader1: &mut R1, reader2: &mut R2, n: usize) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
R1: AsyncRead + Unpin,
|
||||||
|
R2: AsyncRead + Unpin,
|
||||||
|
{
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
const BUF_SIZE: usize = 32 * 1024;
|
||||||
|
|
||||||
|
let mut buffer1 = vec![0u8; BUF_SIZE];
|
||||||
|
let mut buffer2 = vec![0u8; BUF_SIZE];
|
||||||
|
|
||||||
|
let mut offset = 0;
|
||||||
|
|
||||||
|
while offset < n {
|
||||||
|
let bytes_to_read = std::cmp::min(BUF_SIZE, n - offset);
|
||||||
|
|
||||||
|
let bytes_read1 = reader1
|
||||||
|
.read(&mut buffer1[..bytes_to_read])
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("failed to read from reader1 at offset {}", offset))?;
|
||||||
|
if bytes_read1 == 0 {
|
||||||
|
anyhow::bail!("unexpected EOF from reader1 at offset {}", offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes_read2 = reader2
|
||||||
|
.read_exact(&mut buffer2[..bytes_read1])
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to read {} bytes from reader2 at offset {}",
|
||||||
|
bytes_read1, offset
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
assert!(bytes_read2 == bytes_read1);
|
||||||
|
|
||||||
|
if buffer1[..bytes_read1] != buffer2[..bytes_read2] {
|
||||||
|
let diff_offset = buffer1[..bytes_read1]
|
||||||
|
.iter()
|
||||||
|
.zip(buffer2[..bytes_read2].iter())
|
||||||
|
.position(|(a, b)| a != b)
|
||||||
|
.expect("mismatched buffers, but no difference found");
|
||||||
|
anyhow::bail!("mismatch at offset {}", offset + diff_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += bytes_read1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_end<R>(mut reader: R) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
R: AsyncRead + Unpin,
|
||||||
|
{
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
let mut buffer = [0u8; 1];
|
||||||
|
let bytes_read = reader.read(&mut buffer).await?;
|
||||||
|
if bytes_read != 0 {
|
||||||
|
anyhow::bail!("expected EOF, found bytes");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_n_zeroes<R>(reader: &mut R, n: usize) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
R: AsyncRead + Unpin,
|
||||||
|
{
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
const BUF_SIZE: usize = 32 * 1024;
|
||||||
|
let mut buffer = vec![0u8; BUF_SIZE];
|
||||||
|
let mut offset = 0;
|
||||||
|
|
||||||
|
while offset < n {
|
||||||
|
let bytes_to_read = std::cmp::min(BUF_SIZE, n - offset);
|
||||||
|
|
||||||
|
let bytes_read = reader
|
||||||
|
.read(&mut buffer[..bytes_to_read])
|
||||||
|
.await
|
||||||
|
.context("expected zeroes, got read error")?;
|
||||||
|
if bytes_read == 0 {
|
||||||
|
anyhow::bail!("expected zeroes, got EOF");
|
||||||
|
}
|
||||||
|
|
||||||
|
if buffer[..bytes_read].iter().all(|&b| b == 0) {
|
||||||
|
offset += bytes_read;
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("non-zero byte found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
//! Timeline residence guard is needed to ensure that WAL segments are present on disk,
|
||||||
|
//! as long as the code is holding the guard. This file implements guard logic, to issue
|
||||||
|
//! and drop guards, and to notify the manager when the guard is dropped.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
|
use crate::timeline_manager::ManagerCtlMessage;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct GuardId(u64);
|
||||||
|
|
||||||
|
pub struct ResidenceGuard {
|
||||||
|
manager_tx: tokio::sync::mpsc::UnboundedSender<ManagerCtlMessage>,
|
||||||
|
guard_id: GuardId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ResidenceGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// notify the manager that the guard is dropped
|
||||||
|
let res = self
|
||||||
|
.manager_tx
|
||||||
|
.send(ManagerCtlMessage::GuardDrop(self.guard_id));
|
||||||
|
if let Err(e) = res {
|
||||||
|
warn!("failed to send GuardDrop message: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AccessService is responsible for issuing and dropping residence guards.
|
||||||
|
/// All guards are stored in the `guards` set.
|
||||||
|
/// TODO: it's possible to add `String` name to each guard, for better observability.
|
||||||
|
pub(crate) struct AccessService {
|
||||||
|
next_guard_id: u64,
|
||||||
|
guards: HashSet<u64>,
|
||||||
|
manager_tx: tokio::sync::mpsc::UnboundedSender<ManagerCtlMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccessService {
|
||||||
|
pub(crate) fn new(manager_tx: tokio::sync::mpsc::UnboundedSender<ManagerCtlMessage>) -> Self {
|
||||||
|
Self {
|
||||||
|
next_guard_id: 0,
|
||||||
|
guards: HashSet::new(),
|
||||||
|
manager_tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_empty(&self) -> bool {
|
||||||
|
self.guards.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create_guard(&mut self) -> ResidenceGuard {
|
||||||
|
let guard_id = self.next_guard_id;
|
||||||
|
self.next_guard_id += 1;
|
||||||
|
self.guards.insert(guard_id);
|
||||||
|
|
||||||
|
let guard_id = GuardId(guard_id);
|
||||||
|
debug!("issued a new guard {:?}", guard_id);
|
||||||
|
|
||||||
|
ResidenceGuard {
|
||||||
|
manager_tx: self.manager_tx.clone(),
|
||||||
|
guard_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn drop_guard(&mut self, guard_id: GuardId) {
|
||||||
|
debug!("dropping guard {:?}", guard_id);
|
||||||
|
assert!(self.guards.remove(&guard_id.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
+526
-259
@@ -2,66 +2,83 @@
|
|||||||
//! It is spawned alongside each timeline and exits when the timeline is deleted.
|
//! It is spawned alongside each timeline and exits when the timeline is deleted.
|
||||||
//! It watches for changes in the timeline state and decides when to spawn or kill background tasks.
|
//! It watches for changes in the timeline state and decides when to spawn or kill background tasks.
|
||||||
//! It also can manage some reactive state, like should the timeline be active for broker pushes or not.
|
//! It also can manage some reactive state, like should the timeline be active for broker pushes or not.
|
||||||
|
//!
|
||||||
|
//! Be aware that you need to be extra careful with manager code, because it is not respawned on panic.
|
||||||
|
//! Also, if it will stuck in some branch, it will prevent any further progress in the timeline.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
sync::Arc,
|
sync::{atomic::AtomicUsize, Arc},
|
||||||
time::{Duration, Instant},
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use postgres_ffi::XLogSegNo;
|
use postgres_ffi::XLogSegNo;
|
||||||
use tokio::task::{JoinError, JoinHandle};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::{info, info_span, instrument, warn, Instrument};
|
use tokio::{
|
||||||
|
task::{JoinError, JoinHandle},
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
use tracing::{debug, info, info_span, instrument, warn, Instrument};
|
||||||
use utils::lsn::Lsn;
|
use utils::lsn::Lsn;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
control_file::Storage,
|
control_file::{FileStorage, Storage},
|
||||||
metrics::{MANAGER_ACTIVE_CHANGES, MANAGER_ITERATIONS_TOTAL},
|
metrics::{MANAGER_ACTIVE_CHANGES, MANAGER_ITERATIONS_TOTAL, MISC_OPERATION_SECONDS},
|
||||||
recovery::recovery_main,
|
recovery::recovery_main,
|
||||||
remove_wal::calc_horizon_lsn,
|
remove_wal::calc_horizon_lsn,
|
||||||
|
safekeeper::Term,
|
||||||
send_wal::WalSenders,
|
send_wal::WalSenders,
|
||||||
timeline::{PeerInfo, ReadGuardSharedState, Timeline},
|
state::TimelineState,
|
||||||
|
timeline::{ManagerTimeline, PeerInfo, ReadGuardSharedState, StateSK, WalResidentTimeline},
|
||||||
|
timeline_guard::{AccessService, GuardId, ResidenceGuard},
|
||||||
timelines_set::{TimelineSetGuard, TimelinesSet},
|
timelines_set::{TimelineSetGuard, TimelinesSet},
|
||||||
wal_backup::{self, WalBackupTaskHandle},
|
wal_backup::{self, WalBackupTaskHandle},
|
||||||
wal_backup_partial, SafeKeeperConf,
|
wal_backup_partial::{self, PartialRemoteSegment},
|
||||||
|
SafeKeeperConf,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct StateSnapshot {
|
pub(crate) struct StateSnapshot {
|
||||||
// inmem values
|
// inmem values
|
||||||
pub commit_lsn: Lsn,
|
pub(crate) commit_lsn: Lsn,
|
||||||
pub backup_lsn: Lsn,
|
pub(crate) backup_lsn: Lsn,
|
||||||
pub remote_consistent_lsn: Lsn,
|
pub(crate) remote_consistent_lsn: Lsn,
|
||||||
|
|
||||||
// persistent control file values
|
// persistent control file values
|
||||||
pub cfile_peer_horizon_lsn: Lsn,
|
pub(crate) cfile_peer_horizon_lsn: Lsn,
|
||||||
pub cfile_remote_consistent_lsn: Lsn,
|
pub(crate) cfile_remote_consistent_lsn: Lsn,
|
||||||
pub cfile_backup_lsn: Lsn,
|
pub(crate) cfile_backup_lsn: Lsn,
|
||||||
|
|
||||||
|
// latest state
|
||||||
|
pub(crate) flush_lsn: Lsn,
|
||||||
|
pub(crate) last_log_term: Term,
|
||||||
|
|
||||||
// misc
|
// misc
|
||||||
pub cfile_last_persist_at: Instant,
|
pub(crate) cfile_last_persist_at: std::time::Instant,
|
||||||
pub inmem_flush_pending: bool,
|
pub(crate) inmem_flush_pending: bool,
|
||||||
pub wal_removal_on_hold: bool,
|
pub(crate) wal_removal_on_hold: bool,
|
||||||
pub peers: Vec<PeerInfo>,
|
pub(crate) peers: Vec<PeerInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StateSnapshot {
|
impl StateSnapshot {
|
||||||
/// Create a new snapshot of the timeline state.
|
/// Create a new snapshot of the timeline state.
|
||||||
fn new(read_guard: ReadGuardSharedState, heartbeat_timeout: Duration) -> Self {
|
fn new(read_guard: ReadGuardSharedState, heartbeat_timeout: Duration) -> Self {
|
||||||
|
let state = read_guard.sk.state();
|
||||||
Self {
|
Self {
|
||||||
commit_lsn: read_guard.sk.state.inmem.commit_lsn,
|
commit_lsn: state.inmem.commit_lsn,
|
||||||
backup_lsn: read_guard.sk.state.inmem.backup_lsn,
|
backup_lsn: state.inmem.backup_lsn,
|
||||||
remote_consistent_lsn: read_guard.sk.state.inmem.remote_consistent_lsn,
|
remote_consistent_lsn: state.inmem.remote_consistent_lsn,
|
||||||
cfile_peer_horizon_lsn: read_guard.sk.state.peer_horizon_lsn,
|
cfile_peer_horizon_lsn: state.peer_horizon_lsn,
|
||||||
cfile_remote_consistent_lsn: read_guard.sk.state.remote_consistent_lsn,
|
cfile_remote_consistent_lsn: state.remote_consistent_lsn,
|
||||||
cfile_backup_lsn: read_guard.sk.state.backup_lsn,
|
cfile_backup_lsn: state.backup_lsn,
|
||||||
cfile_last_persist_at: read_guard.sk.state.pers.last_persist_at(),
|
flush_lsn: read_guard.sk.flush_lsn(),
|
||||||
inmem_flush_pending: Self::has_unflushed_inmem_state(&read_guard),
|
last_log_term: read_guard.sk.last_log_term(),
|
||||||
|
cfile_last_persist_at: state.pers.last_persist_at(),
|
||||||
|
inmem_flush_pending: Self::has_unflushed_inmem_state(state),
|
||||||
wal_removal_on_hold: read_guard.wal_removal_on_hold,
|
wal_removal_on_hold: read_guard.wal_removal_on_hold,
|
||||||
peers: read_guard.get_peers(heartbeat_timeout),
|
peers: read_guard.get_peers(heartbeat_timeout),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_unflushed_inmem_state(read_guard: &ReadGuardSharedState) -> bool {
|
fn has_unflushed_inmem_state(state: &TimelineState<FileStorage>) -> bool {
|
||||||
let state = &read_guard.sk.state;
|
|
||||||
state.inmem.commit_lsn > state.commit_lsn
|
state.inmem.commit_lsn > state.commit_lsn
|
||||||
|| state.inmem.backup_lsn > state.backup_lsn
|
|| state.inmem.backup_lsn > state.backup_lsn
|
||||||
|| state.inmem.peer_horizon_lsn > state.peer_horizon_lsn
|
|| state.inmem.peer_horizon_lsn > state.peer_horizon_lsn
|
||||||
@@ -73,314 +90,564 @@ impl StateSnapshot {
|
|||||||
/// There is no need to check for updates more often than this.
|
/// There is no need to check for updates more often than this.
|
||||||
const REFRESH_INTERVAL: Duration = Duration::from_millis(300);
|
const REFRESH_INTERVAL: Duration = Duration::from_millis(300);
|
||||||
|
|
||||||
/// How often to save the control file if the is no other activity.
|
pub enum ManagerCtlMessage {
|
||||||
const CF_SAVE_INTERVAL: Duration = Duration::from_secs(300);
|
/// Request to get a guard for WalResidentTimeline, with WAL files available locally.
|
||||||
|
GuardRequest(tokio::sync::oneshot::Sender<anyhow::Result<ResidenceGuard>>),
|
||||||
|
/// Request to drop the guard.
|
||||||
|
GuardDrop(GuardId),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for ManagerCtlMessage {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ManagerCtlMessage::GuardRequest(_) => write!(f, "GuardRequest"),
|
||||||
|
ManagerCtlMessage::GuardDrop(id) => write!(f, "GuardDrop({:?})", id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ManagerCtl {
|
||||||
|
manager_tx: tokio::sync::mpsc::UnboundedSender<ManagerCtlMessage>,
|
||||||
|
|
||||||
|
// this is used to initialize manager, it will be moved out in bootstrap().
|
||||||
|
init_manager_rx:
|
||||||
|
std::sync::Mutex<Option<tokio::sync::mpsc::UnboundedReceiver<ManagerCtlMessage>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ManagerCtl {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ManagerCtl {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
Self {
|
||||||
|
manager_tx: tx,
|
||||||
|
init_manager_rx: std::sync::Mutex::new(Some(rx)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Issue a new guard and wait for manager to prepare the timeline.
|
||||||
|
/// Sends a message to the manager and waits for the response.
|
||||||
|
/// Can be blocked indefinitely if the manager is stuck.
|
||||||
|
pub async fn wal_residence_guard(&self) -> anyhow::Result<ResidenceGuard> {
|
||||||
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
|
self.manager_tx.send(ManagerCtlMessage::GuardRequest(tx))?;
|
||||||
|
|
||||||
|
// wait for the manager to respond with the guard
|
||||||
|
rx.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("response read fail: {:?}", e))
|
||||||
|
.and_then(std::convert::identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Must be called exactly once to bootstrap the manager.
|
||||||
|
pub fn bootstrap_manager(
|
||||||
|
&self,
|
||||||
|
) -> (
|
||||||
|
tokio::sync::mpsc::UnboundedSender<ManagerCtlMessage>,
|
||||||
|
tokio::sync::mpsc::UnboundedReceiver<ManagerCtlMessage>,
|
||||||
|
) {
|
||||||
|
let rx = self
|
||||||
|
.init_manager_rx
|
||||||
|
.lock()
|
||||||
|
.expect("mutex init_manager_rx poisoned")
|
||||||
|
.take()
|
||||||
|
.expect("manager already bootstrapped");
|
||||||
|
|
||||||
|
(self.manager_tx.clone(), rx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Manager {
|
||||||
|
// configuration & dependencies
|
||||||
|
pub(crate) tli: ManagerTimeline,
|
||||||
|
pub(crate) conf: SafeKeeperConf,
|
||||||
|
pub(crate) wal_seg_size: usize,
|
||||||
|
pub(crate) walsenders: Arc<WalSenders>,
|
||||||
|
|
||||||
|
// current state
|
||||||
|
pub(crate) state_version_rx: tokio::sync::watch::Receiver<usize>,
|
||||||
|
pub(crate) num_computes_rx: tokio::sync::watch::Receiver<usize>,
|
||||||
|
pub(crate) tli_broker_active: TimelineSetGuard,
|
||||||
|
pub(crate) last_removed_segno: XLogSegNo,
|
||||||
|
pub(crate) is_offloaded: bool,
|
||||||
|
|
||||||
|
// background tasks
|
||||||
|
pub(crate) backup_task: Option<WalBackupTaskHandle>,
|
||||||
|
pub(crate) recovery_task: Option<JoinHandle<()>>,
|
||||||
|
pub(crate) wal_removal_task: Option<JoinHandle<anyhow::Result<u64>>>,
|
||||||
|
|
||||||
|
// partial backup
|
||||||
|
pub(crate) partial_backup_task: Option<JoinHandle<Option<PartialRemoteSegment>>>,
|
||||||
|
pub(crate) partial_backup_uploaded: Option<PartialRemoteSegment>,
|
||||||
|
|
||||||
|
// misc
|
||||||
|
pub(crate) access_service: AccessService,
|
||||||
|
}
|
||||||
|
|
||||||
/// This task gets spawned alongside each timeline and is responsible for managing the timeline's
|
/// This task gets spawned alongside each timeline and is responsible for managing the timeline's
|
||||||
/// background tasks.
|
/// background tasks.
|
||||||
/// Be careful, this task is not respawned on panic, so it should not panic.
|
/// Be careful, this task is not respawned on panic, so it should not panic.
|
||||||
#[instrument(name = "manager", skip_all, fields(ttid = %tli.ttid))]
|
#[instrument(name = "manager", skip_all, fields(ttid = %tli.ttid))]
|
||||||
pub async fn main_task(
|
pub async fn main_task(
|
||||||
tli: Arc<Timeline>,
|
tli: ManagerTimeline,
|
||||||
conf: SafeKeeperConf,
|
conf: SafeKeeperConf,
|
||||||
broker_active_set: Arc<TimelinesSet>,
|
broker_active_set: Arc<TimelinesSet>,
|
||||||
|
manager_tx: tokio::sync::mpsc::UnboundedSender<ManagerCtlMessage>,
|
||||||
|
mut manager_rx: tokio::sync::mpsc::UnboundedReceiver<ManagerCtlMessage>,
|
||||||
) {
|
) {
|
||||||
|
tli.set_status(Status::Started);
|
||||||
|
|
||||||
|
let defer_tli = tli.tli.clone();
|
||||||
scopeguard::defer! {
|
scopeguard::defer! {
|
||||||
if tli.is_cancelled() {
|
if defer_tli.is_cancelled() {
|
||||||
info!("manager task finished");
|
info!("manager task finished");
|
||||||
} else {
|
} else {
|
||||||
warn!("manager task finished prematurely");
|
warn!("manager task finished prematurely");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// configuration & dependencies
|
let mut mgr = Manager::new(tli, conf, broker_active_set, manager_tx).await;
|
||||||
let wal_seg_size = tli.get_wal_seg_size().await;
|
|
||||||
let heartbeat_timeout = conf.heartbeat_timeout;
|
|
||||||
let walsenders = tli.get_walsenders();
|
|
||||||
let walreceivers = tli.get_walreceivers();
|
|
||||||
|
|
||||||
// current state
|
|
||||||
let mut state_version_rx = tli.get_state_version_rx();
|
|
||||||
let mut num_computes_rx = walreceivers.get_num_rx();
|
|
||||||
let mut tli_broker_active = broker_active_set.guard(tli.clone());
|
|
||||||
let mut last_removed_segno = 0 as XLogSegNo;
|
|
||||||
|
|
||||||
// list of background tasks
|
|
||||||
let mut backup_task: Option<WalBackupTaskHandle> = None;
|
|
||||||
let mut recovery_task: Option<JoinHandle<()>> = None;
|
|
||||||
let mut partial_backup_task: Option<JoinHandle<()>> = None;
|
|
||||||
let mut wal_removal_task: Option<JoinHandle<anyhow::Result<u64>>> = None;
|
|
||||||
|
|
||||||
// Start recovery task which always runs on the timeline.
|
// Start recovery task which always runs on the timeline.
|
||||||
if conf.peer_recovery_enabled {
|
if !mgr.is_offloaded && mgr.conf.peer_recovery_enabled {
|
||||||
match tli.full_access_guard().await {
|
let tli = mgr.wal_resident_timeline();
|
||||||
Ok(tli) => {
|
mgr.recovery_task = Some(tokio::spawn(recovery_main(tli, mgr.conf.clone())));
|
||||||
recovery_task = Some(tokio::spawn(recovery_main(tli, conf.clone())));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("failed to start recovery task: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start partial backup task which always runs on the timeline.
|
|
||||||
if conf.is_wal_backup_enabled() && conf.partial_backup_enabled {
|
|
||||||
match tli.full_access_guard().await {
|
|
||||||
Ok(tli) => {
|
|
||||||
partial_backup_task = Some(tokio::spawn(wal_backup_partial::main_task(
|
|
||||||
tli,
|
|
||||||
conf.clone(),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("failed to start partial backup task: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let last_state = 'outer: loop {
|
let last_state = 'outer: loop {
|
||||||
MANAGER_ITERATIONS_TOTAL.inc();
|
MANAGER_ITERATIONS_TOTAL.inc();
|
||||||
|
|
||||||
let state_snapshot = StateSnapshot::new(tli.read_shared_state().await, heartbeat_timeout);
|
mgr.set_status(Status::StateSnapshot);
|
||||||
let num_computes = *num_computes_rx.borrow();
|
let state_snapshot = mgr.state_snapshot().await;
|
||||||
|
|
||||||
let is_wal_backup_required = update_backup(
|
let mut next_event: Option<Instant> = None;
|
||||||
&conf,
|
if !mgr.is_offloaded {
|
||||||
&tli,
|
let num_computes = *mgr.num_computes_rx.borrow();
|
||||||
wal_seg_size,
|
|
||||||
num_computes,
|
|
||||||
&state_snapshot,
|
|
||||||
&mut backup_task,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let _is_active = update_is_active(
|
mgr.set_status(Status::UpdateBackup);
|
||||||
is_wal_backup_required,
|
let is_wal_backup_required = mgr.update_backup(num_computes, &state_snapshot).await;
|
||||||
num_computes,
|
mgr.update_is_active(is_wal_backup_required, num_computes, &state_snapshot);
|
||||||
&state_snapshot,
|
|
||||||
&mut tli_broker_active,
|
|
||||||
&tli,
|
|
||||||
);
|
|
||||||
|
|
||||||
let next_cfile_save = update_control_file_save(&state_snapshot, &tli).await;
|
mgr.set_status(Status::UpdateControlFile);
|
||||||
|
mgr.update_control_file_save(&state_snapshot, &mut next_event)
|
||||||
|
.await;
|
||||||
|
|
||||||
update_wal_removal(
|
mgr.set_status(Status::UpdateWalRemoval);
|
||||||
&conf,
|
mgr.update_wal_removal(&state_snapshot).await;
|
||||||
walsenders,
|
|
||||||
&tli,
|
|
||||||
wal_seg_size,
|
|
||||||
&state_snapshot,
|
|
||||||
last_removed_segno,
|
|
||||||
&mut wal_removal_task,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
|
mgr.set_status(Status::UpdatePartialBackup);
|
||||||
|
mgr.update_partial_backup(&state_snapshot).await;
|
||||||
|
|
||||||
|
if mgr.conf.enable_offload && mgr.ready_for_eviction(&next_event, &state_snapshot) {
|
||||||
|
mgr.set_status(Status::EvictTimeline);
|
||||||
|
mgr.evict_timeline().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr.set_status(Status::Wait);
|
||||||
// wait until something changes. tx channels are stored under Arc, so they will not be
|
// wait until something changes. tx channels are stored under Arc, so they will not be
|
||||||
// dropped until the manager task is finished.
|
// dropped until the manager task is finished.
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = tli.cancel.cancelled() => {
|
_ = mgr.tli.cancel.cancelled() => {
|
||||||
// timeline was deleted
|
// timeline was deleted
|
||||||
break 'outer state_snapshot;
|
break 'outer state_snapshot;
|
||||||
}
|
}
|
||||||
_ = async {
|
_ = async {
|
||||||
// don't wake up on every state change, but at most every REFRESH_INTERVAL
|
// don't wake up on every state change, but at most every REFRESH_INTERVAL
|
||||||
tokio::time::sleep(REFRESH_INTERVAL).await;
|
tokio::time::sleep(REFRESH_INTERVAL).await;
|
||||||
let _ = state_version_rx.changed().await;
|
let _ = mgr.state_version_rx.changed().await;
|
||||||
} => {
|
} => {
|
||||||
// state was updated
|
// state was updated
|
||||||
}
|
}
|
||||||
_ = num_computes_rx.changed() => {
|
_ = mgr.num_computes_rx.changed() => {
|
||||||
// number of connected computes was updated
|
// number of connected computes was updated
|
||||||
}
|
}
|
||||||
_ = async {
|
_ = sleep_until(&next_event) => {
|
||||||
if let Some(timeout) = next_cfile_save {
|
// we were waiting for some event (e.g. cfile save)
|
||||||
tokio::time::sleep_until(timeout).await
|
|
||||||
} else {
|
|
||||||
futures::future::pending().await
|
|
||||||
}
|
|
||||||
} => {
|
|
||||||
// it's time to save the control file
|
|
||||||
}
|
}
|
||||||
res = async {
|
res = await_task_finish(&mut mgr.wal_removal_task) => {
|
||||||
if let Some(task) = &mut wal_removal_task {
|
|
||||||
task.await
|
|
||||||
} else {
|
|
||||||
futures::future::pending().await
|
|
||||||
}
|
|
||||||
} => {
|
|
||||||
// WAL removal task finished
|
// WAL removal task finished
|
||||||
wal_removal_task = None;
|
mgr.wal_removal_task = None;
|
||||||
update_wal_removal_end(res, &tli, &mut last_removed_segno);
|
mgr.update_wal_removal_end(res);
|
||||||
|
}
|
||||||
|
res = await_task_finish(&mut mgr.partial_backup_task) => {
|
||||||
|
// partial backup task finished
|
||||||
|
mgr.partial_backup_task = None;
|
||||||
|
mgr.update_partial_backup_end(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = manager_rx.recv() => {
|
||||||
|
mgr.set_status(Status::HandleMessage);
|
||||||
|
mgr.handle_message(msg).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
mgr.set_status(Status::Exiting);
|
||||||
|
|
||||||
// remove timeline from the broker active set sooner, before waiting for background tasks
|
// remove timeline from the broker active set sooner, before waiting for background tasks
|
||||||
tli_broker_active.set(false);
|
mgr.tli_broker_active.set(false);
|
||||||
|
|
||||||
// shutdown background tasks
|
// shutdown background tasks
|
||||||
if conf.is_wal_backup_enabled() {
|
if mgr.conf.is_wal_backup_enabled() {
|
||||||
wal_backup::update_task(&conf, &tli, false, &last_state, &mut backup_task).await;
|
wal_backup::update_task(&mut mgr, false, &last_state).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(recovery_task) = recovery_task {
|
if let Some(recovery_task) = &mut mgr.recovery_task {
|
||||||
if let Err(e) = recovery_task.await {
|
if let Err(e) = recovery_task.await {
|
||||||
warn!("recovery task failed: {:?}", e);
|
warn!("recovery task failed: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(partial_backup_task) = partial_backup_task {
|
if let Some(partial_backup_task) = &mut mgr.partial_backup_task {
|
||||||
if let Err(e) = partial_backup_task.await {
|
if let Err(e) = partial_backup_task.await {
|
||||||
warn!("partial backup task failed: {:?}", e);
|
warn!("partial backup task failed: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(wal_removal_task) = wal_removal_task {
|
if let Some(wal_removal_task) = &mut mgr.wal_removal_task {
|
||||||
let res = wal_removal_task.await;
|
let res = wal_removal_task.await;
|
||||||
update_wal_removal_end(res, &tli, &mut last_removed_segno);
|
mgr.update_wal_removal_end(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mgr.set_status(Status::Finished);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawns/kills backup task and returns true if backup is required.
|
impl Manager {
|
||||||
async fn update_backup(
|
async fn new(
|
||||||
conf: &SafeKeeperConf,
|
tli: ManagerTimeline,
|
||||||
tli: &Arc<Timeline>,
|
conf: SafeKeeperConf,
|
||||||
wal_seg_size: usize,
|
broker_active_set: Arc<TimelinesSet>,
|
||||||
num_computes: usize,
|
manager_tx: tokio::sync::mpsc::UnboundedSender<ManagerCtlMessage>,
|
||||||
state: &StateSnapshot,
|
) -> Manager {
|
||||||
backup_task: &mut Option<WalBackupTaskHandle>,
|
let (is_offloaded, partial_backup_uploaded) = tli.bootstrap_mgr().await;
|
||||||
) -> bool {
|
Manager {
|
||||||
let is_wal_backup_required =
|
conf,
|
||||||
wal_backup::is_wal_backup_required(wal_seg_size, num_computes, state);
|
wal_seg_size: tli.get_wal_seg_size().await,
|
||||||
|
walsenders: tli.get_walsenders().clone(),
|
||||||
if conf.is_wal_backup_enabled() {
|
state_version_rx: tli.get_state_version_rx(),
|
||||||
wal_backup::update_task(conf, tli, is_wal_backup_required, state, backup_task).await;
|
num_computes_rx: tli.get_walreceivers().get_num_rx(),
|
||||||
|
tli_broker_active: broker_active_set.guard(tli.clone()),
|
||||||
|
last_removed_segno: 0,
|
||||||
|
is_offloaded,
|
||||||
|
backup_task: None,
|
||||||
|
recovery_task: None,
|
||||||
|
wal_removal_task: None,
|
||||||
|
partial_backup_task: None,
|
||||||
|
partial_backup_uploaded,
|
||||||
|
access_service: AccessService::new(manager_tx),
|
||||||
|
tli,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the state in Arc<Timeline>
|
fn set_status(&self, status: Status) {
|
||||||
tli.wal_backup_active
|
self.tli.set_status(status);
|
||||||
.store(backup_task.is_some(), std::sync::atomic::Ordering::Relaxed);
|
|
||||||
is_wal_backup_required
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update is_active flag and returns its value.
|
|
||||||
fn update_is_active(
|
|
||||||
is_wal_backup_required: bool,
|
|
||||||
num_computes: usize,
|
|
||||||
state: &StateSnapshot,
|
|
||||||
tli_broker_active: &mut TimelineSetGuard,
|
|
||||||
tli: &Arc<Timeline>,
|
|
||||||
) -> bool {
|
|
||||||
let is_active = is_wal_backup_required
|
|
||||||
|| num_computes > 0
|
|
||||||
|| state.remote_consistent_lsn < state.commit_lsn;
|
|
||||||
|
|
||||||
// update the broker timeline set
|
|
||||||
if tli_broker_active.set(is_active) {
|
|
||||||
// write log if state has changed
|
|
||||||
info!(
|
|
||||||
"timeline active={} now, remote_consistent_lsn={}, commit_lsn={}",
|
|
||||||
is_active, state.remote_consistent_lsn, state.commit_lsn,
|
|
||||||
);
|
|
||||||
|
|
||||||
MANAGER_ACTIVE_CHANGES.inc();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the state in Arc<Timeline>
|
/// Get a WalResidentTimeline.
|
||||||
tli.broker_active
|
/// Manager code must use this function instead of one from `Timeline`
|
||||||
.store(is_active, std::sync::atomic::Ordering::Relaxed);
|
/// directly, because it will deadlock.
|
||||||
is_active
|
pub(crate) fn wal_resident_timeline(&mut self) -> WalResidentTimeline {
|
||||||
}
|
assert!(!self.is_offloaded);
|
||||||
|
let guard = self.access_service.create_guard();
|
||||||
/// Save control file if needed. Returns Instant if we should persist the control file in the future.
|
WalResidentTimeline::new(self.tli.clone(), guard)
|
||||||
async fn update_control_file_save(
|
|
||||||
state: &StateSnapshot,
|
|
||||||
tli: &Arc<Timeline>,
|
|
||||||
) -> Option<tokio::time::Instant> {
|
|
||||||
if !state.inmem_flush_pending {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.cfile_last_persist_at.elapsed() > CF_SAVE_INTERVAL {
|
/// Get a snapshot of the timeline state.
|
||||||
let mut write_guard = tli.write_shared_state().await;
|
async fn state_snapshot(&self) -> StateSnapshot {
|
||||||
// this can be done in the background because it blocks manager task, but flush() should
|
let _timer = MISC_OPERATION_SECONDS
|
||||||
// be fast enough not to be a problem now
|
.with_label_values(&["state_snapshot"])
|
||||||
if let Err(e) = write_guard.sk.state.flush().await {
|
.start_timer();
|
||||||
warn!("failed to save control file: {:?}", e);
|
|
||||||
|
StateSnapshot::new(
|
||||||
|
self.tli.read_shared_state().await,
|
||||||
|
self.conf.heartbeat_timeout,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns/kills backup task and returns true if backup is required.
|
||||||
|
async fn update_backup(&mut self, num_computes: usize, state: &StateSnapshot) -> bool {
|
||||||
|
let is_wal_backup_required =
|
||||||
|
wal_backup::is_wal_backup_required(self.wal_seg_size, num_computes, state);
|
||||||
|
|
||||||
|
if self.conf.is_wal_backup_enabled() {
|
||||||
|
wal_backup::update_task(self, is_wal_backup_required, state).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
// update the state in Arc<Timeline>
|
||||||
} else {
|
self.tli.wal_backup_active.store(
|
||||||
// we should wait until next CF_SAVE_INTERVAL
|
self.backup_task.is_some(),
|
||||||
Some((state.cfile_last_persist_at + CF_SAVE_INTERVAL).into())
|
std::sync::atomic::Ordering::Relaxed,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Spawns WAL removal task if needed.
|
|
||||||
async fn update_wal_removal(
|
|
||||||
conf: &SafeKeeperConf,
|
|
||||||
walsenders: &Arc<WalSenders>,
|
|
||||||
tli: &Arc<Timeline>,
|
|
||||||
wal_seg_size: usize,
|
|
||||||
state: &StateSnapshot,
|
|
||||||
last_removed_segno: u64,
|
|
||||||
wal_removal_task: &mut Option<JoinHandle<anyhow::Result<u64>>>,
|
|
||||||
) {
|
|
||||||
if wal_removal_task.is_some() || state.wal_removal_on_hold {
|
|
||||||
// WAL removal is already in progress or hold off
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If enabled, we use LSN of the most lagging walsender as a WAL removal horizon.
|
|
||||||
// This allows to get better read speed for pageservers that are lagging behind,
|
|
||||||
// at the cost of keeping more WAL on disk.
|
|
||||||
let replication_horizon_lsn = if conf.walsenders_keep_horizon {
|
|
||||||
walsenders.laggard_lsn()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let removal_horizon_lsn = calc_horizon_lsn(state, replication_horizon_lsn);
|
|
||||||
let removal_horizon_segno = removal_horizon_lsn
|
|
||||||
.segment_number(wal_seg_size)
|
|
||||||
.saturating_sub(1);
|
|
||||||
|
|
||||||
if removal_horizon_segno > last_removed_segno {
|
|
||||||
// we need to remove WAL
|
|
||||||
let remover = crate::wal_storage::Storage::remove_up_to(
|
|
||||||
&tli.read_shared_state().await.sk.wal_store,
|
|
||||||
removal_horizon_segno,
|
|
||||||
);
|
);
|
||||||
*wal_removal_task = Some(tokio::spawn(
|
is_wal_backup_required
|
||||||
async move {
|
}
|
||||||
remover.await?;
|
|
||||||
Ok(removal_horizon_segno)
|
/// Update is_active flag and returns its value.
|
||||||
|
fn update_is_active(
|
||||||
|
&mut self,
|
||||||
|
is_wal_backup_required: bool,
|
||||||
|
num_computes: usize,
|
||||||
|
state: &StateSnapshot,
|
||||||
|
) {
|
||||||
|
let is_active = is_wal_backup_required
|
||||||
|
|| num_computes > 0
|
||||||
|
|| state.remote_consistent_lsn < state.commit_lsn;
|
||||||
|
|
||||||
|
// update the broker timeline set
|
||||||
|
if self.tli_broker_active.set(is_active) {
|
||||||
|
// write log if state has changed
|
||||||
|
info!(
|
||||||
|
"timeline active={} now, remote_consistent_lsn={}, commit_lsn={}",
|
||||||
|
is_active, state.remote_consistent_lsn, state.commit_lsn,
|
||||||
|
);
|
||||||
|
|
||||||
|
MANAGER_ACTIVE_CHANGES.inc();
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the state in Arc<Timeline>
|
||||||
|
self.tli
|
||||||
|
.broker_active
|
||||||
|
.store(is_active, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save control file if needed. Returns Instant if we should persist the control file in the future.
|
||||||
|
async fn update_control_file_save(
|
||||||
|
&self,
|
||||||
|
state: &StateSnapshot,
|
||||||
|
next_event: &mut Option<Instant>,
|
||||||
|
) {
|
||||||
|
if !state.inmem_flush_pending {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.cfile_last_persist_at.elapsed() > self.conf.control_file_save_interval {
|
||||||
|
let mut write_guard = self.tli.write_shared_state().await;
|
||||||
|
// it should be done in the background because it blocks manager task, but flush() should
|
||||||
|
// be fast enough not to be a problem now
|
||||||
|
if let Err(e) = write_guard.sk.state_mut().flush().await {
|
||||||
|
warn!("failed to save control file: {:?}", e);
|
||||||
}
|
}
|
||||||
.instrument(info_span!("WAL removal", ttid=%tli.ttid)),
|
} else {
|
||||||
));
|
// we should wait until some time passed until the next save
|
||||||
|
update_next_event(
|
||||||
|
next_event,
|
||||||
|
(state.cfile_last_persist_at + self.conf.control_file_save_interval).into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns WAL removal task if needed.
|
||||||
|
async fn update_wal_removal(&mut self, state: &StateSnapshot) {
|
||||||
|
if self.wal_removal_task.is_some() || state.wal_removal_on_hold {
|
||||||
|
// WAL removal is already in progress or hold off
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If enabled, we use LSN of the most lagging walsender as a WAL removal horizon.
|
||||||
|
// This allows to get better read speed for pageservers that are lagging behind,
|
||||||
|
// at the cost of keeping more WAL on disk.
|
||||||
|
let replication_horizon_lsn = if self.conf.walsenders_keep_horizon {
|
||||||
|
self.walsenders.laggard_lsn()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let removal_horizon_lsn = calc_horizon_lsn(state, replication_horizon_lsn);
|
||||||
|
let removal_horizon_segno = removal_horizon_lsn
|
||||||
|
.segment_number(self.wal_seg_size)
|
||||||
|
.saturating_sub(1);
|
||||||
|
|
||||||
|
if removal_horizon_segno > self.last_removed_segno {
|
||||||
|
// we need to remove WAL
|
||||||
|
let remover = match self.tli.read_shared_state().await.sk {
|
||||||
|
StateSK::Loaded(ref sk) => {
|
||||||
|
crate::wal_storage::Storage::remove_up_to(&sk.wal_store, removal_horizon_segno)
|
||||||
|
}
|
||||||
|
StateSK::Offloaded(_) => {
|
||||||
|
// we can't remove WAL if it's not loaded
|
||||||
|
warn!("unexpectedly trying to run WAL removal on offloaded timeline");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StateSK::Empty => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.wal_removal_task = Some(tokio::spawn(
|
||||||
|
async move {
|
||||||
|
remover.await?;
|
||||||
|
Ok(removal_horizon_segno)
|
||||||
|
}
|
||||||
|
.instrument(info_span!("WAL removal", ttid=%self.tli.ttid)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the state after WAL removal task finished.
|
||||||
|
fn update_wal_removal_end(&mut self, res: Result<anyhow::Result<u64>, JoinError>) {
|
||||||
|
let new_last_removed_segno = match res {
|
||||||
|
Ok(Ok(segno)) => segno,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("WAL removal task failed: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
warn!("WAL removal task failed: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.last_removed_segno = new_last_removed_segno;
|
||||||
|
// update the state in Arc<Timeline>
|
||||||
|
self.tli
|
||||||
|
.last_removed_segno
|
||||||
|
.store(new_last_removed_segno, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns partial WAL backup task if needed.
|
||||||
|
async fn update_partial_backup(&mut self, state: &StateSnapshot) {
|
||||||
|
// check if partial backup is enabled and should be started
|
||||||
|
if !self.conf.is_wal_backup_enabled() || !self.conf.partial_backup_enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.partial_backup_task.is_some() {
|
||||||
|
// partial backup is already running
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !wal_backup_partial::needs_uploading(state, &self.partial_backup_uploaded) {
|
||||||
|
// nothing to upload
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get WalResidentTimeline and start partial backup task.
|
||||||
|
self.partial_backup_task = Some(tokio::spawn(wal_backup_partial::main_task(
|
||||||
|
self.wal_resident_timeline(),
|
||||||
|
self.conf.clone(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the state after partial WAL backup task finished.
|
||||||
|
fn update_partial_backup_end(&mut self, res: Result<Option<PartialRemoteSegment>, JoinError>) {
|
||||||
|
match res {
|
||||||
|
Ok(new_upload_state) => {
|
||||||
|
self.partial_backup_uploaded = new_upload_state;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("partial backup task panicked: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle message arrived from ManagerCtl.
|
||||||
|
async fn handle_message(&mut self, msg: Option<ManagerCtlMessage>) {
|
||||||
|
debug!("received manager message: {:?}", msg);
|
||||||
|
match msg {
|
||||||
|
Some(ManagerCtlMessage::GuardRequest(tx)) => {
|
||||||
|
if self.is_offloaded {
|
||||||
|
// trying to unevict timeline, but without gurarantee that it will be successful
|
||||||
|
self.unevict_timeline().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let guard = if self.is_offloaded {
|
||||||
|
Err(anyhow::anyhow!("timeline is offloaded, can't get a guard"))
|
||||||
|
} else {
|
||||||
|
Ok(self.access_service.create_guard())
|
||||||
|
};
|
||||||
|
|
||||||
|
if tx.send(guard).is_err() {
|
||||||
|
warn!("failed to reply with a guard, receiver dropped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(ManagerCtlMessage::GuardDrop(guard_id)) => {
|
||||||
|
self.access_service.drop_guard(guard_id);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// can't happen, we're holding the sender
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the state after WAL removal task finished.
|
// utility functions
|
||||||
fn update_wal_removal_end(
|
async fn sleep_until(option: &Option<tokio::time::Instant>) {
|
||||||
res: Result<anyhow::Result<u64>, JoinError>,
|
if let Some(timeout) = option {
|
||||||
tli: &Arc<Timeline>,
|
tokio::time::sleep_until(*timeout).await;
|
||||||
last_removed_segno: &mut u64,
|
} else {
|
||||||
) {
|
futures::future::pending::<()>().await;
|
||||||
let new_last_removed_segno = match res {
|
}
|
||||||
Ok(Ok(segno)) => segno,
|
}
|
||||||
Err(e) => {
|
|
||||||
warn!("WAL removal task failed: {:?}", e);
|
async fn await_task_finish<T>(option: &mut Option<JoinHandle<T>>) -> Result<T, JoinError> {
|
||||||
return;
|
if let Some(task) = option {
|
||||||
}
|
task.await
|
||||||
Ok(Err(e)) => {
|
} else {
|
||||||
warn!("WAL removal task failed: {:?}", e);
|
futures::future::pending().await
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
/// Update next_event if candidate is earlier.
|
||||||
*last_removed_segno = new_last_removed_segno;
|
fn update_next_event(next_event: &mut Option<Instant>, candidate: Instant) {
|
||||||
// update the state in Arc<Timeline>
|
if let Some(next) = next_event {
|
||||||
tli.last_removed_segno
|
if candidate < *next {
|
||||||
.store(new_last_removed_segno, std::sync::atomic::Ordering::Relaxed);
|
*next = candidate;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*next_event = Some(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(usize)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Status {
|
||||||
|
NotStarted,
|
||||||
|
Started,
|
||||||
|
StateSnapshot,
|
||||||
|
UpdateBackup,
|
||||||
|
UpdateControlFile,
|
||||||
|
UpdateWalRemoval,
|
||||||
|
UpdatePartialBackup,
|
||||||
|
EvictTimeline,
|
||||||
|
Wait,
|
||||||
|
HandleMessage,
|
||||||
|
Exiting,
|
||||||
|
Finished,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AtomicStatus is a wrapper around AtomicUsize adapted for the Status enum.
|
||||||
|
pub struct AtomicStatus {
|
||||||
|
inner: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AtomicStatus {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AtomicStatus {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
AtomicStatus {
|
||||||
|
inner: AtomicUsize::new(Status::NotStarted as usize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(&self, order: std::sync::atomic::Ordering) -> Status {
|
||||||
|
// Safety: This line of code uses `std::mem::transmute` to reinterpret the loaded value as `Status`.
|
||||||
|
// It is safe to use `transmute` in this context because `Status` is a repr(usize) enum,
|
||||||
|
// which means it has the same memory layout as usize.
|
||||||
|
// However, it is important to ensure that the loaded value is a valid variant of `Status`,
|
||||||
|
// otherwise, the behavior will be undefined.
|
||||||
|
unsafe { std::mem::transmute(self.inner.load(order)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self) -> Status {
|
||||||
|
self.load(std::sync::atomic::Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store(&self, val: Status, order: std::sync::atomic::Ordering) {
|
||||||
|
self.inner.store(val as usize, order);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ impl TimelineSetGuard {
|
|||||||
self.timelines_set.set_present(self.tli.clone(), present);
|
self.timelines_set.set_present(self.tli.clone(), present);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get(&self) -> bool {
|
||||||
|
self.is_present
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TimelineSetGuard {
|
impl Drop for TimelineSetGuard {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user