mirror of
https://github.com/neondatabase/neon.git
synced 2026-03-04 00:40:38 +00:00
Compare commits
72 Commits
extension_
...
extension_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cdd313973 | ||
|
|
9829ff7ae9 | ||
|
|
08be107f8a | ||
|
|
3f003c97bb | ||
|
|
7ec9d4f2b8 | ||
|
|
1291680389 | ||
|
|
f697f44f1e | ||
|
|
5cdbe09606 | ||
|
|
11fcfe2d00 | ||
|
|
e08b1b57b9 | ||
|
|
9ff352469c | ||
|
|
5305548079 | ||
|
|
3ae91eb50a | ||
|
|
7ed120a98e | ||
|
|
edc9408023 | ||
|
|
4f6edae2ad | ||
|
|
7bcd06b2f7 | ||
|
|
908de0af74 | ||
|
|
40e8a9bba7 | ||
|
|
659de49db6 | ||
|
|
d132a79010 | ||
|
|
1f8cf9d53f | ||
|
|
2305f766ca | ||
|
|
3d402f39e6 | ||
|
|
7e4b55a933 | ||
|
|
681ed9261e | ||
|
|
3ce678b3bb | ||
|
|
33f1bacfb7 | ||
|
|
9c8c55e819 | ||
|
|
10110bee69 | ||
|
|
cff7ae0b0d | ||
|
|
78a7f68902 | ||
|
|
24eaa3b7ca | ||
|
|
26828560a8 | ||
|
|
86604b3b7d | ||
|
|
4957bb2d48 | ||
|
|
ff1a1aea86 | ||
|
|
c9f05d418d | ||
|
|
9de1a6fb14 | ||
|
|
fbd37740c5 | ||
|
|
3e55d9dec6 | ||
|
|
f558f88a08 | ||
|
|
b990200496 | ||
|
|
7e20b49da4 | ||
|
|
032b603011 | ||
|
|
ca0e0781c8 | ||
|
|
b2a5e91a88 | ||
|
|
44e7d5132f | ||
|
|
c19681bc12 | ||
|
|
ec9b585837 | ||
|
|
02ef246db6 | ||
|
|
195d4932c6 | ||
|
|
7fe0a4bf1a | ||
|
|
ef2b9ffbcb | ||
|
|
250a27fb85 | ||
|
|
d748615c1f | ||
|
|
681c6910c2 | ||
|
|
148f0f9b21 | ||
|
|
a7f3f5f356 | ||
|
|
00d1cfa503 | ||
|
|
1faf69a698 | ||
|
|
44a441080d | ||
|
|
c215389f1c | ||
|
|
b1477b4448 | ||
|
|
a500bb06fb | ||
|
|
15456625c2 | ||
|
|
a3f0dd2d30 | ||
|
|
76718472be | ||
|
|
c07b6ffbdc | ||
|
|
6c3605fc24 | ||
|
|
d96d51a3b7 | ||
|
|
a010b2108a |
9
.github/workflows/benchmarking.yml
vendored
9
.github/workflows/benchmarking.yml
vendored
@@ -180,7 +180,8 @@ jobs:
|
|||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
||||||
options: --init
|
options: --init
|
||||||
|
|
||||||
timeout-minutes: 360 # 6h
|
# Increase timeout to 8h, default timeout is 6h
|
||||||
|
timeout-minutes: 480
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -321,8 +322,6 @@ jobs:
|
|||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
||||||
options: --init
|
options: --init
|
||||||
|
|
||||||
timeout-minutes: 360 # 6h
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
@@ -414,8 +413,6 @@ jobs:
|
|||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
||||||
options: --init
|
options: --init
|
||||||
|
|
||||||
timeout-minutes: 360 # 6h
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
@@ -501,8 +498,6 @@ jobs:
|
|||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
||||||
options: --init
|
options: --init
|
||||||
|
|
||||||
timeout-minutes: 360 # 6h
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
|||||||
118
.github/workflows/build_and_test.yml
vendored
118
.github/workflows/build_and_test.yml
vendored
@@ -722,6 +722,35 @@ jobs:
|
|||||||
--dockerfile Dockerfile.compute-node
|
--dockerfile Dockerfile.compute-node
|
||||||
--destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
--destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
||||||
--destination neondatabase/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
--destination neondatabase/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
||||||
|
--cleanup
|
||||||
|
|
||||||
|
# Due to a kaniko bug, we can't use cache for extensions image, thus it takes about the same amount of time as compute-node image to build (~10 min)
|
||||||
|
# During the transition period we need to have extensions in both places (in S3 and in compute-node image),
|
||||||
|
# so we won't build extension twice, but extract them from compute-node.
|
||||||
|
#
|
||||||
|
# For now we use extensions image only for new custom extensitons
|
||||||
|
- name: Kaniko build extensions only
|
||||||
|
run: |
|
||||||
|
# Kaniko is suposed to clean up after itself if --cleanup flag is set, but it doesn't.
|
||||||
|
# Despite some fixes were made in https://github.com/GoogleContainerTools/kaniko/pull/2504 (in kaniko v1.11.0),
|
||||||
|
# it still fails with error:
|
||||||
|
# error building image: could not save file: copying file: symlink postgres /kaniko/1/usr/local/pgsql/bin/postmaster: file exists
|
||||||
|
#
|
||||||
|
# Ref https://github.com/GoogleContainerTools/kaniko/issues/1406
|
||||||
|
find /kaniko -maxdepth 1 -mindepth 1 -type d -regex "/kaniko/[0-9]*" -exec rm -rv {} \;
|
||||||
|
|
||||||
|
/kaniko/executor --reproducible --snapshot-mode=redo --skip-unused-stages --cache=true \
|
||||||
|
--cache-repo 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cache \
|
||||||
|
--context . \
|
||||||
|
--build-arg GIT_VERSION=${{ github.sha }} \
|
||||||
|
--build-arg PG_VERSION=${{ matrix.version }} \
|
||||||
|
--build-arg BUILD_TAG=${{needs.tag.outputs.build-tag}} \
|
||||||
|
--build-arg REPOSITORY=369495373322.dkr.ecr.eu-central-1.amazonaws.com \
|
||||||
|
--dockerfile Dockerfile.compute-node \
|
||||||
|
--destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/extensions-${{ matrix.version }}:${{needs.tag.outputs.build-tag}} \
|
||||||
|
--destination neondatabase/extensions-${{ matrix.version }}:${{needs.tag.outputs.build-tag}} \
|
||||||
|
--cleanup \
|
||||||
|
--target postgres-extensions
|
||||||
|
|
||||||
# Cleanup script fails otherwise - rm: cannot remove '/nvme/actions-runner/_work/_temp/_github_home/.ecr': Permission denied
|
# Cleanup script fails otherwise - rm: cannot remove '/nvme/actions-runner/_work/_temp/_github_home/.ecr': Permission denied
|
||||||
- name: Cleanup ECR folder
|
- name: Cleanup ECR folder
|
||||||
@@ -738,7 +767,7 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
shell: sh -eu {0}
|
shell: sh -eu {0}
|
||||||
env:
|
env:
|
||||||
VM_BUILDER_VERSION: v0.8.0
|
VM_BUILDER_VERSION: v0.11.1
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -840,8 +869,10 @@ jobs:
|
|||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} latest
|
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} latest
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:${{needs.tag.outputs.build-tag}} latest
|
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:${{needs.tag.outputs.build-tag}} latest
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} latest
|
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} latest
|
||||||
|
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/extensions-v14:${{needs.tag.outputs.build-tag}} latest
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
||||||
|
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/extensions-v15:${{needs.tag.outputs.build-tag}} latest
|
||||||
|
|
||||||
- name: Push images to production ECR
|
- name: Push images to production ECR
|
||||||
if: |
|
if: |
|
||||||
@@ -852,8 +883,10 @@ jobs:
|
|||||||
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:latest
|
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:latest
|
||||||
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:latest
|
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:latest
|
||||||
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:latest
|
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:latest
|
||||||
|
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/extensions-v14:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/extensions-v14:latest
|
||||||
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:latest
|
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:latest
|
||||||
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:latest
|
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:latest
|
||||||
|
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/extensions-v15:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/extensions-v15:latest
|
||||||
|
|
||||||
- name: Configure Docker Hub login
|
- name: Configure Docker Hub login
|
||||||
run: |
|
run: |
|
||||||
@@ -875,16 +908,93 @@ jobs:
|
|||||||
crane tag neondatabase/compute-tools:${{needs.tag.outputs.build-tag}} latest
|
crane tag neondatabase/compute-tools:${{needs.tag.outputs.build-tag}} latest
|
||||||
crane tag neondatabase/compute-node-v14:${{needs.tag.outputs.build-tag}} latest
|
crane tag neondatabase/compute-node-v14:${{needs.tag.outputs.build-tag}} latest
|
||||||
crane tag neondatabase/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} latest
|
crane tag neondatabase/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} latest
|
||||||
|
crane tag neondatabase/extensions-v14:${{needs.tag.outputs.build-tag}} latest
|
||||||
crane tag neondatabase/compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
crane tag neondatabase/compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
||||||
crane tag neondatabase/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
crane tag neondatabase/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
||||||
|
crane tag neondatabase/extensions-v15:${{needs.tag.outputs.build-tag}} latest
|
||||||
|
|
||||||
- name: Cleanup ECR folder
|
- name: Cleanup ECR folder
|
||||||
run: rm -rf ~/.ecr
|
run: rm -rf ~/.ecr
|
||||||
|
|
||||||
|
upload-postgres-extensions-to-s3:
|
||||||
|
if: |
|
||||||
|
(github.ref_name == 'main' || github.ref_name == 'release') &&
|
||||||
|
github.event_name != 'workflow_dispatch'
|
||||||
|
runs-on: ${{ github.ref_name == 'release' && fromJSON('["self-hosted", "prod", "x64"]') || fromJSON('["self-hosted", "gen3", "small"]') }}
|
||||||
|
needs: [ tag, promote-images ]
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
version: [ v14, v15 ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
# While on transition period we extract public extensions from compute-node image and custom extensions from extensions image.
|
||||||
|
# Later all the extensions will be moved to extensions image.
|
||||||
|
EXTENSIONS_IMAGE: ${{ github.ref_name == 'release' && '093970136003' || '369495373322'}}.dkr.ecr.eu-central-1.amazonaws.com/extensions-${{ matrix.version }}:latest
|
||||||
|
COMPUTE_NODE_IMAGE: ${{ github.ref_name == 'release' && '093970136003' || '369495373322'}}.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:latest
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ github.ref_name == 'release' && secrets.AWS_ACCESS_KEY_PROD || secrets.AWS_ACCESS_KEY_DEV }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ github.ref_name == 'release' && secrets.AWS_SECRET_KEY_PROD || secrets.AWS_SECRET_KEY_DEV }}
|
||||||
|
S3_BUCKETS: |
|
||||||
|
${{ github.ref_name == 'release' &&
|
||||||
|
'neon-prod-extensions-ap-southeast-1 neon-prod-extensions-eu-central-1 neon-prod-extensions-us-east-1 neon-prod-extensions-us-east-2 neon-prod-extensions-us-west-2' ||
|
||||||
|
'neon-dev-extensions-eu-central-1 neon-dev-extensions-eu-west-1 neon-dev-extensions-us-east-2' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Pull postgres-extensions image
|
||||||
|
run: |
|
||||||
|
docker pull ${EXTENSIONS_IMAGE}
|
||||||
|
docker pull ${COMPUTE_NODE_IMAGE}
|
||||||
|
|
||||||
|
- name: Create postgres-extensions container
|
||||||
|
id: create-container
|
||||||
|
run: |
|
||||||
|
EID=$(docker create ${EXTENSIONS_IMAGE} true)
|
||||||
|
echo "EID=${EID}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
CID=$(docker create ${COMPUTE_NODE_IMAGE} true)
|
||||||
|
echo "CID=${CID}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Extract postgres-extensions from container
|
||||||
|
run: |
|
||||||
|
rm -rf ./extensions-to-upload ./custom-extensions # Just in case
|
||||||
|
|
||||||
|
# In compute image we have a bit different directory layout
|
||||||
|
mkdir -p extensions-to-upload/share
|
||||||
|
docker cp ${{ steps.create-container.outputs.CID }}:/usr/local/share/extension ./extensions-to-upload/share/extension
|
||||||
|
docker cp ${{ steps.create-container.outputs.CID }}:/usr/local/lib ./extensions-to-upload/lib
|
||||||
|
|
||||||
|
# Delete Neon extensitons (they always present on compute-node image)
|
||||||
|
rm -rf ./extensions-to-upload/share/extension/neon*
|
||||||
|
rm -rf ./extensions-to-upload/lib/neon*
|
||||||
|
|
||||||
|
# Delete leftovers from the extension build step
|
||||||
|
rm -rf ./extensions-to-upload/lib/pgxs
|
||||||
|
rm -rf ./extensions-to-upload/lib/pkgconfig
|
||||||
|
|
||||||
|
docker cp ${{ steps.create-container.outputs.EID }}:/extensions ./custom-extensions
|
||||||
|
for EXT_NAME in $(ls ./custom-extensions); do
|
||||||
|
mkdir -p ./extensions-to-upload/${EXT_NAME}/share
|
||||||
|
|
||||||
|
mv ./custom-extensions/${EXT_NAME}/share/extension ./extensions-to-upload/${EXT_NAME}/share/extension
|
||||||
|
mv ./custom-extensions/${EXT_NAME}/lib ./extensions-to-upload/${EXT_NAME}/lib
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Upload postgres-extensions to S3
|
||||||
|
run: |
|
||||||
|
for BUCKET in $(echo ${S3_BUCKETS}); do
|
||||||
|
aws s3 cp --recursive --only-show-errors ./extensions-to-upload s3://${BUCKET}/${{ needs.tag.outputs.build-tag }}/${{ matrix.version }}
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: ${{ always() && (steps.create-container.outputs.CID || steps.create-container.outputs.EID) }}
|
||||||
|
run: |
|
||||||
|
docker rm ${{ steps.create-container.outputs.CID }} || true
|
||||||
|
docker rm ${{ steps.create-container.outputs.EID }} || true
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: [ self-hosted, gen3, small ]
|
||||||
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:latest
|
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:latest
|
||||||
needs: [ promote-images, tag, regress-tests ]
|
needs: [ upload-postgres-extensions-to-s3, promote-images, tag, regress-tests ]
|
||||||
if: ( github.ref_name == 'main' || github.ref_name == 'release' ) && github.event_name != 'workflow_dispatch'
|
if: ( github.ref_name == 'main' || github.ref_name == 'release' ) && github.event_name != 'workflow_dispatch'
|
||||||
steps:
|
steps:
|
||||||
- name: Fix git ownership
|
- name: Fix git ownership
|
||||||
@@ -916,7 +1026,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Create tag "release-${{ needs.tag.outputs.build-tag }}"
|
- name: Create git tag
|
||||||
if: github.ref_name == 'release'
|
if: github.ref_name == 'release'
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
@@ -926,7 +1036,7 @@ jobs:
|
|||||||
github.rest.git.createRef({
|
github.rest.git.createRef({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
ref: "refs/tags/release-${{ needs.tag.outputs.build-tag }}",
|
ref: "refs/tags/${{ needs.tag.outputs.build-tag }}",
|
||||||
sha: context.sha,
|
sha: context.sha,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
223
Cargo.lock
generated
223
Cargo.lock
generated
@@ -200,17 +200,6 @@ dependencies = [
|
|||||||
"critical-section",
|
"critical-section",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "atty"
|
|
||||||
version = "0.2.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
|
||||||
dependencies = [
|
|
||||||
"hermit-abi 0.1.19",
|
|
||||||
"libc",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -604,7 +593,7 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"miniz_oxide",
|
"miniz_oxide 0.6.2",
|
||||||
"object",
|
"object",
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
]
|
]
|
||||||
@@ -805,18 +794,6 @@ dependencies = [
|
|||||||
"libloading",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap"
|
|
||||||
version = "3.2.25"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"clap_lex 0.2.4",
|
|
||||||
"indexmap",
|
|
||||||
"textwrap",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.3.0"
|
version = "4.3.0"
|
||||||
@@ -837,7 +814,7 @@ dependencies = [
|
|||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"clap_lex 0.5.0",
|
"clap_lex",
|
||||||
"strsim",
|
"strsim",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -853,15 +830,6 @@ dependencies = [
|
|||||||
"syn 2.0.16",
|
"syn 2.0.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap_lex"
|
|
||||||
version = "0.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
|
|
||||||
dependencies = [
|
|
||||||
"os_str_bytes",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_lex"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -915,8 +883,9 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"compute_api",
|
"compute_api",
|
||||||
|
"flate2",
|
||||||
"futures",
|
"futures",
|
||||||
"hyper",
|
"hyper",
|
||||||
"notify",
|
"notify",
|
||||||
@@ -924,12 +893,14 @@ dependencies = [
|
|||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
"postgres",
|
"postgres",
|
||||||
"regex",
|
"regex",
|
||||||
|
"remote_storage",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tar",
|
"tar",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-postgres",
|
"tokio-postgres",
|
||||||
|
"toml_edit",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-opentelemetry",
|
"tracing-opentelemetry",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -977,7 +948,7 @@ name = "control_plane"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"comfy-table",
|
"comfy-table",
|
||||||
"compute_api",
|
"compute_api",
|
||||||
"git-version",
|
"git-version",
|
||||||
@@ -997,6 +968,7 @@ dependencies = [
|
|||||||
"tar",
|
"tar",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"utils",
|
"utils",
|
||||||
"workspace_hack",
|
"workspace_hack",
|
||||||
@@ -1047,19 +1019,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "criterion"
|
name = "criterion"
|
||||||
version = "0.4.0"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb"
|
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anes",
|
"anes",
|
||||||
"atty",
|
|
||||||
"cast",
|
"cast",
|
||||||
"ciborium",
|
"ciborium",
|
||||||
"clap 3.2.25",
|
"clap",
|
||||||
"criterion-plot",
|
"criterion-plot",
|
||||||
|
"is-terminal",
|
||||||
"itertools",
|
"itertools",
|
||||||
"lazy_static",
|
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"once_cell",
|
||||||
"oorandom",
|
"oorandom",
|
||||||
"plotters",
|
"plotters",
|
||||||
"rayon",
|
"rayon",
|
||||||
@@ -1140,7 +1112,7 @@ dependencies = [
|
|||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot 0.12.1",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
"signal-hook-mio",
|
"signal-hook-mio",
|
||||||
"winapi",
|
"winapi",
|
||||||
@@ -1210,7 +1182,7 @@ dependencies = [
|
|||||||
"hashbrown 0.12.3",
|
"hashbrown 0.12.3",
|
||||||
"lock_api",
|
"lock_api",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot_core",
|
"parking_lot_core 0.9.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1399,6 +1371,16 @@ version = "0.4.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.0.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide 0.7.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -1676,15 +1658,6 @@ version = "0.4.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hermit-abi"
|
|
||||||
version = "0.1.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@@ -1939,6 +1912,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2189,6 +2165,15 @@ dependencies = [
|
|||||||
"adler",
|
"adler",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
|
||||||
|
dependencies = [
|
||||||
|
"adler",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
@@ -2267,16 +2252,6 @@ dependencies = [
|
|||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nu-ansi-term"
|
|
||||||
version = "0.46.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
|
||||||
dependencies = [
|
|
||||||
"overload",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -2504,31 +2479,19 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "os_str_bytes"
|
|
||||||
version = "6.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "outref"
|
name = "outref"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a"
|
checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "overload"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pagectl"
|
name = "pagectl"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"git-version",
|
"git-version",
|
||||||
"pageserver",
|
"pageserver",
|
||||||
"postgres_ffi",
|
"postgres_ffi",
|
||||||
@@ -2547,7 +2510,7 @@ dependencies = [
|
|||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"close_fds",
|
"close_fds",
|
||||||
"const_format",
|
"const_format",
|
||||||
"consumption_metrics",
|
"consumption_metrics",
|
||||||
@@ -2629,6 +2592,17 @@ dependencies = [
|
|||||||
"workspace_hack",
|
"workspace_hack",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
||||||
|
dependencies = [
|
||||||
|
"instant",
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core 0.8.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -2636,7 +2610,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lock_api",
|
"lock_api",
|
||||||
"parking_lot_core",
|
"parking_lot_core 0.9.7",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"instant",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall 0.2.16",
|
||||||
|
"smallvec",
|
||||||
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2957,7 +2945,7 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"memchr",
|
"memchr",
|
||||||
"parking_lot",
|
"parking_lot 0.12.1",
|
||||||
"procfs",
|
"procfs",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
@@ -3022,12 +3010,11 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"atty",
|
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"bstr",
|
"bstr",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"consumption_metrics",
|
"consumption_metrics",
|
||||||
"futures",
|
"futures",
|
||||||
"git-version",
|
"git-version",
|
||||||
@@ -3045,7 +3032,7 @@ dependencies = [
|
|||||||
"native-tls",
|
"native-tls",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
"parking_lot",
|
"parking_lot 0.12.1",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"postgres-native-tls",
|
"postgres-native-tls",
|
||||||
"postgres_backend",
|
"postgres_backend",
|
||||||
@@ -3056,6 +3043,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
|
"reqwest-retry",
|
||||||
"reqwest-tracing",
|
"reqwest-tracing",
|
||||||
"routerify",
|
"routerify",
|
||||||
"rstest",
|
"rstest",
|
||||||
@@ -3291,6 +3279,29 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest-retry"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48d0fd6ef4c6d23790399fe15efc8d12cd9f3d4133958f9bd7801ee5cbaec6c4"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"chrono",
|
||||||
|
"futures",
|
||||||
|
"getrandom",
|
||||||
|
"http",
|
||||||
|
"hyper",
|
||||||
|
"parking_lot 0.11.2",
|
||||||
|
"reqwest",
|
||||||
|
"reqwest-middleware",
|
||||||
|
"retry-policies",
|
||||||
|
"task-local-extensions",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"wasm-timer",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest-tracing"
|
name = "reqwest-tracing"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@@ -3309,6 +3320,17 @@ dependencies = [
|
|||||||
"tracing-opentelemetry",
|
"tracing-opentelemetry",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "retry-policies"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e09bbcb5003282bcb688f0bae741b278e9c7e8f378f561522c9806c58e075d9b"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.16.20"
|
version = "0.16.20"
|
||||||
@@ -3507,7 +3529,7 @@ dependencies = [
|
|||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"const_format",
|
"const_format",
|
||||||
"crc32c",
|
"crc32c",
|
||||||
"fs2",
|
"fs2",
|
||||||
@@ -3518,7 +3540,7 @@ dependencies = [
|
|||||||
"hyper",
|
"hyper",
|
||||||
"metrics",
|
"metrics",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot 0.12.1",
|
||||||
"postgres",
|
"postgres",
|
||||||
"postgres-protocol",
|
"postgres-protocol",
|
||||||
"postgres_backend",
|
"postgres_backend",
|
||||||
@@ -3937,7 +3959,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"bytes",
|
"bytes",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"const_format",
|
"const_format",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -3947,7 +3969,7 @@ dependencies = [
|
|||||||
"hyper",
|
"hyper",
|
||||||
"metrics",
|
"metrics",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot 0.12.1",
|
||||||
"prost",
|
"prost",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
@@ -4118,12 +4140,6 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "textwrap"
|
|
||||||
version = "0.16.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.40"
|
version = "1.0.40"
|
||||||
@@ -4281,7 +4297,7 @@ dependencies = [
|
|||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
"parking_lot",
|
"parking_lot 0.12.1",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"phf",
|
"phf",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -4539,7 +4555,7 @@ name = "trace"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"pageserver_api",
|
"pageserver_api",
|
||||||
"utils",
|
"utils",
|
||||||
"workspace_hack",
|
"workspace_hack",
|
||||||
@@ -4641,7 +4657,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"matchers",
|
"matchers",
|
||||||
"nu-ansi-term",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -4810,7 +4825,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"atty",
|
|
||||||
"bincode",
|
"bincode",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -4887,7 +4901,7 @@ name = "wal_craft"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -4991,6 +5005,21 @@ version = "0.2.86"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93"
|
checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-timer"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f"
|
||||||
|
dependencies = [
|
||||||
|
"futures",
|
||||||
|
"js-sys",
|
||||||
|
"parking_lot 0.11.2",
|
||||||
|
"pin-utils",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.63"
|
version = "0.3.63"
|
||||||
@@ -5252,7 +5281,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap 4.3.0",
|
"clap",
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
"either",
|
"either",
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ license = "Apache-2.0"
|
|||||||
anyhow = { version = "1.0", features = ["backtrace"] }
|
anyhow = { version = "1.0", features = ["backtrace"] }
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
atty = "0.2.14"
|
|
||||||
aws-config = { version = "0.55", default-features = false, features=["rustls"] }
|
aws-config = { version = "0.55", default-features = false, features=["rustls"] }
|
||||||
aws-sdk-s3 = "0.27"
|
aws-sdk-s3 = "0.27"
|
||||||
aws-smithy-http = "0.55"
|
aws-smithy-http = "0.55"
|
||||||
@@ -95,6 +94,7 @@ regex = "1.4"
|
|||||||
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
|
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
|
||||||
reqwest-tracing = { version = "0.4.0", features = ["opentelemetry_0_18"] }
|
reqwest-tracing = { version = "0.4.0", features = ["opentelemetry_0_18"] }
|
||||||
reqwest-middleware = "0.2.0"
|
reqwest-middleware = "0.2.0"
|
||||||
|
reqwest-retry = "0.2.2"
|
||||||
routerify = "3"
|
routerify = "3"
|
||||||
rpds = "0.13"
|
rpds = "0.13"
|
||||||
rustls = "0.20"
|
rustls = "0.20"
|
||||||
@@ -128,7 +128,7 @@ tonic = {version = "0.9", features = ["tls", "tls-roots"]}
|
|||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
tracing-opentelemetry = "0.18.0"
|
tracing-opentelemetry = "0.18.0"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", default_features = false, features = ["smallvec", "fmt", "tracing-log", "std", "env-filter"] }
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
uuid = { version = "1.2", features = ["v4", "serde"] }
|
uuid = { version = "1.2", features = ["v4", "serde"] }
|
||||||
walkdir = "2.3.2"
|
walkdir = "2.3.2"
|
||||||
@@ -170,7 +170,7 @@ utils = { version = "0.1", path = "./libs/utils/" }
|
|||||||
workspace_hack = { version = "0.1", path = "./workspace_hack/" }
|
workspace_hack = { version = "0.1", path = "./workspace_hack/" }
|
||||||
|
|
||||||
## Build dependencies
|
## Build dependencies
|
||||||
criterion = "0.4"
|
criterion = "0.5.1"
|
||||||
rcgen = "0.10"
|
rcgen = "0.10"
|
||||||
rstest = "0.17"
|
rstest = "0.17"
|
||||||
tempfile = "3.4"
|
tempfile = "3.4"
|
||||||
|
|||||||
@@ -481,6 +481,60 @@ RUN wget https://github.com/rdkit/rdkit/archive/refs/tags/Release_2023_03_1.tar.
|
|||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/rdkit.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/rdkit.control
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "pg-uuidv7-pg-build"
|
||||||
|
# compile pg_uuidv7 extension
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM build-deps AS pg-uuidv7-pg-build
|
||||||
|
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
|
||||||
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
|
RUN wget https://github.com/fboulnois/pg_uuidv7/archive/refs/tags/v1.0.1.tar.gz -O pg_uuidv7.tar.gz && \
|
||||||
|
echo "0d0759ab01b7fb23851ecffb0bce27822e1868a4a5819bfd276101c716637a7a pg_uuidv7.tar.gz" | sha256sum --check && \
|
||||||
|
mkdir pg_uuidv7-src && cd pg_uuidv7-src && tar xvzf ../pg_uuidv7.tar.gz --strip-components=1 -C . && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_uuidv7.control
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "pg-roaringbitmap-pg-build"
|
||||||
|
# compile pg_roaringbitmap extension
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM build-deps AS pg-roaringbitmap-pg-build
|
||||||
|
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
|
||||||
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
|
RUN wget https://github.com/ChenHuajun/pg_roaringbitmap/archive/refs/tags/v0.5.4.tar.gz -O pg_roaringbitmap.tar.gz && \
|
||||||
|
echo "b75201efcb1c2d1b014ec4ae6a22769cc7a224e6e406a587f5784a37b6b5a2aa pg_roaringbitmap.tar.gz" | sha256sum --check && \
|
||||||
|
mkdir pg_roaringbitmap-src && cd pg_roaringbitmap-src && tar xvzf ../pg_roaringbitmap.tar.gz --strip-components=1 -C . && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/roaringbitmap.control
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "pg-anon-pg-build"
|
||||||
|
# compile anon extension
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM build-deps AS pg-anon-pg-build
|
||||||
|
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
|
||||||
|
# Kaniko doesn't allow to do `${from#/usr/local/pgsql/}`, so we use `${from:17}` instead
|
||||||
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
|
RUN wget https://gitlab.com/dalibo/postgresql_anonymizer/-/archive/1.1.0/postgresql_anonymizer-1.1.0.tar.gz -O pg_anon.tar.gz && \
|
||||||
|
echo "08b09d2ff9b962f96c60db7e6f8e79cf7253eb8772516998fc35ece08633d3ad pg_anon.tar.gz" | sha256sum --check && \
|
||||||
|
mkdir pg_anon-src && cd pg_anon-src && tar xvzf ../pg_anon.tar.gz --strip-components=1 -C . && \
|
||||||
|
find /usr/local/pgsql -type f | sort > /before.txt && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/anon.control && \
|
||||||
|
find /usr/local/pgsql -type f | sort > /after.txt && \
|
||||||
|
/bin/bash -c 'for from in $(comm -13 /before.txt /after.txt); do to=/extensions/anon/${from:17} && mkdir -p $(dirname ${to}) && cp -a ${from} ${to}; done'
|
||||||
|
|
||||||
#########################################################################################
|
#########################################################################################
|
||||||
#
|
#
|
||||||
# Layer "rust extensions"
|
# Layer "rust extensions"
|
||||||
@@ -589,6 +643,7 @@ RUN wget https://github.com/pksunkara/pgx_ulid/archive/refs/tags/v0.1.0.tar.gz -
|
|||||||
#
|
#
|
||||||
#########################################################################################
|
#########################################################################################
|
||||||
FROM build-deps AS neon-pg-ext-build
|
FROM build-deps AS neon-pg-ext-build
|
||||||
|
# Public extensions
|
||||||
COPY --from=postgis-build /usr/local/pgsql/ /usr/local/pgsql/
|
COPY --from=postgis-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
COPY --from=postgis-build /sfcgal/* /
|
COPY --from=postgis-build /sfcgal/* /
|
||||||
COPY --from=plv8-build /usr/local/pgsql/ /usr/local/pgsql/
|
COPY --from=plv8-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
@@ -614,6 +669,8 @@ COPY --from=kq-imcx-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
COPY --from=pg-cron-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
COPY --from=pg-cron-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
COPY --from=pg-pgx-ulid-build /usr/local/pgsql/ /usr/local/pgsql/
|
COPY --from=pg-pgx-ulid-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
COPY --from=rdkit-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
COPY --from=rdkit-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
COPY --from=pg-uuidv7-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
COPY --from=pg-roaringbitmap-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
COPY pgxn/ pgxn/
|
COPY pgxn/ pgxn/
|
||||||
|
|
||||||
RUN make -j $(getconf _NPROCESSORS_ONLN) \
|
RUN make -j $(getconf _NPROCESSORS_ONLN) \
|
||||||
@@ -662,6 +719,22 @@ RUN rm -r /usr/local/pgsql/include
|
|||||||
# if they were to be used by other libraries.
|
# if they were to be used by other libraries.
|
||||||
RUN rm /usr/local/pgsql/lib/lib*.a
|
RUN rm /usr/local/pgsql/lib/lib*.a
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Extenstion only
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM scratch AS postgres-extensions
|
||||||
|
# After the transition this layer will include all extensitons.
|
||||||
|
# As for now, it's only for new custom ones
|
||||||
|
#
|
||||||
|
# # Default extensions
|
||||||
|
# COPY --from=postgres-cleanup-layer /usr/local/pgsql/share/extension /usr/local/pgsql/share/extension
|
||||||
|
# COPY --from=postgres-cleanup-layer /usr/local/pgsql/lib /usr/local/pgsql/lib
|
||||||
|
# Custom extensions
|
||||||
|
COPY --from=pg-anon-pg-build /extensions/anon/lib/ /extensions/anon/lib
|
||||||
|
COPY --from=pg-anon-pg-build /extensions/anon/share/extension /extensions/anon/share/extension
|
||||||
|
|
||||||
#########################################################################################
|
#########################################################################################
|
||||||
#
|
#
|
||||||
# Final layer
|
# Final layer
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -132,13 +132,13 @@ Python (3.9 or higher), and install python3 packages using `./scripts/pysync` (r
|
|||||||
# Create repository in .neon with proper paths to binaries and data
|
# Create repository in .neon with proper paths to binaries and data
|
||||||
# Later that would be responsibility of a package install script
|
# Later that would be responsibility of a package install script
|
||||||
> cargo neon init
|
> cargo neon init
|
||||||
Starting pageserver at '127.0.0.1:64000' in '.neon'.
|
Initializing pageserver node 1 at '127.0.0.1:64000' in ".neon"
|
||||||
|
|
||||||
# start pageserver, safekeeper, and broker for their intercommunication
|
# start pageserver, safekeeper, and broker for their intercommunication
|
||||||
> cargo neon start
|
> cargo neon start
|
||||||
Starting neon broker at 127.0.0.1:50051
|
Starting neon broker at 127.0.0.1:50051.
|
||||||
storage_broker started, pid: 2918372
|
storage_broker started, pid: 2918372
|
||||||
Starting pageserver at '127.0.0.1:64000' in '.neon'.
|
Starting pageserver node 1 at '127.0.0.1:64000' in ".neon".
|
||||||
pageserver started, pid: 2918386
|
pageserver started, pid: 2918386
|
||||||
Starting safekeeper at '127.0.0.1:5454' in '.neon/safekeepers/sk1'.
|
Starting safekeeper at '127.0.0.1:5454' in '.neon/safekeepers/sk1'.
|
||||||
safekeeper 1 started, pid: 2918437
|
safekeeper 1 started, pid: 2918437
|
||||||
@@ -152,8 +152,7 @@ Setting tenant 9ef87a5bf0d92544f6fafeeb3239695c as a default one
|
|||||||
# start postgres compute node
|
# start postgres compute node
|
||||||
> cargo neon endpoint start main
|
> cargo neon endpoint start main
|
||||||
Starting new endpoint main (PostgreSQL v14) on timeline de200bd42b49cc1814412c7e592dd6e9 ...
|
Starting new endpoint main (PostgreSQL v14) on timeline de200bd42b49cc1814412c7e592dd6e9 ...
|
||||||
Extracting base backup to create postgres instance: path=.neon/pgdatadirs/tenants/9ef87a5bf0d92544f6fafeeb3239695c/main port=55432
|
Starting postgres at 'postgresql://cloud_admin@127.0.0.1:55432/postgres'
|
||||||
Starting postgres at 'host=127.0.0.1 port=55432 user=cloud_admin dbname=postgres'
|
|
||||||
|
|
||||||
# check list of running postgres instances
|
# check list of running postgres instances
|
||||||
> cargo neon endpoint list
|
> cargo neon endpoint list
|
||||||
@@ -189,18 +188,17 @@ Created timeline 'b3b863fa45fa9e57e615f9f2d944e601' at Lsn 0/16F9A00 for tenant:
|
|||||||
# start postgres on that branch
|
# start postgres on that branch
|
||||||
> cargo neon endpoint start migration_check --branch-name migration_check
|
> cargo neon endpoint start migration_check --branch-name migration_check
|
||||||
Starting new endpoint migration_check (PostgreSQL v14) on timeline b3b863fa45fa9e57e615f9f2d944e601 ...
|
Starting new endpoint migration_check (PostgreSQL v14) on timeline b3b863fa45fa9e57e615f9f2d944e601 ...
|
||||||
Extracting base backup to create postgres instance: path=.neon/pgdatadirs/tenants/9ef87a5bf0d92544f6fafeeb3239695c/migration_check port=55433
|
Starting postgres at 'postgresql://cloud_admin@127.0.0.1:55434/postgres'
|
||||||
Starting postgres at 'host=127.0.0.1 port=55433 user=cloud_admin dbname=postgres'
|
|
||||||
|
|
||||||
# check the new list of running postgres instances
|
# check the new list of running postgres instances
|
||||||
> cargo neon endpoint list
|
> cargo neon endpoint list
|
||||||
ENDPOINT ADDRESS TIMELINE BRANCH NAME LSN STATUS
|
ENDPOINT ADDRESS TIMELINE BRANCH NAME LSN STATUS
|
||||||
main 127.0.0.1:55432 de200bd42b49cc1814412c7e592dd6e9 main 0/16F9A38 running
|
main 127.0.0.1:55432 de200bd42b49cc1814412c7e592dd6e9 main 0/16F9A38 running
|
||||||
migration_check 127.0.0.1:55433 b3b863fa45fa9e57e615f9f2d944e601 migration_check 0/16F9A70 running
|
migration_check 127.0.0.1:55434 b3b863fa45fa9e57e615f9f2d944e601 migration_check 0/16F9A70 running
|
||||||
|
|
||||||
# this new postgres instance will have all the data from 'main' postgres,
|
# this new postgres instance will have all the data from 'main' postgres,
|
||||||
# but all modifications would not affect data in original postgres
|
# but all modifications would not affect data in original postgres
|
||||||
> psql -p55433 -h 127.0.0.1 -U cloud_admin postgres
|
> psql -p55434 -h 127.0.0.1 -U cloud_admin postgres
|
||||||
postgres=# select * from t;
|
postgres=# select * from t;
|
||||||
key | value
|
key | value
|
||||||
-----+-------
|
-----+-------
|
||||||
|
|||||||
@@ -30,3 +30,6 @@ url.workspace = true
|
|||||||
compute_api.workspace = true
|
compute_api.workspace = true
|
||||||
utils.workspace = true
|
utils.workspace = true
|
||||||
workspace_hack.workspace = true
|
workspace_hack.workspace = true
|
||||||
|
toml_edit.workspace = true
|
||||||
|
remote_storage = { version = "0.1", path = "../libs/remote_storage/" }
|
||||||
|
flate2 = "1.0.26"
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
//! - `compute_ctl` accepts cluster (compute node) specification as a JSON file.
|
//! - `compute_ctl` accepts cluster (compute node) specification as a JSON file.
|
||||||
//! - Every start is a fresh start, so the data directory is removed and
|
//! - Every start is a fresh start, so the data directory is removed and
|
||||||
//! initialized again on each run.
|
//! initialized again on each run.
|
||||||
|
//! - If remote_extension_config is provided, it will be used to fetch extensions list
|
||||||
|
//! and download `shared_preload_libraries` from the remote storage.
|
||||||
//! - Next it will put configuration files into the `PGDATA` directory.
|
//! - Next it will put configuration files into the `PGDATA` directory.
|
||||||
//! - Sync safekeepers and get commit LSN.
|
//! - Sync safekeepers and get commit LSN.
|
||||||
//! - Get `basebackup` from pageserver using the returned on the previous step LSN.
|
//! - Get `basebackup` from pageserver using the returned on the previous step LSN.
|
||||||
@@ -27,7 +29,8 @@
|
|||||||
//! compute_ctl -D /var/db/postgres/compute \
|
//! compute_ctl -D /var/db/postgres/compute \
|
||||||
//! -C 'postgresql://cloud_admin@localhost/postgres' \
|
//! -C 'postgresql://cloud_admin@localhost/postgres' \
|
||||||
//! -S /var/db/postgres/specs/current.json \
|
//! -S /var/db/postgres/specs/current.json \
|
||||||
//! -b /usr/local/bin/postgres
|
//! -b /usr/local/bin/postgres \
|
||||||
|
//! -r {"bucket": "my-bucket", "region": "eu-central-1", "endpoint": "http:://localhost:9000"} \
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -35,7 +38,7 @@ use std::fs::File;
|
|||||||
use std::panic;
|
use std::panic;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::sync::{mpsc, Arc, Condvar, Mutex};
|
use std::sync::{mpsc, Arc, Condvar, Mutex, OnceLock};
|
||||||
use std::{thread, time::Duration};
|
use std::{thread, time::Duration};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
@@ -48,6 +51,8 @@ use compute_api::responses::ComputeStatus;
|
|||||||
|
|
||||||
use compute_tools::compute::{ComputeNode, ComputeState, ParsedSpec};
|
use compute_tools::compute::{ComputeNode, ComputeState, ParsedSpec};
|
||||||
use compute_tools::configurator::launch_configurator;
|
use compute_tools::configurator::launch_configurator;
|
||||||
|
use compute_tools::extension_server::launch_download_extensions;
|
||||||
|
use compute_tools::extension_server::{get_pg_version, init_remote_storage};
|
||||||
use compute_tools::http::api::launch_http_server;
|
use compute_tools::http::api::launch_http_server;
|
||||||
use compute_tools::logger::*;
|
use compute_tools::logger::*;
|
||||||
use compute_tools::monitor::launch_monitor;
|
use compute_tools::monitor::launch_monitor;
|
||||||
@@ -60,10 +65,21 @@ fn main() -> Result<()> {
|
|||||||
init_tracing_and_logging(DEFAULT_LOG_LEVEL)?;
|
init_tracing_and_logging(DEFAULT_LOG_LEVEL)?;
|
||||||
|
|
||||||
let build_tag = option_env!("BUILD_TAG").unwrap_or(BUILD_TAG_DEFAULT);
|
let build_tag = option_env!("BUILD_TAG").unwrap_or(BUILD_TAG_DEFAULT);
|
||||||
|
|
||||||
info!("build_tag: {build_tag}");
|
info!("build_tag: {build_tag}");
|
||||||
|
|
||||||
let matches = cli().get_matches();
|
let matches = cli().get_matches();
|
||||||
|
let pgbin_default = String::from("postgres");
|
||||||
|
let pgbin = matches.get_one::<String>("pgbin").unwrap_or(&pgbin_default);
|
||||||
|
|
||||||
|
let remote_ext_config = matches.get_one::<String>("remote-ext-config");
|
||||||
|
// NOTE TODO: until control-plane changes, we can use the following line to forcibly enable remote extensions
|
||||||
|
// let remote_ext_config = Some(
|
||||||
|
// r#"{"bucket": "neon-dev-extensions", "region": "eu-central-1", "endpoint": null, "prefix": "5555"}"#.to_string(),
|
||||||
|
// );
|
||||||
|
let ext_remote_storage = remote_ext_config.map(|x| {
|
||||||
|
init_remote_storage(x, build_tag)
|
||||||
|
.expect("cannot initialize remote extension storage from config")
|
||||||
|
});
|
||||||
|
|
||||||
let http_port = *matches
|
let http_port = *matches
|
||||||
.get_one::<u16>("http-port")
|
.get_one::<u16>("http-port")
|
||||||
@@ -128,9 +144,6 @@ fn main() -> Result<()> {
|
|||||||
let compute_id = matches.get_one::<String>("compute-id");
|
let compute_id = matches.get_one::<String>("compute-id");
|
||||||
let control_plane_uri = matches.get_one::<String>("control-plane-uri");
|
let control_plane_uri = matches.get_one::<String>("control-plane-uri");
|
||||||
|
|
||||||
// Try to use just 'postgres' if no path is provided
|
|
||||||
let pgbin = matches.get_one::<String>("pgbin").unwrap();
|
|
||||||
|
|
||||||
let spec;
|
let spec;
|
||||||
let mut live_config_allowed = false;
|
let mut live_config_allowed = false;
|
||||||
match spec_json {
|
match spec_json {
|
||||||
@@ -168,6 +181,7 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
let mut new_state = ComputeState::new();
|
let mut new_state = ComputeState::new();
|
||||||
let spec_set;
|
let spec_set;
|
||||||
|
|
||||||
if let Some(spec) = spec {
|
if let Some(spec) = spec {
|
||||||
let pspec = ParsedSpec::try_from(spec).map_err(|msg| anyhow::anyhow!(msg))?;
|
let pspec = ParsedSpec::try_from(spec).map_err(|msg| anyhow::anyhow!(msg))?;
|
||||||
new_state.pspec = Some(pspec);
|
new_state.pspec = Some(pspec);
|
||||||
@@ -179,9 +193,12 @@ fn main() -> Result<()> {
|
|||||||
connstr: Url::parse(connstr).context("cannot parse connstr as a URL")?,
|
connstr: Url::parse(connstr).context("cannot parse connstr as a URL")?,
|
||||||
pgdata: pgdata.to_string(),
|
pgdata: pgdata.to_string(),
|
||||||
pgbin: pgbin.to_string(),
|
pgbin: pgbin.to_string(),
|
||||||
|
pgversion: get_pg_version(pgbin),
|
||||||
live_config_allowed,
|
live_config_allowed,
|
||||||
state: Mutex::new(new_state),
|
state: Mutex::new(new_state),
|
||||||
state_changed: Condvar::new(),
|
state_changed: Condvar::new(),
|
||||||
|
ext_remote_storage,
|
||||||
|
available_extensions: OnceLock::new(),
|
||||||
};
|
};
|
||||||
let compute = Arc::new(compute_node);
|
let compute = Arc::new(compute_node);
|
||||||
|
|
||||||
@@ -190,6 +207,8 @@ fn main() -> Result<()> {
|
|||||||
let _http_handle =
|
let _http_handle =
|
||||||
launch_http_server(http_port, &compute).expect("cannot launch http endpoint thread");
|
launch_http_server(http_port, &compute).expect("cannot launch http endpoint thread");
|
||||||
|
|
||||||
|
let extension_server_port: u16 = http_port;
|
||||||
|
|
||||||
if !spec_set {
|
if !spec_set {
|
||||||
// No spec provided, hang waiting for it.
|
// No spec provided, hang waiting for it.
|
||||||
info!("no compute spec provided, waiting");
|
info!("no compute spec provided, waiting");
|
||||||
@@ -227,10 +246,13 @@ fn main() -> Result<()> {
|
|||||||
let _configurator_handle =
|
let _configurator_handle =
|
||||||
launch_configurator(&compute).expect("cannot launch configurator thread");
|
launch_configurator(&compute).expect("cannot launch configurator thread");
|
||||||
|
|
||||||
|
let _download_extensions_handle =
|
||||||
|
launch_download_extensions(&compute).expect("cannot launch download extensions thread");
|
||||||
|
|
||||||
// Start Postgres
|
// Start Postgres
|
||||||
let mut delay_exit = false;
|
let mut delay_exit = false;
|
||||||
let mut exit_code = None;
|
let mut exit_code = None;
|
||||||
let pg = match compute.start_compute() {
|
let pg = match compute.start_compute(extension_server_port) {
|
||||||
Ok(pg) => Some(pg),
|
Ok(pg) => Some(pg),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("could not start the compute node: {:?}", err);
|
error!("could not start the compute node: {:?}", err);
|
||||||
@@ -256,6 +278,16 @@ fn main() -> Result<()> {
|
|||||||
exit_code = ecode.code()
|
exit_code = ecode.code()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Maybe sync safekeepers again, to speed up next startup
|
||||||
|
let compute_state = compute.state.lock().unwrap().clone();
|
||||||
|
let pspec = compute_state.pspec.as_ref().expect("spec must be set");
|
||||||
|
if matches!(pspec.spec.mode, compute_api::spec::ComputeMode::Primary) {
|
||||||
|
info!("syncing safekeepers on shutdown");
|
||||||
|
let storage_auth_token = pspec.storage_auth_token.clone();
|
||||||
|
let lsn = compute.sync_safekeepers(storage_auth_token)?;
|
||||||
|
info!("synced safekeepers at lsn {lsn}");
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(err) = compute.check_for_core_dumps() {
|
if let Err(err) = compute.check_for_core_dumps() {
|
||||||
error!("error while checking for core dumps: {err:?}");
|
error!("error while checking for core dumps: {err:?}");
|
||||||
}
|
}
|
||||||
@@ -349,6 +381,12 @@ fn cli() -> clap::Command {
|
|||||||
.long("control-plane-uri")
|
.long("control-plane-uri")
|
||||||
.value_name("CONTROL_PLANE_API_BASE_URI"),
|
.value_name("CONTROL_PLANE_API_BASE_URI"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("remote-ext-config")
|
||||||
|
.short('r')
|
||||||
|
.long("remote-ext-config")
|
||||||
|
.value_name("REMOTE_EXT_CONFIG"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::{Condvar, Mutex};
|
use std::sync::{Condvar, Mutex, OnceLock};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use futures::future::join_all;
|
||||||
use postgres::{Client, NoTls};
|
use postgres::{Client, NoTls};
|
||||||
|
use tokio;
|
||||||
use tokio_postgres;
|
use tokio_postgres;
|
||||||
use tracing::{info, instrument, warn};
|
use tracing::{info, instrument, warn};
|
||||||
use utils::id::{TenantId, TimelineId};
|
use utils::id::{TenantId, TimelineId};
|
||||||
@@ -16,9 +19,11 @@ use utils::lsn::Lsn;
|
|||||||
use compute_api::responses::{ComputeMetrics, ComputeStatus};
|
use compute_api::responses::{ComputeMetrics, ComputeStatus};
|
||||||
use compute_api::spec::{ComputeMode, ComputeSpec};
|
use compute_api::spec::{ComputeMode, ComputeSpec};
|
||||||
|
|
||||||
use crate::config;
|
use remote_storage::GenericRemoteStorage;
|
||||||
|
|
||||||
use crate::pg_helpers::*;
|
use crate::pg_helpers::*;
|
||||||
use crate::spec::*;
|
use crate::spec::*;
|
||||||
|
use crate::{config, extension_server};
|
||||||
|
|
||||||
/// Compute node info shared across several `compute_ctl` threads.
|
/// Compute node info shared across several `compute_ctl` threads.
|
||||||
pub struct ComputeNode {
|
pub struct ComputeNode {
|
||||||
@@ -26,6 +31,7 @@ pub struct ComputeNode {
|
|||||||
pub connstr: url::Url,
|
pub connstr: url::Url,
|
||||||
pub pgdata: String,
|
pub pgdata: String,
|
||||||
pub pgbin: String,
|
pub pgbin: String,
|
||||||
|
pub pgversion: String,
|
||||||
/// We should only allow live re- / configuration of the compute node if
|
/// We should only allow live re- / configuration of the compute node if
|
||||||
/// it uses 'pull model', i.e. it can go to control-plane and fetch
|
/// it uses 'pull model', i.e. it can go to control-plane and fetch
|
||||||
/// the latest configuration. Otherwise, there could be a case:
|
/// the latest configuration. Otherwise, there could be a case:
|
||||||
@@ -45,6 +51,10 @@ pub struct ComputeNode {
|
|||||||
pub state: Mutex<ComputeState>,
|
pub state: Mutex<ComputeState>,
|
||||||
/// `Condvar` to allow notifying waiters about state changes.
|
/// `Condvar` to allow notifying waiters about state changes.
|
||||||
pub state_changed: Condvar,
|
pub state_changed: Condvar,
|
||||||
|
/// the S3 bucket that we search for extensions in
|
||||||
|
pub ext_remote_storage: Option<GenericRemoteStorage>,
|
||||||
|
// cached lists of available extensions and libraries
|
||||||
|
pub available_extensions: OnceLock<HashSet<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -133,6 +143,84 @@ impl TryFrom<ComputeSpec> for ParsedSpec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create special neon_superuser role, that's a slightly nerfed version of a real superuser
|
||||||
|
/// that we give to customers
|
||||||
|
fn create_neon_superuser(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
|
||||||
|
let roles = spec
|
||||||
|
.cluster
|
||||||
|
.roles
|
||||||
|
.iter()
|
||||||
|
.map(|r| format!("'{}'", escape_literal(&r.name)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let dbs = spec
|
||||||
|
.cluster
|
||||||
|
.databases
|
||||||
|
.iter()
|
||||||
|
.map(|db| format!("'{}'", escape_literal(&db.name)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let roles_decl = if roles.is_empty() {
|
||||||
|
String::from("roles text[] := NULL;")
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
roles text[] := ARRAY(SELECT rolname
|
||||||
|
FROM pg_catalog.pg_roles
|
||||||
|
WHERE rolname IN ({}));"#,
|
||||||
|
roles.join(", ")
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let database_decl = if dbs.is_empty() {
|
||||||
|
String::from("dbs text[] := NULL;")
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
dbs text[] := ARRAY(SELECT datname
|
||||||
|
FROM pg_catalog.pg_database
|
||||||
|
WHERE datname IN ({}));"#,
|
||||||
|
dbs.join(", ")
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ALL PRIVILEGES grants CREATE, CONNECT, and TEMPORARY on all databases
|
||||||
|
// (see https://www.postgresql.org/docs/current/ddl-priv.html)
|
||||||
|
let query = format!(
|
||||||
|
r#"
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
r text;
|
||||||
|
{}
|
||||||
|
{}
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT FROM pg_catalog.pg_roles WHERE rolname = 'neon_superuser')
|
||||||
|
THEN
|
||||||
|
CREATE ROLE neon_superuser CREATEDB CREATEROLE NOLOGIN IN ROLE pg_read_all_data, pg_write_all_data;
|
||||||
|
IF array_length(roles, 1) IS NOT NULL THEN
|
||||||
|
EXECUTE format('GRANT neon_superuser TO %s',
|
||||||
|
array_to_string(ARRAY(SELECT quote_ident(x) FROM unnest(roles) as x), ', '));
|
||||||
|
FOREACH r IN ARRAY roles LOOP
|
||||||
|
EXECUTE format('ALTER ROLE %s CREATEROLE CREATEDB', quote_ident(r));
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
IF array_length(dbs, 1) IS NOT NULL THEN
|
||||||
|
EXECUTE format('GRANT ALL PRIVILEGES ON DATABASE %s TO neon_superuser',
|
||||||
|
array_to_string(ARRAY(SELECT quote_ident(x) FROM unnest(dbs) as x), ', '));
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;"#,
|
||||||
|
roles_decl, database_decl,
|
||||||
|
);
|
||||||
|
info!("Neon superuser created:\n{}", &query);
|
||||||
|
client
|
||||||
|
.simple_query(&query)
|
||||||
|
.map_err(|e| anyhow::anyhow!(e).context(query))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
impl ComputeNode {
|
impl ComputeNode {
|
||||||
pub fn set_status(&self, status: ComputeStatus) {
|
pub fn set_status(&self, status: ComputeStatus) {
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
@@ -157,7 +245,7 @@ impl ComputeNode {
|
|||||||
|
|
||||||
// Get basebackup from the libpq connection to pageserver using `connstr` and
|
// Get basebackup from the libpq connection to pageserver using `connstr` and
|
||||||
// unarchive it to `pgdata` directory overriding all its previous content.
|
// unarchive it to `pgdata` directory overriding all its previous content.
|
||||||
#[instrument(skip(self, compute_state))]
|
#[instrument(skip_all, fields(%lsn))]
|
||||||
fn get_basebackup(&self, compute_state: &ComputeState, lsn: Lsn) -> Result<()> {
|
fn get_basebackup(&self, compute_state: &ComputeState, lsn: Lsn) -> Result<()> {
|
||||||
let spec = compute_state.pspec.as_ref().expect("spec must be set");
|
let spec = compute_state.pspec.as_ref().expect("spec must be set");
|
||||||
let start_time = Utc::now();
|
let start_time = Utc::now();
|
||||||
@@ -199,8 +287,8 @@ impl ComputeNode {
|
|||||||
|
|
||||||
// Run `postgres` in a special mode with `--sync-safekeepers` argument
|
// Run `postgres` in a special mode with `--sync-safekeepers` argument
|
||||||
// and return the reported LSN back to the caller.
|
// and return the reported LSN back to the caller.
|
||||||
#[instrument(skip(self, storage_auth_token))]
|
#[instrument(skip_all)]
|
||||||
fn sync_safekeepers(&self, storage_auth_token: Option<String>) -> Result<Lsn> {
|
pub fn sync_safekeepers(&self, storage_auth_token: Option<String>) -> Result<Lsn> {
|
||||||
let start_time = Utc::now();
|
let start_time = Utc::now();
|
||||||
|
|
||||||
let sync_handle = Command::new(&self.pgbin)
|
let sync_handle = Command::new(&self.pgbin)
|
||||||
@@ -244,15 +332,23 @@ impl ComputeNode {
|
|||||||
|
|
||||||
/// Do all the preparations like PGDATA directory creation, configuration,
|
/// Do all the preparations like PGDATA directory creation, configuration,
|
||||||
/// safekeepers sync, basebackup, etc.
|
/// safekeepers sync, basebackup, etc.
|
||||||
#[instrument(skip(self, compute_state))]
|
#[instrument(skip_all)]
|
||||||
pub fn prepare_pgdata(&self, compute_state: &ComputeState) -> Result<()> {
|
pub fn prepare_pgdata(
|
||||||
|
&self,
|
||||||
|
compute_state: &ComputeState,
|
||||||
|
extension_server_port: u16,
|
||||||
|
) -> Result<()> {
|
||||||
let pspec = compute_state.pspec.as_ref().expect("spec must be set");
|
let pspec = compute_state.pspec.as_ref().expect("spec must be set");
|
||||||
let spec = &pspec.spec;
|
let spec = &pspec.spec;
|
||||||
let pgdata_path = Path::new(&self.pgdata);
|
let pgdata_path = Path::new(&self.pgdata);
|
||||||
|
|
||||||
// Remove/create an empty pgdata directory and put configuration there.
|
// Remove/create an empty pgdata directory and put configuration there.
|
||||||
self.create_pgdata()?;
|
self.create_pgdata()?;
|
||||||
config::write_postgres_conf(&pgdata_path.join("postgresql.conf"), &pspec.spec)?;
|
config::write_postgres_conf(
|
||||||
|
&pgdata_path.join("postgresql.conf"),
|
||||||
|
&pspec.spec,
|
||||||
|
Some(extension_server_port),
|
||||||
|
)?;
|
||||||
|
|
||||||
// Syncing safekeepers is only safe with primary nodes: if a primary
|
// Syncing safekeepers is only safe with primary nodes: if a primary
|
||||||
// is already connected it will be kicked out, so a secondary (standby)
|
// is already connected it will be kicked out, so a secondary (standby)
|
||||||
@@ -302,7 +398,7 @@ impl ComputeNode {
|
|||||||
|
|
||||||
/// Start Postgres as a child process and manage DBs/roles.
|
/// Start Postgres as a child process and manage DBs/roles.
|
||||||
/// After that this will hang waiting on the postmaster process to exit.
|
/// After that this will hang waiting on the postmaster process to exit.
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip_all)]
|
||||||
pub fn start_postgres(
|
pub fn start_postgres(
|
||||||
&self,
|
&self,
|
||||||
storage_auth_token: Option<String>,
|
storage_auth_token: Option<String>,
|
||||||
@@ -326,7 +422,7 @@ impl ComputeNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Do initial configuration of the already started Postgres.
|
/// Do initial configuration of the already started Postgres.
|
||||||
#[instrument(skip(self, compute_state))]
|
#[instrument(skip_all)]
|
||||||
pub fn apply_config(&self, compute_state: &ComputeState) -> Result<()> {
|
pub fn apply_config(&self, compute_state: &ComputeState) -> Result<()> {
|
||||||
// If connection fails,
|
// If connection fails,
|
||||||
// it may be the old node with `zenith_admin` superuser.
|
// it may be the old node with `zenith_admin` superuser.
|
||||||
@@ -347,6 +443,8 @@ impl ComputeNode {
|
|||||||
.map_err(|_| anyhow::anyhow!("invalid connstr"))?;
|
.map_err(|_| anyhow::anyhow!("invalid connstr"))?;
|
||||||
|
|
||||||
let mut client = Client::connect(zenith_admin_connstr.as_str(), NoTls)?;
|
let mut client = Client::connect(zenith_admin_connstr.as_str(), NoTls)?;
|
||||||
|
// Disable forwarding so that users don't get a cloud_admin role
|
||||||
|
client.simple_query("SET neon.forward_ddl = false")?;
|
||||||
client.simple_query("CREATE USER cloud_admin WITH SUPERUSER")?;
|
client.simple_query("CREATE USER cloud_admin WITH SUPERUSER")?;
|
||||||
client.simple_query("GRANT zenith_admin TO cloud_admin")?;
|
client.simple_query("GRANT zenith_admin TO cloud_admin")?;
|
||||||
drop(client);
|
drop(client);
|
||||||
@@ -357,14 +455,16 @@ impl ComputeNode {
|
|||||||
Ok(client) => client,
|
Ok(client) => client,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Proceed with post-startup configuration. Note, that order of operations is important.
|
|
||||||
// Disable DDL forwarding because control plane already knows about these roles/databases.
|
// Disable DDL forwarding because control plane already knows about these roles/databases.
|
||||||
client.simple_query("SET neon.forward_ddl = false")?;
|
client.simple_query("SET neon.forward_ddl = false")?;
|
||||||
|
|
||||||
|
// Proceed with post-startup configuration. Note, that order of operations is important.
|
||||||
let spec = &compute_state.pspec.as_ref().expect("spec must be set").spec;
|
let spec = &compute_state.pspec.as_ref().expect("spec must be set").spec;
|
||||||
|
create_neon_superuser(spec, &mut client)?;
|
||||||
handle_roles(spec, &mut client)?;
|
handle_roles(spec, &mut client)?;
|
||||||
handle_databases(spec, &mut client)?;
|
handle_databases(spec, &mut client)?;
|
||||||
handle_role_deletions(spec, self.connstr.as_str(), &mut client)?;
|
handle_role_deletions(spec, self.connstr.as_str(), &mut client)?;
|
||||||
handle_grants(spec, self.connstr.as_str(), &mut client)?;
|
handle_grants(spec, self.connstr.as_str())?;
|
||||||
handle_extensions(spec, &mut client)?;
|
handle_extensions(spec, &mut client)?;
|
||||||
|
|
||||||
// 'Close' connection
|
// 'Close' connection
|
||||||
@@ -376,7 +476,7 @@ impl ComputeNode {
|
|||||||
// We could've wrapped this around `pg_ctl reload`, but right now we don't use
|
// We could've wrapped this around `pg_ctl reload`, but right now we don't use
|
||||||
// `pg_ctl` for start / stop, so this just seems much easier to do as we already
|
// `pg_ctl` for start / stop, so this just seems much easier to do as we already
|
||||||
// have opened connection to Postgres and superuser access.
|
// have opened connection to Postgres and superuser access.
|
||||||
#[instrument(skip(self, client))]
|
#[instrument(skip_all)]
|
||||||
fn pg_reload_conf(&self, client: &mut Client) -> Result<()> {
|
fn pg_reload_conf(&self, client: &mut Client) -> Result<()> {
|
||||||
client.simple_query("SELECT pg_reload_conf()")?;
|
client.simple_query("SELECT pg_reload_conf()")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -384,13 +484,13 @@ impl ComputeNode {
|
|||||||
|
|
||||||
/// Similar to `apply_config()`, but does a bit different sequence of operations,
|
/// Similar to `apply_config()`, but does a bit different sequence of operations,
|
||||||
/// as it's used to reconfigure a previously started and configured Postgres node.
|
/// as it's used to reconfigure a previously started and configured Postgres node.
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip_all)]
|
||||||
pub fn reconfigure(&self) -> Result<()> {
|
pub fn reconfigure(&self) -> Result<()> {
|
||||||
let spec = self.state.lock().unwrap().pspec.clone().unwrap().spec;
|
let spec = self.state.lock().unwrap().pspec.clone().unwrap().spec;
|
||||||
|
|
||||||
// Write new config
|
// Write new config
|
||||||
let pgdata_path = Path::new(&self.pgdata);
|
let pgdata_path = Path::new(&self.pgdata);
|
||||||
config::write_postgres_conf(&pgdata_path.join("postgresql.conf"), &spec)?;
|
config::write_postgres_conf(&pgdata_path.join("postgresql.conf"), &spec, None)?;
|
||||||
|
|
||||||
let mut client = Client::connect(self.connstr.as_str(), NoTls)?;
|
let mut client = Client::connect(self.connstr.as_str(), NoTls)?;
|
||||||
self.pg_reload_conf(&mut client)?;
|
self.pg_reload_conf(&mut client)?;
|
||||||
@@ -402,7 +502,7 @@ impl ComputeNode {
|
|||||||
handle_roles(&spec, &mut client)?;
|
handle_roles(&spec, &mut client)?;
|
||||||
handle_databases(&spec, &mut client)?;
|
handle_databases(&spec, &mut client)?;
|
||||||
handle_role_deletions(&spec, self.connstr.as_str(), &mut client)?;
|
handle_role_deletions(&spec, self.connstr.as_str(), &mut client)?;
|
||||||
handle_grants(&spec, self.connstr.as_str(), &mut client)?;
|
handle_grants(&spec, self.connstr.as_str())?;
|
||||||
handle_extensions(&spec, &mut client)?;
|
handle_extensions(&spec, &mut client)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,8 +519,8 @@ impl ComputeNode {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip_all)]
|
||||||
pub fn start_compute(&self) -> Result<std::process::Child> {
|
pub fn start_compute(&self, extension_server_port: u16) -> Result<std::process::Child> {
|
||||||
let compute_state = self.state.lock().unwrap().clone();
|
let compute_state = self.state.lock().unwrap().clone();
|
||||||
let pspec = compute_state.pspec.as_ref().expect("spec must be set");
|
let pspec = compute_state.pspec.as_ref().expect("spec must be set");
|
||||||
info!(
|
info!(
|
||||||
@@ -431,7 +531,26 @@ impl ComputeNode {
|
|||||||
pspec.timeline_id,
|
pspec.timeline_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.prepare_pgdata(&compute_state)?;
|
// This part is sync, because we need to download
|
||||||
|
// remote shared_preload_libraries before postgres start (if any)
|
||||||
|
{
|
||||||
|
let library_load_start_time = Utc::now();
|
||||||
|
self.prepare_preload_libraries(&compute_state)?;
|
||||||
|
|
||||||
|
let library_load_time = Utc::now()
|
||||||
|
.signed_duration_since(library_load_start_time)
|
||||||
|
.to_std()
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as u64;
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
state.metrics.load_libraries_ms = library_load_time;
|
||||||
|
info!(
|
||||||
|
"Loading shared_preload_libraries took {:?}ms",
|
||||||
|
library_load_time
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.prepare_pgdata(&compute_state, extension_server_port)?;
|
||||||
|
|
||||||
let start_time = Utc::now();
|
let start_time = Utc::now();
|
||||||
|
|
||||||
@@ -567,4 +686,92 @@ LIMIT 100",
|
|||||||
"{{\"pg_stat_statements\": []}}".to_string()
|
"{{\"pg_stat_statements\": []}}".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If remote extension storage is configured,
|
||||||
|
// download extension control files
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn prepare_external_extensions(&self, compute_state: &ComputeState) -> Result<()> {
|
||||||
|
if let Some(ref ext_remote_storage) = self.ext_remote_storage {
|
||||||
|
let pspec = compute_state.pspec.as_ref().expect("spec must be set");
|
||||||
|
let spec = &pspec.spec;
|
||||||
|
let custom_ext_prefixes = spec.custom_extensions.clone().unwrap_or(Vec::new());
|
||||||
|
info!("custom_ext_prefixes: {:?}", &custom_ext_prefixes);
|
||||||
|
let available_extensions = extension_server::get_available_extensions(
|
||||||
|
ext_remote_storage,
|
||||||
|
&self.pgbin,
|
||||||
|
&self.pgversion,
|
||||||
|
&custom_ext_prefixes,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
self.available_extensions
|
||||||
|
.set(available_extensions)
|
||||||
|
.expect("available_extensions.set error");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn download_extension(&self, ext_name: &str) -> Result<()> {
|
||||||
|
match &self.ext_remote_storage {
|
||||||
|
None => anyhow::bail!("No remote extension storage"),
|
||||||
|
Some(remote_storage) => {
|
||||||
|
extension_server::download_extension(
|
||||||
|
ext_name,
|
||||||
|
remote_storage,
|
||||||
|
&self.pgbin,
|
||||||
|
&self.pgversion,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn prepare_preload_libraries(&self, compute_state: &ComputeState) -> Result<()> {
|
||||||
|
if self.ext_remote_storage.is_none() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let pspec = compute_state.pspec.as_ref().expect("spec must be set");
|
||||||
|
let spec = &pspec.spec;
|
||||||
|
|
||||||
|
info!("parse shared_preload_libraries from spec.cluster.settings");
|
||||||
|
let mut libs_vec = Vec::new();
|
||||||
|
if let Some(libs) = spec.cluster.settings.find("shared_preload_libraries") {
|
||||||
|
libs_vec = libs
|
||||||
|
.split(&[',', '\'', ' '])
|
||||||
|
.filter(|s| *s != "neon" && !s.is_empty())
|
||||||
|
.map(str::to_string)
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
info!("parse shared_preload_libraries from provided postgresql.conf");
|
||||||
|
// that is used in neon_local and python tests
|
||||||
|
if let Some(conf) = &spec.cluster.postgresql_conf {
|
||||||
|
let conf_lines = conf.split('\n').collect::<Vec<&str>>();
|
||||||
|
let mut shared_preload_libraries_line = "";
|
||||||
|
for line in conf_lines {
|
||||||
|
if line.starts_with("shared_preload_libraries") {
|
||||||
|
shared_preload_libraries_line = line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut preload_libs_vec = Vec::new();
|
||||||
|
if let Some(libs) = shared_preload_libraries_line.split("='").nth(1) {
|
||||||
|
preload_libs_vec = libs
|
||||||
|
.split(&[',', '\'', ' '])
|
||||||
|
.filter(|s| *s != "neon" && !s.is_empty())
|
||||||
|
.map(str::to_string)
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
libs_vec.extend(preload_libs_vec);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Downloading to shared preload libraries: {:?}", &libs_vec);
|
||||||
|
let mut download_tasks = Vec::new();
|
||||||
|
for library in &libs_vec {
|
||||||
|
download_tasks.push(self.download_extension(library));
|
||||||
|
}
|
||||||
|
let results = join_all(download_tasks).await;
|
||||||
|
for result in results {
|
||||||
|
result?; // propogate any errors
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ pub fn line_in_file(path: &Path, line: &str) -> Result<bool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create or completely rewrite configuration file specified by `path`
|
/// Create or completely rewrite configuration file specified by `path`
|
||||||
pub fn write_postgres_conf(path: &Path, spec: &ComputeSpec) -> Result<()> {
|
pub fn write_postgres_conf(
|
||||||
|
path: &Path,
|
||||||
|
spec: &ComputeSpec,
|
||||||
|
extension_server_port: Option<u16>,
|
||||||
|
) -> Result<()> {
|
||||||
// File::create() destroys the file content if it exists.
|
// File::create() destroys the file content if it exists.
|
||||||
let mut file = File::create(path)?;
|
let mut file = File::create(path)?;
|
||||||
|
|
||||||
@@ -95,5 +99,9 @@ pub fn write_postgres_conf(path: &Path, spec: &ComputeSpec) -> Result<()> {
|
|||||||
writeln!(file, "# Managed by compute_ctl: end")?;
|
writeln!(file, "# Managed by compute_ctl: end")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(port) = extension_server_port {
|
||||||
|
writeln!(file, "neon.extension_server_port={}", port)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use compute_api::responses::ComputeStatus;
|
|||||||
|
|
||||||
use crate::compute::ComputeNode;
|
use crate::compute::ComputeNode;
|
||||||
|
|
||||||
#[instrument(skip(compute))]
|
#[instrument(skip_all)]
|
||||||
fn configurator_main_loop(compute: &Arc<ComputeNode>) {
|
fn configurator_main_loop(compute: &Arc<ComputeNode>) {
|
||||||
info!("waiting for reconfiguration requests");
|
info!("waiting for reconfiguration requests");
|
||||||
loop {
|
loop {
|
||||||
@@ -42,13 +42,15 @@ fn configurator_main_loop(compute: &Arc<ComputeNode>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn launch_configurator(compute: &Arc<ComputeNode>) -> Result<thread::JoinHandle<()>> {
|
pub fn launch_configurator(
|
||||||
|
compute: &Arc<ComputeNode>,
|
||||||
|
) -> Result<thread::JoinHandle<()>, std::io::Error> {
|
||||||
let compute = Arc::clone(compute);
|
let compute = Arc::clone(compute);
|
||||||
|
|
||||||
Ok(thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name("compute-configurator".into())
|
.name("compute-configurator".into())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
configurator_main_loop(&compute);
|
configurator_main_loop(&compute);
|
||||||
info!("configurator thread is exited");
|
info!("configurator thread is exited");
|
||||||
})?)
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
237
compute_tools/src/extension_server.rs
Normal file
237
compute_tools/src/extension_server.rs
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
// Download extension files from the extension store
|
||||||
|
// and put them in the right place in the postgres directory
|
||||||
|
/*
|
||||||
|
The layout of the S3 bucket is as follows:
|
||||||
|
|
||||||
|
v14/ext_index.json
|
||||||
|
-- this contains information necessary to create control files
|
||||||
|
v14/extensions/test_ext1.tar.gz
|
||||||
|
-- this contains the library files and sql files necessary to create this extension
|
||||||
|
v14/extensions/custom_ext1.tar.gz
|
||||||
|
|
||||||
|
The difference between a private and public extensions is determined by who can
|
||||||
|
load the extension this is specified in ext_index.json
|
||||||
|
|
||||||
|
Speicially, ext_index.json has a list of public extensions, and a list of
|
||||||
|
extensions enabled for specific tenant-ids.
|
||||||
|
*/
|
||||||
|
use crate::compute::ComputeNode;
|
||||||
|
use anyhow::Context;
|
||||||
|
use anyhow::{self, Result};
|
||||||
|
use flate2::read::GzDecoder;
|
||||||
|
use remote_storage::*;
|
||||||
|
use serde_json::{self, Value};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::num::{NonZeroU32, NonZeroUsize};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::str;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
use tar::Archive;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
fn get_pg_config(argument: &str, pgbin: &str) -> String {
|
||||||
|
// gives the result of `pg_config [argument]`
|
||||||
|
// where argument is a flag like `--version` or `--sharedir`
|
||||||
|
let pgconfig = pgbin
|
||||||
|
.strip_suffix("postgres")
|
||||||
|
.expect("bad pgbin")
|
||||||
|
.to_owned()
|
||||||
|
+ "/pg_config";
|
||||||
|
let config_output = std::process::Command::new(pgconfig)
|
||||||
|
.arg(argument)
|
||||||
|
.output()
|
||||||
|
.expect("pg_config error");
|
||||||
|
std::str::from_utf8(&config_output.stdout)
|
||||||
|
.expect("pg_config error")
|
||||||
|
.trim()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_pg_version(pgbin: &str) -> String {
|
||||||
|
// pg_config --version returns a (platform specific) human readable string
|
||||||
|
// such as "PostgreSQL 15.4". We parse this to v14/v15
|
||||||
|
let human_version = get_pg_config("--version", pgbin);
|
||||||
|
if human_version.contains("15") {
|
||||||
|
return "v15".to_string();
|
||||||
|
} else if human_version.contains("14") {
|
||||||
|
return "v14".to_string();
|
||||||
|
}
|
||||||
|
panic!("Unsuported postgres version {human_version}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// download extension control files
|
||||||
|
// if custom_ext_prefixes is provided - search also in custom extension paths
|
||||||
|
pub async fn get_available_extensions(
|
||||||
|
remote_storage: &GenericRemoteStorage,
|
||||||
|
pgbin: &str,
|
||||||
|
pg_version: &str,
|
||||||
|
custom_ext_prefixes: &[String],
|
||||||
|
) -> Result<HashSet<String>> {
|
||||||
|
let local_sharedir = Path::new(&get_pg_config("--sharedir", pgbin)).join("extension");
|
||||||
|
let index_path = pg_version.to_owned() + "/ext_index.json";
|
||||||
|
let index_path = RemotePath::new(Path::new(&index_path)).context("error forming path")?;
|
||||||
|
info!("download ext_index.json: {:?}", &index_path);
|
||||||
|
|
||||||
|
// TODO: potential optimization: cache ext_index.json
|
||||||
|
let mut download = remote_storage.download(&index_path).await?;
|
||||||
|
let mut write_data_buffer = Vec::new();
|
||||||
|
download
|
||||||
|
.download_stream
|
||||||
|
.read_to_end(&mut write_data_buffer)
|
||||||
|
.await?;
|
||||||
|
let ext_index_str = match str::from_utf8(&write_data_buffer) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let ext_index_full: Value = serde_json::from_str(ext_index_str)?;
|
||||||
|
let ext_index_full = ext_index_full.as_object().context("error parsing json")?;
|
||||||
|
let control_data = ext_index_full["control_data"]
|
||||||
|
.as_object()
|
||||||
|
.context("json parse error")?;
|
||||||
|
let enabled_extensions = ext_index_full["enabled_extensions"]
|
||||||
|
.as_object()
|
||||||
|
.context("json parse error")?;
|
||||||
|
info!("{:?}", control_data.clone());
|
||||||
|
info!("{:?}", enabled_extensions.clone());
|
||||||
|
|
||||||
|
let mut prefixes = vec!["public".to_string()];
|
||||||
|
prefixes.extend(custom_ext_prefixes.to_owned());
|
||||||
|
info!("{:?}", &prefixes);
|
||||||
|
let mut all_extensions = HashSet::new();
|
||||||
|
for prefix in prefixes {
|
||||||
|
let prefix_extensions = match enabled_extensions.get(&prefix) {
|
||||||
|
Some(Value::Array(ext_name)) => ext_name,
|
||||||
|
_ => {
|
||||||
|
info!("prefix {} has no extensions", prefix);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!("{:?}", prefix_extensions);
|
||||||
|
for ext_name in prefix_extensions {
|
||||||
|
all_extensions.insert(ext_name.as_str().context("json parse error")?.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for prefix in &all_extensions {
|
||||||
|
let control_contents = control_data[prefix].as_str().context("json parse error")?;
|
||||||
|
let control_path = local_sharedir.join(prefix.to_owned() + ".control");
|
||||||
|
|
||||||
|
info!("WRITING FILE {:?}{:?}", control_path, control_contents);
|
||||||
|
std::fs::write(control_path, control_contents)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(all_extensions.into_iter().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
// download all sqlfiles (and possibly data files) for a given extension name
|
||||||
|
pub async fn download_extension(
|
||||||
|
ext_name: &str,
|
||||||
|
remote_storage: &GenericRemoteStorage,
|
||||||
|
pgbin: &str,
|
||||||
|
pg_version: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
// TODO: potential optimization: only download the extension if it doesn't exist
|
||||||
|
// problem: how would we tell if it exists?
|
||||||
|
let ext_name = ext_name.replace(".so", "");
|
||||||
|
let ext_name_targz = ext_name.to_owned() + ".tar.gz";
|
||||||
|
if Path::new(&ext_name_targz).exists() {
|
||||||
|
info!("extension {:?} already exists", ext_name_targz);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let ext_path = RemotePath::new(
|
||||||
|
&Path::new(pg_version)
|
||||||
|
.join("extensions")
|
||||||
|
.join(ext_name_targz.clone()),
|
||||||
|
)?;
|
||||||
|
info!(
|
||||||
|
"Start downloading extension {:?} from {:?}",
|
||||||
|
ext_name, ext_path
|
||||||
|
);
|
||||||
|
let mut download = remote_storage.download(&ext_path).await?;
|
||||||
|
let mut write_data_buffer = Vec::new();
|
||||||
|
download
|
||||||
|
.download_stream
|
||||||
|
.read_to_end(&mut write_data_buffer)
|
||||||
|
.await?;
|
||||||
|
let unzip_dest = pgbin.strip_suffix("/bin/postgres").expect("bad pgbin");
|
||||||
|
let tar = GzDecoder::new(std::io::Cursor::new(write_data_buffer));
|
||||||
|
let mut archive = Archive::new(tar);
|
||||||
|
archive.unpack(unzip_dest)?;
|
||||||
|
info!("Download + unzip {:?} completed successfully", &ext_path);
|
||||||
|
|
||||||
|
let local_sharedir = Path::new(&get_pg_config("--sharedir", pgbin)).join("extension");
|
||||||
|
let zip_sharedir = format!("{unzip_dest}/extensions/{ext_name}/share/extension");
|
||||||
|
info!("mv {zip_sharedir:?}/* {local_sharedir:?}");
|
||||||
|
for file in std::fs::read_dir(zip_sharedir)? {
|
||||||
|
let old_file = file?.path();
|
||||||
|
let new_file =
|
||||||
|
Path::new(&local_sharedir).join(old_file.file_name().context("error parsing file")?);
|
||||||
|
std::fs::rename(old_file, new_file)?;
|
||||||
|
}
|
||||||
|
let local_libdir = Path::new(&get_pg_config("--libdir", pgbin)).join("postgresql");
|
||||||
|
let zip_libdir = format!("{unzip_dest}/extensions/{ext_name}/lib");
|
||||||
|
info!("mv {zip_libdir:?}/* {local_libdir:?}");
|
||||||
|
for file in std::fs::read_dir(zip_libdir)? {
|
||||||
|
let old_file = file?.path();
|
||||||
|
let new_file =
|
||||||
|
Path::new(&local_libdir).join(old_file.file_name().context("error parsing file")?);
|
||||||
|
std::fs::rename(old_file, new_file)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function initializes the necessary structs to use remmote storage (should be fairly cheap)
|
||||||
|
pub fn init_remote_storage(
|
||||||
|
remote_ext_config: &str,
|
||||||
|
default_prefix: &str,
|
||||||
|
) -> anyhow::Result<GenericRemoteStorage> {
|
||||||
|
let remote_ext_config: serde_json::Value = serde_json::from_str(remote_ext_config)?;
|
||||||
|
|
||||||
|
let remote_ext_bucket = remote_ext_config["bucket"]
|
||||||
|
.as_str()
|
||||||
|
.context("config parse error")?;
|
||||||
|
let remote_ext_region = remote_ext_config["region"]
|
||||||
|
.as_str()
|
||||||
|
.context("config parse error")?;
|
||||||
|
let remote_ext_endpoint = remote_ext_config["endpoint"].as_str();
|
||||||
|
let remote_ext_prefix = remote_ext_config["prefix"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or(default_prefix)
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// TODO: potentially allow modification of other parameters
|
||||||
|
// however, default values should be fine for now
|
||||||
|
let config = S3Config {
|
||||||
|
bucket_name: remote_ext_bucket.to_string(),
|
||||||
|
bucket_region: remote_ext_region.to_string(),
|
||||||
|
prefix_in_bucket: Some(remote_ext_prefix),
|
||||||
|
endpoint: remote_ext_endpoint.map(|x| x.to_string()),
|
||||||
|
concurrency_limit: NonZeroUsize::new(100).expect("100 != 0"),
|
||||||
|
max_keys_per_list_response: None,
|
||||||
|
};
|
||||||
|
let config = RemoteStorageConfig {
|
||||||
|
max_concurrent_syncs: NonZeroUsize::new(100).expect("100 != 0"),
|
||||||
|
max_sync_errors: NonZeroU32::new(100).expect("100 != 0"),
|
||||||
|
storage: RemoteStorageKind::AwsS3(config),
|
||||||
|
};
|
||||||
|
GenericRemoteStorage::from_config(&config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn launch_download_extensions(
|
||||||
|
compute: &Arc<ComputeNode>,
|
||||||
|
) -> Result<thread::JoinHandle<()>, std::io::Error> {
|
||||||
|
let compute = Arc::clone(compute);
|
||||||
|
thread::Builder::new()
|
||||||
|
.name("download-extensions".into())
|
||||||
|
.spawn(move || {
|
||||||
|
info!("start download_extension_files");
|
||||||
|
let compute_state = compute.state.lock().expect("error unlocking compute.state");
|
||||||
|
compute
|
||||||
|
.prepare_external_extensions(&compute_state)
|
||||||
|
.expect("error preparing extensions");
|
||||||
|
info!("download_extension_files done, exiting thread");
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -121,6 +121,27 @@ async fn routes(req: Request<Body>, compute: &Arc<ComputeNode>) -> Response<Body
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// download extension files from S3 on demand
|
||||||
|
(&Method::POST, route) if route.starts_with("/extension_server/") => {
|
||||||
|
info!("serving {:?} POST request", route);
|
||||||
|
info!("req.uri {:?}", req.uri());
|
||||||
|
let filename = route.split('/').last().unwrap().to_string();
|
||||||
|
info!(
|
||||||
|
"serving /extension_server POST request, filename: {:?}",
|
||||||
|
&filename
|
||||||
|
);
|
||||||
|
|
||||||
|
match compute.download_extension(&filename).await {
|
||||||
|
Ok(_) => Response::new(Body::from("OK")),
|
||||||
|
Err(e) => {
|
||||||
|
error!("extension download failed: {}", e);
|
||||||
|
let mut resp = Response::new(Body::from(e.to_string()));
|
||||||
|
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||||
|
resp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return the `404 Not Found` for any other routes.
|
// Return the `404 Not Found` for any other routes.
|
||||||
_ => {
|
_ => {
|
||||||
let mut not_found = Response::new(Body::from("404 Not Found"));
|
let mut not_found = Response::new(Body::from("404 Not Found"));
|
||||||
|
|||||||
@@ -139,6 +139,34 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/GenericError"
|
$ref: "#/components/schemas/GenericError"
|
||||||
|
/extension_server:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Extension
|
||||||
|
summary: Download extension from S3 to local folder.
|
||||||
|
description: ""
|
||||||
|
operationId: downloadExtension
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Extension downloaded
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Error text or 'OK' if download succeeded.
|
||||||
|
example: "OK"
|
||||||
|
400:
|
||||||
|
description: Request is invalid.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/GenericError"
|
||||||
|
500:
|
||||||
|
description: Extension download request failed.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/GenericError"
|
||||||
|
|
||||||
components:
|
components:
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ pub mod http;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
pub mod compute;
|
pub mod compute;
|
||||||
|
pub mod extension_server;
|
||||||
pub mod monitor;
|
pub mod monitor;
|
||||||
pub mod params;
|
pub mod params;
|
||||||
pub mod pg_helpers;
|
pub mod pg_helpers;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pub fn init_tracing_and_logging(default_log_level: &str) -> anyhow::Result<()> {
|
|||||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_log_level));
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_log_level));
|
||||||
|
|
||||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||||
|
.with_ansi(false)
|
||||||
.with_target(false)
|
.with_target(false)
|
||||||
.with_writer(std::io::stderr);
|
.with_writer(std::io::stderr);
|
||||||
|
|
||||||
|
|||||||
@@ -105,10 +105,10 @@ fn watch_compute_activity(compute: &ComputeNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Launch a separate compute monitor thread and return its `JoinHandle`.
|
/// Launch a separate compute monitor thread and return its `JoinHandle`.
|
||||||
pub fn launch_monitor(state: &Arc<ComputeNode>) -> Result<thread::JoinHandle<()>> {
|
pub fn launch_monitor(state: &Arc<ComputeNode>) -> Result<thread::JoinHandle<()>, std::io::Error> {
|
||||||
let state = Arc::clone(state);
|
let state = Arc::clone(state);
|
||||||
|
|
||||||
Ok(thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name("compute-monitor".into())
|
.name("compute-monitor".into())
|
||||||
.spawn(move || watch_compute_activity(&state))?)
|
.spawn(move || watch_compute_activity(&state))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use compute_api::spec::{Database, GenericOption, GenericOptions, PgIdent, Role};
|
|||||||
const POSTGRES_WAIT_TIMEOUT: Duration = Duration::from_millis(60 * 1000); // milliseconds
|
const POSTGRES_WAIT_TIMEOUT: Duration = Duration::from_millis(60 * 1000); // milliseconds
|
||||||
|
|
||||||
/// Escape a string for including it in a SQL literal
|
/// Escape a string for including it in a SQL literal
|
||||||
fn escape_literal(s: &str) -> String {
|
pub fn escape_literal(s: &str) -> String {
|
||||||
s.replace('\'', "''").replace('\\', "\\\\")
|
s.replace('\'', "''").replace('\\', "\\\\")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ pub fn get_existing_dbs(client: &mut Client) -> Result<Vec<Database>> {
|
|||||||
/// Wait for Postgres to become ready to accept connections. It's ready to
|
/// Wait for Postgres to become ready to accept connections. It's ready to
|
||||||
/// accept connections when the state-field in `pgdata/postmaster.pid` says
|
/// accept connections when the state-field in `pgdata/postmaster.pid` says
|
||||||
/// 'ready'.
|
/// 'ready'.
|
||||||
#[instrument(skip(pg))]
|
#[instrument(skip_all, fields(pgdata = %pgdata.display()))]
|
||||||
pub fn wait_for_postgres(pg: &mut Child, pgdata: &Path) -> Result<()> {
|
pub fn wait_for_postgres(pg: &mut Child, pgdata: &Path) -> Result<()> {
|
||||||
let pid_path = pgdata.join("postmaster.pid");
|
let pid_path = pgdata.join("postmaster.pid");
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ pub fn get_spec_from_control_plane(
|
|||||||
pub fn handle_configuration(spec: &ComputeSpec, pgdata_path: &Path) -> Result<()> {
|
pub fn handle_configuration(spec: &ComputeSpec, pgdata_path: &Path) -> Result<()> {
|
||||||
// File `postgresql.conf` is no longer included into `basebackup`, so just
|
// File `postgresql.conf` is no longer included into `basebackup`, so just
|
||||||
// always write all config into it creating new file.
|
// always write all config into it creating new file.
|
||||||
config::write_postgres_conf(&pgdata_path.join("postgresql.conf"), spec)?;
|
config::write_postgres_conf(&pgdata_path.join("postgresql.conf"), spec, None)?;
|
||||||
|
|
||||||
update_pg_hba(pgdata_path)?;
|
update_pg_hba(pgdata_path)?;
|
||||||
|
|
||||||
@@ -269,17 +269,13 @@ pub fn handle_roles(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
|
|||||||
xact.execute(query.as_str(), &[])?;
|
xact.execute(query.as_str(), &[])?;
|
||||||
}
|
}
|
||||||
RoleAction::Create => {
|
RoleAction::Create => {
|
||||||
let mut query: String = format!("CREATE ROLE {} ", name.pg_quote());
|
let mut query: String = format!(
|
||||||
|
"CREATE ROLE {} CREATEROLE CREATEDB IN ROLE neon_superuser",
|
||||||
|
name.pg_quote()
|
||||||
|
);
|
||||||
info!("role create query: '{}'", &query);
|
info!("role create query: '{}'", &query);
|
||||||
query.push_str(&role.to_pg_options());
|
query.push_str(&role.to_pg_options());
|
||||||
xact.execute(query.as_str(), &[])?;
|
xact.execute(query.as_str(), &[])?;
|
||||||
|
|
||||||
let grant_query = format!(
|
|
||||||
"GRANT pg_read_all_data, pg_write_all_data TO {}",
|
|
||||||
name.pg_quote()
|
|
||||||
);
|
|
||||||
xact.execute(grant_query.as_str(), &[])?;
|
|
||||||
info!("role grant query: '{}'", &grant_query);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,6 +472,11 @@ pub fn handle_databases(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
|
|||||||
query.push_str(&db.to_pg_options());
|
query.push_str(&db.to_pg_options());
|
||||||
let _guard = info_span!("executing", query).entered();
|
let _guard = info_span!("executing", query).entered();
|
||||||
client.execute(query.as_str(), &[])?;
|
client.execute(query.as_str(), &[])?;
|
||||||
|
let grant_query: String = format!(
|
||||||
|
"GRANT ALL PRIVILEGES ON DATABASE {} TO neon_superuser",
|
||||||
|
name.pg_quote()
|
||||||
|
);
|
||||||
|
client.execute(grant_query.as_str(), &[])?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -495,35 +496,9 @@ pub fn handle_databases(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
|
|||||||
/// Grant CREATE ON DATABASE to the database owner and do some other alters and grants
|
/// Grant CREATE ON DATABASE to the database owner and do some other alters and grants
|
||||||
/// to allow users creating trusted extensions and re-creating `public` schema, for example.
|
/// to allow users creating trusted extensions and re-creating `public` schema, for example.
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub fn handle_grants(spec: &ComputeSpec, connstr: &str, client: &mut Client) -> Result<()> {
|
pub fn handle_grants(spec: &ComputeSpec, connstr: &str) -> Result<()> {
|
||||||
info!("cluster spec grants:");
|
info!("cluster spec grants:");
|
||||||
|
|
||||||
// We now have a separate `web_access` role to connect to the database
|
|
||||||
// via the web interface and proxy link auth. And also we grant a
|
|
||||||
// read / write all data privilege to every role. So also grant
|
|
||||||
// create to everyone.
|
|
||||||
// XXX: later we should stop messing with Postgres ACL in such horrible
|
|
||||||
// ways.
|
|
||||||
let roles = spec
|
|
||||||
.cluster
|
|
||||||
.roles
|
|
||||||
.iter()
|
|
||||||
.map(|r| r.name.pg_quote())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
for db in &spec.cluster.databases {
|
|
||||||
let dbname = &db.name;
|
|
||||||
|
|
||||||
let query: String = format!(
|
|
||||||
"GRANT CREATE ON DATABASE {} TO {}",
|
|
||||||
dbname.pg_quote(),
|
|
||||||
roles.join(", ")
|
|
||||||
);
|
|
||||||
info!("grant query {}", &query);
|
|
||||||
|
|
||||||
client.execute(query.as_str(), &[])?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do some per-database access adjustments. We'd better do this at db creation time,
|
// Do some per-database access adjustments. We'd better do this at db creation time,
|
||||||
// but CREATE DATABASE isn't transactional. So we cannot create db + do some grants
|
// but CREATE DATABASE isn't transactional. So we cannot create db + do some grants
|
||||||
// atomically.
|
// atomically.
|
||||||
|
|||||||
@@ -32,3 +32,4 @@ utils.workspace = true
|
|||||||
|
|
||||||
compute_api.workspace = true
|
compute_api.workspace = true
|
||||||
workspace_hack.workspace = true
|
workspace_hack.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
|||||||
@@ -180,6 +180,11 @@ pub fn stop_process(immediate: bool, process_name: &str, pid_file: &Path) -> any
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait until process is gone
|
// Wait until process is gone
|
||||||
|
wait_until_stopped(process_name, pid)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait_until_stopped(process_name: &str, pid: Pid) -> anyhow::Result<()> {
|
||||||
for retries in 0..RETRIES {
|
for retries in 0..RETRIES {
|
||||||
match process_has_stopped(pid) {
|
match process_has_stopped(pid) {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
|
|||||||
@@ -308,7 +308,8 @@ fn handle_init(init_match: &ArgMatches) -> anyhow::Result<LocalEnv> {
|
|||||||
|
|
||||||
let mut env =
|
let mut env =
|
||||||
LocalEnv::parse_config(&toml_file).context("Failed to create neon configuration")?;
|
LocalEnv::parse_config(&toml_file).context("Failed to create neon configuration")?;
|
||||||
env.init(pg_version)
|
let force = init_match.get_flag("force");
|
||||||
|
env.init(pg_version, force)
|
||||||
.context("Failed to initialize neon repository")?;
|
.context("Failed to initialize neon repository")?;
|
||||||
|
|
||||||
// Initialize pageserver, create initial tenant and timeline.
|
// Initialize pageserver, create initial tenant and timeline.
|
||||||
@@ -657,6 +658,8 @@ fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<(
|
|||||||
.get_one::<String>("endpoint_id")
|
.get_one::<String>("endpoint_id")
|
||||||
.ok_or_else(|| anyhow!("No endpoint ID was provided to start"))?;
|
.ok_or_else(|| anyhow!("No endpoint ID was provided to start"))?;
|
||||||
|
|
||||||
|
let remote_ext_config = sub_args.get_one::<String>("remote-ext-config");
|
||||||
|
|
||||||
// If --safekeepers argument is given, use only the listed safekeeper nodes.
|
// If --safekeepers argument is given, use only the listed safekeeper nodes.
|
||||||
let safekeepers =
|
let safekeepers =
|
||||||
if let Some(safekeepers_str) = sub_args.get_one::<String>("safekeepers") {
|
if let Some(safekeepers_str) = sub_args.get_one::<String>("safekeepers") {
|
||||||
@@ -698,7 +701,7 @@ fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<(
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
println!("Starting existing endpoint {endpoint_id}...");
|
println!("Starting existing endpoint {endpoint_id}...");
|
||||||
endpoint.start(&auth_token, safekeepers)?;
|
endpoint.start(&auth_token, safekeepers, remote_ext_config)?;
|
||||||
} else {
|
} else {
|
||||||
let branch_name = sub_args
|
let branch_name = sub_args
|
||||||
.get_one::<String>("branch-name")
|
.get_one::<String>("branch-name")
|
||||||
@@ -742,7 +745,7 @@ fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<(
|
|||||||
pg_version,
|
pg_version,
|
||||||
mode,
|
mode,
|
||||||
)?;
|
)?;
|
||||||
ep.start(&auth_token, safekeepers)?;
|
ep.start(&auth_token, safekeepers, remote_ext_config)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"stop" => {
|
"stop" => {
|
||||||
@@ -1002,6 +1005,12 @@ fn cli() -> Command {
|
|||||||
.help("Additional pageserver's configuration options or overrides, refer to pageserver's 'config-override' CLI parameter docs for more")
|
.help("Additional pageserver's configuration options or overrides, refer to pageserver's 'config-override' CLI parameter docs for more")
|
||||||
.required(false);
|
.required(false);
|
||||||
|
|
||||||
|
let remote_ext_config_args = Arg::new("remote-ext-config")
|
||||||
|
.long("remote-ext-config")
|
||||||
|
.num_args(1)
|
||||||
|
.help("Configure the S3 bucket that we search for extensions in.")
|
||||||
|
.required(false);
|
||||||
|
|
||||||
let lsn_arg = Arg::new("lsn")
|
let lsn_arg = Arg::new("lsn")
|
||||||
.long("lsn")
|
.long("lsn")
|
||||||
.help("Specify Lsn on the timeline to start from. By default, end of the timeline would be used.")
|
.help("Specify Lsn on the timeline to start from. By default, end of the timeline would be used.")
|
||||||
@@ -1013,6 +1022,13 @@ fn cli() -> Command {
|
|||||||
.help("If set, the node will be a hot replica on the specified timeline")
|
.help("If set, the node will be a hot replica on the specified timeline")
|
||||||
.required(false);
|
.required(false);
|
||||||
|
|
||||||
|
let force_arg = Arg::new("force")
|
||||||
|
.value_parser(value_parser!(bool))
|
||||||
|
.long("force")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.help("Force initialization even if the repository is not empty")
|
||||||
|
.required(false);
|
||||||
|
|
||||||
Command::new("Neon CLI")
|
Command::new("Neon CLI")
|
||||||
.arg_required_else_help(true)
|
.arg_required_else_help(true)
|
||||||
.version(GIT_VERSION)
|
.version(GIT_VERSION)
|
||||||
@@ -1028,6 +1044,7 @@ fn cli() -> Command {
|
|||||||
.value_name("config"),
|
.value_name("config"),
|
||||||
)
|
)
|
||||||
.arg(pg_version_arg.clone())
|
.arg(pg_version_arg.clone())
|
||||||
|
.arg(force_arg)
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("timeline")
|
Command::new("timeline")
|
||||||
@@ -1152,6 +1169,7 @@ fn cli() -> Command {
|
|||||||
.arg(pg_version_arg)
|
.arg(pg_version_arg)
|
||||||
.arg(hot_standby_arg)
|
.arg(hot_standby_arg)
|
||||||
.arg(safekeepers_arg)
|
.arg(safekeepers_arg)
|
||||||
|
.arg(remote_ext_config_args)
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("stop")
|
Command::new("stop")
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ impl Endpoint {
|
|||||||
|
|
||||||
// TODO: use future host field from safekeeper spec
|
// TODO: use future host field from safekeeper spec
|
||||||
// Pass the list of safekeepers to the replica so that it can connect to any of them,
|
// Pass the list of safekeepers to the replica so that it can connect to any of them,
|
||||||
// whichever is availiable.
|
// whichever is available.
|
||||||
let sk_ports = self
|
let sk_ports = self
|
||||||
.env
|
.env
|
||||||
.safekeepers
|
.safekeepers
|
||||||
@@ -405,10 +405,25 @@ impl Endpoint {
|
|||||||
String::from_utf8_lossy(&pg_ctl.stderr),
|
String::from_utf8_lossy(&pg_ctl.stderr),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also wait for the compute_ctl process to die. It might have some cleanup
|
||||||
|
// work to do after postgres stops, like syncing safekeepers, etc.
|
||||||
|
//
|
||||||
|
// TODO use background_process::stop_process instead
|
||||||
|
let pidfile_path = self.endpoint_path().join("compute_ctl.pid");
|
||||||
|
let pid: u32 = std::fs::read_to_string(pidfile_path)?.parse()?;
|
||||||
|
let pid = nix::unistd::Pid::from_raw(pid as i32);
|
||||||
|
crate::background_process::wait_until_stopped("compute_ctl", pid)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(&self, auth_token: &Option<String>, safekeepers: Vec<NodeId>) -> Result<()> {
|
pub fn start(
|
||||||
|
&self,
|
||||||
|
auth_token: &Option<String>,
|
||||||
|
safekeepers: Vec<NodeId>,
|
||||||
|
remote_ext_config: Option<&String>,
|
||||||
|
) -> Result<()> {
|
||||||
if self.status() == "running" {
|
if self.status() == "running" {
|
||||||
anyhow::bail!("The endpoint is already running");
|
anyhow::bail!("The endpoint is already running");
|
||||||
}
|
}
|
||||||
@@ -476,6 +491,15 @@ impl Endpoint {
|
|||||||
pageserver_connstring: Some(pageserver_connstring),
|
pageserver_connstring: Some(pageserver_connstring),
|
||||||
safekeeper_connstrings,
|
safekeeper_connstrings,
|
||||||
storage_auth_token: auth_token.clone(),
|
storage_auth_token: auth_token.clone(),
|
||||||
|
// TODO FIXME: This is a hack to test custom extensions locally.
|
||||||
|
// In test_download_extensions, we assume that the custom extension
|
||||||
|
// prefix is the tenant ID. So we set it here.
|
||||||
|
//
|
||||||
|
// The proper way to implement this is to pass the custom extension
|
||||||
|
// in spec, but we don't have a way to do that yet in the python tests.
|
||||||
|
// NEW HACK: we enable the anon custom extension for everyone! this is of course just for testing
|
||||||
|
// how will we do it for real?
|
||||||
|
custom_extensions: Some(vec!["123454321".to_string(), self.tenant_id.to_string()]),
|
||||||
};
|
};
|
||||||
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)?)?;
|
||||||
@@ -507,7 +531,18 @@ impl Endpoint {
|
|||||||
.stdin(std::process::Stdio::null())
|
.stdin(std::process::Stdio::null())
|
||||||
.stderr(logfile.try_clone()?)
|
.stderr(logfile.try_clone()?)
|
||||||
.stdout(logfile);
|
.stdout(logfile);
|
||||||
let _child = cmd.spawn()?;
|
|
||||||
|
if let Some(remote_ext_config) = remote_ext_config {
|
||||||
|
cmd.args(["--remote-ext-config", remote_ext_config]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = cmd.spawn()?;
|
||||||
|
|
||||||
|
// Write down the pid so we can wait for it when we want to stop
|
||||||
|
// TODO use background_process::start_process instead
|
||||||
|
let pid = child.id();
|
||||||
|
let pidfile_path = self.endpoint_path().join("compute_ctl.pid");
|
||||||
|
std::fs::write(pidfile_path, pid.to_string())?;
|
||||||
|
|
||||||
// Wait for it to start
|
// Wait for it to start
|
||||||
let mut attempt = 0;
|
let mut attempt = 0;
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ impl LocalEnv {
|
|||||||
//
|
//
|
||||||
// Initialize a new Neon repository
|
// Initialize a new Neon repository
|
||||||
//
|
//
|
||||||
pub fn init(&mut self, pg_version: u32) -> anyhow::Result<()> {
|
pub fn init(&mut self, pg_version: u32, force: bool) -> anyhow::Result<()> {
|
||||||
// check if config already exists
|
// check if config already exists
|
||||||
let base_path = &self.base_data_dir;
|
let base_path = &self.base_data_dir;
|
||||||
ensure!(
|
ensure!(
|
||||||
@@ -372,11 +372,29 @@ impl LocalEnv {
|
|||||||
"repository base path is missing"
|
"repository base path is missing"
|
||||||
);
|
);
|
||||||
|
|
||||||
ensure!(
|
if base_path.exists() {
|
||||||
!base_path.exists(),
|
if force {
|
||||||
"directory '{}' already exists. Perhaps already initialized?",
|
println!("removing all contents of '{}'", base_path.display());
|
||||||
base_path.display()
|
// instead of directly calling `remove_dir_all`, we keep the original dir but removing
|
||||||
);
|
// all contents inside. This helps if the developer symbol links another directory (i.e.,
|
||||||
|
// S3 local SSD) to the `.neon` base directory.
|
||||||
|
for entry in std::fs::read_dir(base_path)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
fs::remove_dir_all(&path)?;
|
||||||
|
} else {
|
||||||
|
fs::remove_file(&path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bail!(
|
||||||
|
"directory '{}' already exists. Perhaps already initialized? (Hint: use --force to remove all contents)",
|
||||||
|
base_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !self.pg_bin_dir(pg_version)?.join("postgres").exists() {
|
if !self.pg_bin_dir(pg_version)?.join("postgres").exists() {
|
||||||
bail!(
|
bail!(
|
||||||
"Can't find postgres binary at {}",
|
"Can't find postgres binary at {}",
|
||||||
@@ -392,7 +410,9 @@ impl LocalEnv {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::create_dir(base_path)?;
|
if !base_path.exists() {
|
||||||
|
fs::create_dir(base_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Generate keypair for JWT.
|
// Generate keypair for JWT.
|
||||||
//
|
//
|
||||||
|
|||||||
183
docs/rfcs/024-extension-loading.md
Normal file
183
docs/rfcs/024-extension-loading.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# Supporting custom user Extensions (Dynamic Extension Loading)
|
||||||
|
Created 2023-05-03
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
There are many extensions in the PostgreSQL ecosystem, and not all extensions
|
||||||
|
are of a quality that we can confidently support them. Additionally, our
|
||||||
|
current extension inclusion mechanism has several problems because we build all
|
||||||
|
extensions into the primary Compute image: We build the extensions every time
|
||||||
|
we build the compute image regardless of whether we actually need to rebuild
|
||||||
|
the image, and the inclusion of these extensions in the image adds a hard
|
||||||
|
dependency on all supported extensions - thus increasing the image size, and
|
||||||
|
with it the time it takes to download that image - increasing first start
|
||||||
|
latency.
|
||||||
|
|
||||||
|
This RFC proposes a dynamic loading mechanism that solves most of these
|
||||||
|
problems.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
`compute_ctl` is made responsible for loading extensions on-demand into
|
||||||
|
the container's file system for dynamically loaded extensions, and will also
|
||||||
|
make sure that the extensions in `shared_preload_libraries` are downloaded
|
||||||
|
before the compute node starts.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
compute_ctl, PostgreSQL, neon (extension), Compute Host Node, Extension Store
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Compute nodes with no extra extensions should not be negatively impacted by
|
||||||
|
the existence of support for many extensions.
|
||||||
|
|
||||||
|
Installing an extension into PostgreSQL should be easy.
|
||||||
|
|
||||||
|
Non-preloaded extensions shouldn't impact startup latency.
|
||||||
|
|
||||||
|
Uninstalled extensions shouldn't impact query latency.
|
||||||
|
|
||||||
|
A small latency penalty for dynamically loaded extensions is acceptable in
|
||||||
|
the first seconds of compute startup, but not in steady-state operations.
|
||||||
|
|
||||||
|
## Proposed implementation
|
||||||
|
|
||||||
|
### On-demand, JIT-loading of extensions
|
||||||
|
|
||||||
|
Before postgres starts we download
|
||||||
|
- control files for all extensions available to that compute node;
|
||||||
|
- all `shared_preload_libraries`;
|
||||||
|
|
||||||
|
After postgres is running, `compute_ctl` listens for requests to load files.
|
||||||
|
When PostgreSQL requests a file, `compute_ctl` downloads it.
|
||||||
|
|
||||||
|
PostgreSQL requests files in the following cases:
|
||||||
|
- When loading a preload library set in `local_preload_libraries`
|
||||||
|
- When explicitly loading a library with `LOAD`
|
||||||
|
- Wnen creating extension with `CREATE EXTENSION` (download sql scripts, (optional) extension data files and (optional) library files)))
|
||||||
|
|
||||||
|
|
||||||
|
#### Summary
|
||||||
|
|
||||||
|
Pros:
|
||||||
|
- Startup is only as slow as it takes to load all (shared_)preload_libraries
|
||||||
|
- Supports BYO Extension
|
||||||
|
|
||||||
|
Cons:
|
||||||
|
- O(sizeof(extensions)) IO requirement for loading all extensions.
|
||||||
|
|
||||||
|
### Alternative solutions
|
||||||
|
|
||||||
|
1. Allow users to add their extensions to the base image
|
||||||
|
|
||||||
|
Pros:
|
||||||
|
- Easy to deploy
|
||||||
|
|
||||||
|
Cons:
|
||||||
|
- Doesn't scale - first start size is dependent on image size;
|
||||||
|
- All extensions are shared across all users: It doesn't allow users to
|
||||||
|
bring their own restrictive-licensed extensions
|
||||||
|
|
||||||
|
2. Bring Your Own compute image
|
||||||
|
|
||||||
|
Pros:
|
||||||
|
- Still easy to deploy
|
||||||
|
- User can bring own patched version of PostgreSQL
|
||||||
|
|
||||||
|
Cons:
|
||||||
|
- First start latency is O(sizeof(extensions image))
|
||||||
|
- Warm instance pool for skipping pod schedule latency is not feasible with
|
||||||
|
O(n) custom images
|
||||||
|
- Support channels are difficult to manage
|
||||||
|
|
||||||
|
3. Download all user extensions in bulk on compute start
|
||||||
|
|
||||||
|
Pros:
|
||||||
|
- Easy to deploy
|
||||||
|
- No startup latency issues for "clean" users.
|
||||||
|
- Warm instance pool for skipping pod schedule latency is possible
|
||||||
|
|
||||||
|
Cons:
|
||||||
|
- Downloading all extensions in advance takes a lot of time, thus startup
|
||||||
|
latency issues
|
||||||
|
|
||||||
|
4. Store user's extensions in persistent storage
|
||||||
|
|
||||||
|
Pros:
|
||||||
|
- Easy to deploy
|
||||||
|
- No startup latency issues
|
||||||
|
- Warm instance pool for skipping pod schedule latency is possible
|
||||||
|
|
||||||
|
Cons:
|
||||||
|
- EC2 instances have only limited number of attachments shared between EBS
|
||||||
|
volumes, direct-attached NVMe drives, and ENIs.
|
||||||
|
- Compute instance migration isn't trivially solved for EBS mounts (e.g.
|
||||||
|
the device is unavailable whilst moving the mount between instances).
|
||||||
|
- EBS can only mount on one instance at a time (except the expensive IO2
|
||||||
|
device type).
|
||||||
|
|
||||||
|
5. Store user's extensions in network drive
|
||||||
|
|
||||||
|
Pros:
|
||||||
|
- Easy to deploy
|
||||||
|
- Few startup latency issues
|
||||||
|
- Warm instance pool for skipping pod schedule latency is possible
|
||||||
|
|
||||||
|
Cons:
|
||||||
|
- We'd need networked drives, and a lot of them, which would store many
|
||||||
|
duplicate extensions.
|
||||||
|
- **UNCHECKED:** Compute instance migration may not work nicely with
|
||||||
|
networked IOs
|
||||||
|
|
||||||
|
|
||||||
|
### Idea extensions
|
||||||
|
|
||||||
|
The extension store does not have to be S3 directly, but could be a Node-local
|
||||||
|
caching service on top of S3. This would reduce the load on the network for
|
||||||
|
popular extensions.
|
||||||
|
|
||||||
|
## Extension Storage implementation
|
||||||
|
|
||||||
|
Extension Storage in our case is an S3 bucket with a "directory" per build and postgres version,
|
||||||
|
where extension files are stored as plain files in the bucket following the same directory structure as in the postgres.
|
||||||
|
|
||||||
|
i.e.
|
||||||
|
|
||||||
|
`s3://<the-bucket>/<build-version>/<postgres-version>/lib/postgis-3.1.so`
|
||||||
|
`s3://<the-bucket>/<build-version>/<postgres-version>/share/extension/postgis.control`
|
||||||
|
`s3://<the-bucket>/<build-version>/<postgres-version>/share/extension/postgis--3.1.sql`
|
||||||
|
|
||||||
|
To handle custom extensions, that available only to specific users, we use per-extension subdirectories:
|
||||||
|
|
||||||
|
i.e.
|
||||||
|
`s3://<the-bucket>/<build-version>/<postgres-version>/<custom-ext-prefix>/lib/ext-name.so`, etc.
|
||||||
|
`s3://<the-bucket>/<build-version>/<postgres-version>/<custom-ext-prefix>/share/extension/ext-name.control`, etc.
|
||||||
|
|
||||||
|
On compute start, `compute_ctl` accepts a list of custom_ext_prefixes.
|
||||||
|
|
||||||
|
To get the list of available extensions,`compute_ctl` downloads control files from all prefixes:
|
||||||
|
|
||||||
|
`s3://<the-bucket>/<build-version>/<postgres-version>/share/extension/`
|
||||||
|
`s3://<the-bucket>/<build-version>/<postgres-version>/<custom-ext-prefix1>/share/extension/`
|
||||||
|
`s3://<the-bucket>/<build-version>/<postgres-version>/<custom-ext-prefix2>/share/extension/`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### How to add new extension to the Extension Storage?
|
||||||
|
|
||||||
|
Simply upload build artifacts to the S3 bucket.
|
||||||
|
Implement a CI step for that. Splitting it from ompute-node-image build.
|
||||||
|
|
||||||
|
### How do we deal with extension versions and updates?
|
||||||
|
|
||||||
|
Currently, we rebuild extensions on every compute-node-image build and store them in the <build-version> prefix.
|
||||||
|
This is needed to ensure that `/share` and `/lib` files are in sync.
|
||||||
|
|
||||||
|
For extension updates, we rely on the PostgreSQL extension versioning mechanism (sql update scripts) and extension authors to not break backwards compatibility within one major version of PostgreSQL.
|
||||||
|
|
||||||
|
### Alternatives
|
||||||
|
|
||||||
|
For extensions written on trusted languages we can also adopt
|
||||||
|
`dbdev` PostgreSQL Package Manager based on `pg_tle` by Supabase.
|
||||||
|
This will increase the amount supported extensions and decrease the amount of work required to support them.
|
||||||
@@ -73,6 +73,7 @@ pub struct ComputeMetrics {
|
|||||||
pub basebackup_ms: u64,
|
pub basebackup_ms: u64,
|
||||||
pub config_ms: u64,
|
pub config_ms: u64,
|
||||||
pub total_startup_ms: u64,
|
pub total_startup_ms: u64,
|
||||||
|
pub load_libraries_ms: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response of the `/computes/{compute_id}/spec` control-plane API.
|
/// Response of the `/computes/{compute_id}/spec` control-plane API.
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ pub struct ComputeSpec {
|
|||||||
/// If set, 'storage_auth_token' is used as the password to authenticate to
|
/// If set, 'storage_auth_token' is used as the password to authenticate to
|
||||||
/// the pageserver and safekeepers.
|
/// the pageserver and safekeepers.
|
||||||
pub storage_auth_token: Option<String>,
|
pub storage_auth_token: Option<String>,
|
||||||
|
|
||||||
|
// list of prefixes to search for custom extensions in remote extension storage
|
||||||
|
pub custom_extensions: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use prometheus::{Registry, Result};
|
|||||||
pub mod launch_timestamp;
|
pub mod launch_timestamp;
|
||||||
mod wrappers;
|
mod wrappers;
|
||||||
pub use wrappers::{CountedReader, CountedWriter};
|
pub use wrappers::{CountedReader, CountedWriter};
|
||||||
|
pub mod metric_vec_duration;
|
||||||
|
|
||||||
pub type UIntGauge = GenericGauge<AtomicU64>;
|
pub type UIntGauge = GenericGauge<AtomicU64>;
|
||||||
pub type UIntGaugeVec = GenericGaugeVec<AtomicU64>;
|
pub type UIntGaugeVec = GenericGaugeVec<AtomicU64>;
|
||||||
|
|||||||
23
libs/metrics/src/metric_vec_duration.rs
Normal file
23
libs/metrics/src/metric_vec_duration.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//! Helpers for observing duration on HistogramVec / CounterVec / GaugeVec / MetricVec<T>.
|
||||||
|
|
||||||
|
use std::{future::Future, time::Instant};
|
||||||
|
|
||||||
|
pub trait DurationResultObserver {
|
||||||
|
fn observe_result<T, E>(&self, res: &Result<T, E>, duration: std::time::Duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn observe_async_block_duration_by_result<
|
||||||
|
T,
|
||||||
|
E,
|
||||||
|
F: Future<Output = Result<T, E>>,
|
||||||
|
O: DurationResultObserver,
|
||||||
|
>(
|
||||||
|
observer: &O,
|
||||||
|
block: F,
|
||||||
|
) -> Result<T, E> {
|
||||||
|
let start = Instant::now();
|
||||||
|
let result = block.await;
|
||||||
|
let duration = start.elapsed();
|
||||||
|
observer.observe_result(&result, duration);
|
||||||
|
result
|
||||||
|
}
|
||||||
@@ -184,6 +184,20 @@ pub enum GenericRemoteStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GenericRemoteStorage {
|
impl GenericRemoteStorage {
|
||||||
|
// A function for listing all the files in a "directory"
|
||||||
|
// Example:
|
||||||
|
// list_files("foo/bar") = ["foo/bar/a.txt", "foo/bar/b.txt"]
|
||||||
|
pub async fn list_files(&self, folder: Option<&RemotePath>) -> anyhow::Result<Vec<RemotePath>> {
|
||||||
|
match self {
|
||||||
|
Self::LocalFs(s) => s.list_files(folder).await,
|
||||||
|
Self::AwsS3(s) => s.list_files(folder).await,
|
||||||
|
Self::Unreliable(s) => s.list_files(folder).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lists common *prefixes*, if any of files
|
||||||
|
// Example:
|
||||||
|
// list_prefixes("foo123","foo567","bar123","bar432") = ["foo", "bar"]
|
||||||
pub async fn list_prefixes(
|
pub async fn list_prefixes(
|
||||||
&self,
|
&self,
|
||||||
prefix: Option<&RemotePath>,
|
prefix: Option<&RemotePath>,
|
||||||
@@ -195,14 +209,6 @@ impl GenericRemoteStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_files(&self, folder: Option<&RemotePath>) -> anyhow::Result<Vec<RemotePath>> {
|
|
||||||
match self {
|
|
||||||
Self::LocalFs(s) => s.list_files(folder).await,
|
|
||||||
Self::AwsS3(s) => s.list_files(folder).await,
|
|
||||||
Self::Unreliable(s) => s.list_files(folder).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn upload(
|
pub async fn upload(
|
||||||
&self,
|
&self,
|
||||||
from: impl io::AsyncRead + Unpin + Send + Sync + 'static,
|
from: impl io::AsyncRead + Unpin + Send + Sync + 'static,
|
||||||
|
|||||||
@@ -349,10 +349,17 @@ impl RemoteStorage for S3Bucket {
|
|||||||
|
|
||||||
/// See the doc for `RemoteStorage::list_files`
|
/// See the doc for `RemoteStorage::list_files`
|
||||||
async fn list_files(&self, folder: Option<&RemotePath>) -> anyhow::Result<Vec<RemotePath>> {
|
async fn list_files(&self, folder: Option<&RemotePath>) -> anyhow::Result<Vec<RemotePath>> {
|
||||||
let folder_name = folder
|
let mut folder_name = folder
|
||||||
.map(|p| self.relative_path_to_s3_object(p))
|
.map(|p| self.relative_path_to_s3_object(p))
|
||||||
.or_else(|| self.prefix_in_bucket.clone());
|
.or_else(|| self.prefix_in_bucket.clone());
|
||||||
|
|
||||||
|
// remove leading "/" if one exists
|
||||||
|
if let Some(folder_name_slash) = folder_name.clone() {
|
||||||
|
if folder_name_slash.starts_with(REMOTE_STORAGE_PREFIX_SEPARATOR) {
|
||||||
|
folder_name = Some(folder_name_slash[1..].to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// AWS may need to break the response into several parts
|
// AWS may need to break the response into several parts
|
||||||
let mut continuation_token = None;
|
let mut continuation_token = None;
|
||||||
let mut all_files = vec![];
|
let mut all_files = vec![];
|
||||||
|
|||||||
@@ -173,10 +173,15 @@ async fn s3_delete_objects_works(ctx: &mut MaybeEnabledS3) -> anyhow::Result<()>
|
|||||||
let path2 = RemotePath::new(&PathBuf::from(format!("{}/path2", ctx.base_prefix,)))
|
let path2 = RemotePath::new(&PathBuf::from(format!("{}/path2", ctx.base_prefix,)))
|
||||||
.with_context(|| "RemotePath conversion")?;
|
.with_context(|| "RemotePath conversion")?;
|
||||||
|
|
||||||
|
let path3 = RemotePath::new(&PathBuf::from(format!("{}/path3", ctx.base_prefix,)))
|
||||||
|
.with_context(|| "RemotePath conversion")?;
|
||||||
|
|
||||||
let data1 = "remote blob data1".as_bytes();
|
let data1 = "remote blob data1".as_bytes();
|
||||||
let data1_len = data1.len();
|
let data1_len = data1.len();
|
||||||
let data2 = "remote blob data2".as_bytes();
|
let data2 = "remote blob data2".as_bytes();
|
||||||
let data2_len = data2.len();
|
let data2_len = data2.len();
|
||||||
|
let data3 = "remote blob data3".as_bytes();
|
||||||
|
let data3_len = data3.len();
|
||||||
ctx.client
|
ctx.client
|
||||||
.upload(std::io::Cursor::new(data1), data1_len, &path1, None)
|
.upload(std::io::Cursor::new(data1), data1_len, &path1, None)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -185,8 +190,18 @@ async fn s3_delete_objects_works(ctx: &mut MaybeEnabledS3) -> anyhow::Result<()>
|
|||||||
.upload(std::io::Cursor::new(data2), data2_len, &path2, None)
|
.upload(std::io::Cursor::new(data2), data2_len, &path2, None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
ctx.client
|
||||||
|
.upload(std::io::Cursor::new(data3), data3_len, &path3, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
ctx.client.delete_objects(&[path1, path2]).await?;
|
ctx.client.delete_objects(&[path1, path2]).await?;
|
||||||
|
|
||||||
|
let prefixes = ctx.client.list_prefixes(None).await?;
|
||||||
|
|
||||||
|
assert_eq!(prefixes.len(), 1);
|
||||||
|
|
||||||
|
ctx.client.delete_objects(&[path3]).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ edition.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
atty.workspace = true
|
|
||||||
sentry.workspace = true
|
sentry.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ pub fn init(
|
|||||||
let r = r.with({
|
let r = r.with({
|
||||||
let log_layer = tracing_subscriber::fmt::layer()
|
let log_layer = tracing_subscriber::fmt::layer()
|
||||||
.with_target(false)
|
.with_target(false)
|
||||||
.with_ansi(atty::is(atty::Stream::Stdout))
|
.with_ansi(false)
|
||||||
.with_writer(std::io::stdout);
|
.with_writer(std::io::stdout);
|
||||||
let log_layer = match log_format {
|
let log_layer = match log_format {
|
||||||
LogFormat::Json => log_layer.json().boxed(),
|
LogFormat::Json => log_layer.json().boxed(),
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
use pageserver::keyspace::{KeyPartitioning, KeySpace};
|
use pageserver::keyspace::{KeyPartitioning, KeySpace};
|
||||||
use pageserver::repository::Key;
|
use pageserver::repository::Key;
|
||||||
use pageserver::tenant::layer_map::LayerMap;
|
use pageserver::tenant::layer_map::LayerMap;
|
||||||
use pageserver::tenant::storage_layer::{Layer, LayerDescriptor, LayerFileName};
|
use pageserver::tenant::storage_layer::{tests::LayerDescriptor, Layer, LayerFileName};
|
||||||
|
use pageserver::tenant::storage_layer::{PersistentLayer, PersistentLayerDesc};
|
||||||
use rand::prelude::{SeedableRng, SliceRandom, StdRng};
|
use rand::prelude::{SeedableRng, SliceRandom, StdRng};
|
||||||
use std::cmp::{max, min};
|
use std::cmp::{max, min};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
use utils::id::{TenantId, TimelineId};
|
||||||
|
|
||||||
use utils::lsn::Lsn;
|
use utils::lsn::Lsn;
|
||||||
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
fn build_layer_map(filename_dump: PathBuf) -> LayerMap<LayerDescriptor> {
|
fn build_layer_map(filename_dump: PathBuf) -> LayerMap {
|
||||||
let mut layer_map = LayerMap::<LayerDescriptor>::default();
|
let mut layer_map = LayerMap::default();
|
||||||
|
|
||||||
let mut min_lsn = Lsn(u64::MAX);
|
let mut min_lsn = Lsn(u64::MAX);
|
||||||
let mut max_lsn = Lsn(0);
|
let mut max_lsn = Lsn(0);
|
||||||
@@ -33,7 +34,7 @@ fn build_layer_map(filename_dump: PathBuf) -> LayerMap<LayerDescriptor> {
|
|||||||
min_lsn = min(min_lsn, lsn_range.start);
|
min_lsn = min(min_lsn, lsn_range.start);
|
||||||
max_lsn = max(max_lsn, Lsn(lsn_range.end.0 - 1));
|
max_lsn = max(max_lsn, Lsn(lsn_range.end.0 - 1));
|
||||||
|
|
||||||
updates.insert_historic(layer.get_persistent_layer_desc(), Arc::new(layer));
|
updates.insert_historic(layer.layer_desc().clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("min: {min_lsn}, max: {max_lsn}");
|
println!("min: {min_lsn}, max: {max_lsn}");
|
||||||
@@ -43,7 +44,7 @@ fn build_layer_map(filename_dump: PathBuf) -> LayerMap<LayerDescriptor> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a layer map query pattern for benchmarks
|
/// Construct a layer map query pattern for benchmarks
|
||||||
fn uniform_query_pattern(layer_map: &LayerMap<LayerDescriptor>) -> Vec<(Key, Lsn)> {
|
fn uniform_query_pattern(layer_map: &LayerMap) -> Vec<(Key, Lsn)> {
|
||||||
// For each image layer we query one of the pages contained, at LSN right
|
// For each image layer we query one of the pages contained, at LSN right
|
||||||
// before the image layer was created. This gives us a somewhat uniform
|
// before the image layer was created. This gives us a somewhat uniform
|
||||||
// coverage of both the lsn and key space because image layers have
|
// coverage of both the lsn and key space because image layers have
|
||||||
@@ -69,7 +70,7 @@ fn uniform_query_pattern(layer_map: &LayerMap<LayerDescriptor>) -> Vec<(Key, Lsn
|
|||||||
|
|
||||||
// Construct a partitioning for testing get_difficulty map when we
|
// Construct a partitioning for testing get_difficulty map when we
|
||||||
// don't have an exact result of `collect_keyspace` to work with.
|
// don't have an exact result of `collect_keyspace` to work with.
|
||||||
fn uniform_key_partitioning(layer_map: &LayerMap<LayerDescriptor>, _lsn: Lsn) -> KeyPartitioning {
|
fn uniform_key_partitioning(layer_map: &LayerMap, _lsn: Lsn) -> KeyPartitioning {
|
||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
|
|
||||||
// We add a partition boundary at the start of each image layer,
|
// We add a partition boundary at the start of each image layer,
|
||||||
@@ -209,13 +210,15 @@ fn bench_sequential(c: &mut Criterion) {
|
|||||||
for i in 0..100_000 {
|
for i in 0..100_000 {
|
||||||
let i32 = (i as u32) % 100;
|
let i32 = (i as u32) % 100;
|
||||||
let zero = Key::from_hex("000000000000000000000000000000000000").unwrap();
|
let zero = Key::from_hex("000000000000000000000000000000000000").unwrap();
|
||||||
let layer = LayerDescriptor {
|
let layer = LayerDescriptor::from(PersistentLayerDesc::new_img(
|
||||||
key: zero.add(10 * i32)..zero.add(10 * i32 + 1),
|
TenantId::generate(),
|
||||||
lsn: Lsn(i)..Lsn(i + 1),
|
TimelineId::generate(),
|
||||||
is_incremental: false,
|
zero.add(10 * i32)..zero.add(10 * i32 + 1),
|
||||||
short_id: format!("Layer {}", i),
|
Lsn(i),
|
||||||
};
|
false,
|
||||||
updates.insert_historic(layer.get_persistent_layer_desc(), Arc::new(layer));
|
0,
|
||||||
|
));
|
||||||
|
updates.insert_historic(layer.layer_desc().clone());
|
||||||
}
|
}
|
||||||
updates.flush();
|
updates.flush();
|
||||||
println!("Finished layer map init in {:?}", now.elapsed());
|
println!("Finished layer map init in {:?}", now.elapsed());
|
||||||
|
|||||||
@@ -495,50 +495,50 @@ fn start_pageserver(
|
|||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(metric_collection_endpoint) = &conf.metric_collection_endpoint {
|
if let Some(metric_collection_endpoint) = &conf.metric_collection_endpoint {
|
||||||
let background_jobs_barrier = background_jobs_barrier;
|
let background_jobs_barrier = background_jobs_barrier;
|
||||||
let metrics_ctx = RequestContext::todo_child(
|
let metrics_ctx = RequestContext::todo_child(
|
||||||
TaskKind::MetricsCollection,
|
TaskKind::MetricsCollection,
|
||||||
// This task itself shouldn't download anything.
|
// This task itself shouldn't download anything.
|
||||||
// The actual size calculation does need downloads, and
|
// The actual size calculation does need downloads, and
|
||||||
// creates a child context with the right DownloadBehavior.
|
// creates a child context with the right DownloadBehavior.
|
||||||
DownloadBehavior::Error,
|
DownloadBehavior::Error,
|
||||||
);
|
);
|
||||||
task_mgr::spawn(
|
task_mgr::spawn(
|
||||||
MGMT_REQUEST_RUNTIME.handle(),
|
crate::BACKGROUND_RUNTIME.handle(),
|
||||||
TaskKind::MetricsCollection,
|
TaskKind::MetricsCollection,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
"consumption metrics collection",
|
"consumption metrics collection",
|
||||||
true,
|
true,
|
||||||
async move {
|
async move {
|
||||||
// first wait until background jobs are cleared to launch.
|
// first wait until background jobs are cleared to launch.
|
||||||
//
|
//
|
||||||
// this is because we only process active tenants and timelines, and the
|
// this is because we only process active tenants and timelines, and the
|
||||||
// Timeline::get_current_logical_size will spawn the logical size calculation,
|
// Timeline::get_current_logical_size will spawn the logical size calculation,
|
||||||
// which will not be rate-limited.
|
// which will not be rate-limited.
|
||||||
let cancel = task_mgr::shutdown_token();
|
let cancel = task_mgr::shutdown_token();
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = cancel.cancelled() => { return Ok(()); },
|
_ = cancel.cancelled() => { return Ok(()); },
|
||||||
_ = background_jobs_barrier.wait() => {}
|
_ = background_jobs_barrier.wait() => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
pageserver::consumption_metrics::collect_metrics(
|
pageserver::consumption_metrics::collect_metrics(
|
||||||
metric_collection_endpoint,
|
metric_collection_endpoint,
|
||||||
conf.metric_collection_interval,
|
conf.metric_collection_interval,
|
||||||
conf.cached_metric_collection_interval,
|
conf.cached_metric_collection_interval,
|
||||||
conf.synthetic_size_calculation_interval,
|
conf.synthetic_size_calculation_interval,
|
||||||
conf.id,
|
conf.id,
|
||||||
metrics_ctx,
|
metrics_ctx,
|
||||||
)
|
)
|
||||||
.instrument(info_span!("metrics_collection"))
|
.instrument(info_span!("metrics_collection"))
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn a task to listen for libpq connections. It will spawn further tasks
|
// Spawn a task to listen for libpq connections. It will spawn further tasks
|
||||||
|
|||||||
@@ -96,12 +96,12 @@ pub mod defaults {
|
|||||||
|
|
||||||
#background_task_maximum_delay = '{DEFAULT_BACKGROUND_TASK_MAXIMUM_DELAY}'
|
#background_task_maximum_delay = '{DEFAULT_BACKGROUND_TASK_MAXIMUM_DELAY}'
|
||||||
|
|
||||||
# [tenant_config]
|
[tenant_config]
|
||||||
#checkpoint_distance = {DEFAULT_CHECKPOINT_DISTANCE} # in bytes
|
#checkpoint_distance = {DEFAULT_CHECKPOINT_DISTANCE} # in bytes
|
||||||
#checkpoint_timeout = {DEFAULT_CHECKPOINT_TIMEOUT}
|
#checkpoint_timeout = {DEFAULT_CHECKPOINT_TIMEOUT}
|
||||||
#compaction_target_size = {DEFAULT_COMPACTION_TARGET_SIZE} # in bytes
|
#compaction_target_size = {DEFAULT_COMPACTION_TARGET_SIZE} # in bytes
|
||||||
#compaction_period = '{DEFAULT_COMPACTION_PERIOD}'
|
#compaction_period = '{DEFAULT_COMPACTION_PERIOD}'
|
||||||
#compaction_threshold = '{DEFAULT_COMPACTION_THRESHOLD}'
|
#compaction_threshold = {DEFAULT_COMPACTION_THRESHOLD}
|
||||||
|
|
||||||
#gc_period = '{DEFAULT_GC_PERIOD}'
|
#gc_period = '{DEFAULT_GC_PERIOD}'
|
||||||
#gc_horizon = {DEFAULT_GC_HORIZON}
|
#gc_horizon = {DEFAULT_GC_HORIZON}
|
||||||
@@ -111,7 +111,8 @@ pub mod defaults {
|
|||||||
#min_resident_size_override = .. # in bytes
|
#min_resident_size_override = .. # in bytes
|
||||||
#evictions_low_residence_duration_metric_threshold = '{DEFAULT_EVICTIONS_LOW_RESIDENCE_DURATION_METRIC_THRESHOLD}'
|
#evictions_low_residence_duration_metric_threshold = '{DEFAULT_EVICTIONS_LOW_RESIDENCE_DURATION_METRIC_THRESHOLD}'
|
||||||
#gc_feedback = false
|
#gc_feedback = false
|
||||||
# [remote_storage]
|
|
||||||
|
[remote_storage]
|
||||||
|
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ const RESIDENT_SIZE: &str = "resident_size";
|
|||||||
const REMOTE_STORAGE_SIZE: &str = "remote_storage_size";
|
const REMOTE_STORAGE_SIZE: &str = "remote_storage_size";
|
||||||
const TIMELINE_LOGICAL_SIZE: &str = "timeline_logical_size";
|
const TIMELINE_LOGICAL_SIZE: &str = "timeline_logical_size";
|
||||||
|
|
||||||
|
const DEFAULT_HTTP_REPORTING_TIMEOUT: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
struct Ids {
|
struct Ids {
|
||||||
@@ -73,7 +75,10 @@ pub async fn collect_metrics(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// define client here to reuse it for all requests
|
// define client here to reuse it for all requests
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::ClientBuilder::new()
|
||||||
|
.timeout(DEFAULT_HTTP_REPORTING_TIMEOUT)
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create http client with timeout");
|
||||||
let mut cached_metrics: HashMap<PageserverConsumptionMetricsKey, u64> = HashMap::new();
|
let mut cached_metrics: HashMap<PageserverConsumptionMetricsKey, u64> = HashMap::new();
|
||||||
let mut prev_iteration_time: std::time::Instant = std::time::Instant::now();
|
let mut prev_iteration_time: std::time::Instant = std::time::Instant::now();
|
||||||
|
|
||||||
@@ -83,7 +88,7 @@ pub async fn collect_metrics(
|
|||||||
info!("collect_metrics received cancellation request");
|
info!("collect_metrics received cancellation request");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
},
|
},
|
||||||
_ = ticker.tick() => {
|
tick_at = ticker.tick() => {
|
||||||
|
|
||||||
// send cached metrics every cached_metric_collection_interval
|
// send cached metrics every cached_metric_collection_interval
|
||||||
let send_cached = prev_iteration_time.elapsed() >= cached_metric_collection_interval;
|
let send_cached = prev_iteration_time.elapsed() >= cached_metric_collection_interval;
|
||||||
@@ -93,6 +98,12 @@ pub async fn collect_metrics(
|
|||||||
}
|
}
|
||||||
|
|
||||||
collect_metrics_iteration(&client, &mut cached_metrics, metric_collection_endpoint, node_id, &ctx, send_cached).await;
|
collect_metrics_iteration(&client, &mut cached_metrics, metric_collection_endpoint, node_id, &ctx, send_cached).await;
|
||||||
|
|
||||||
|
crate::tenant::tasks::warn_when_period_overrun(
|
||||||
|
tick_at.elapsed(),
|
||||||
|
metric_collection_interval,
|
||||||
|
"consumption_metrics_collect_metrics",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,31 +284,42 @@ pub async fn collect_metrics_iteration(
|
|||||||
})
|
})
|
||||||
.expect("PageserverConsumptionMetric should not fail serialization");
|
.expect("PageserverConsumptionMetric should not fail serialization");
|
||||||
|
|
||||||
let res = client
|
const MAX_RETRIES: u32 = 3;
|
||||||
.post(metric_collection_endpoint.clone())
|
|
||||||
.json(&chunk_json)
|
|
||||||
.send()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match res {
|
for attempt in 0..MAX_RETRIES {
|
||||||
Ok(res) => {
|
let res = client
|
||||||
if res.status().is_success() {
|
.post(metric_collection_endpoint.clone())
|
||||||
// update cached metrics after they were sent successfully
|
.json(&chunk_json)
|
||||||
for (curr_key, curr_val) in chunk.iter() {
|
.send()
|
||||||
cached_metrics.insert(curr_key.clone(), *curr_val);
|
.await;
|
||||||
}
|
|
||||||
} else {
|
match res {
|
||||||
error!("metrics endpoint refused the sent metrics: {:?}", res);
|
Ok(res) => {
|
||||||
for metric in chunk_to_send.iter() {
|
if res.status().is_success() {
|
||||||
// Report if the metric value is suspiciously large
|
// update cached metrics after they were sent successfully
|
||||||
if metric.value > (1u64 << 40) {
|
for (curr_key, curr_val) in chunk.iter() {
|
||||||
|
cached_metrics.insert(curr_key.clone(), *curr_val);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("metrics endpoint refused the sent metrics: {:?}", res);
|
||||||
|
for metric in chunk_to_send
|
||||||
|
.iter()
|
||||||
|
.filter(|metric| metric.value > (1u64 << 40))
|
||||||
|
{
|
||||||
|
// Report if the metric value is suspiciously large
|
||||||
error!("potentially abnormal metric value: {:?}", metric);
|
error!("potentially abnormal metric value: {:?}", metric);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(err) if err.is_timeout() => {
|
||||||
|
error!(attempt, "timeout sending metrics, retrying immediately");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!(attempt, ?err, "failed to send metrics");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("failed to send metrics: {:?}", err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,7 +339,7 @@ pub async fn calculate_synthetic_size_worker(
|
|||||||
_ = task_mgr::shutdown_watcher() => {
|
_ = task_mgr::shutdown_watcher() => {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
},
|
},
|
||||||
_ = ticker.tick() => {
|
tick_at = ticker.tick() => {
|
||||||
|
|
||||||
let tenants = match mgr::list_tenants().await {
|
let tenants = match mgr::list_tenants().await {
|
||||||
Ok(tenants) => tenants,
|
Ok(tenants) => tenants,
|
||||||
@@ -343,6 +365,12 @@ pub async fn calculate_synthetic_size_worker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crate::tenant::tasks::warn_when_period_overrun(
|
||||||
|
tick_at.elapsed(),
|
||||||
|
synthetic_size_calculation_interval,
|
||||||
|
"consumption_metrics_synthetic_size_worker",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,10 +186,8 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
delete:
|
delete:
|
||||||
description: "Attempts to delete specified timeline. On 500 errors should be retried"
|
description: "Attempts to delete specified timeline. 500 and 409 errors should be retried"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
|
||||||
description: Ok
|
|
||||||
"400":
|
"400":
|
||||||
description: Error when no tenant id found in path or no timeline id
|
description: Error when no tenant id found in path or no timeline id
|
||||||
content:
|
content:
|
||||||
@@ -214,6 +212,12 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/NotFoundError"
|
$ref: "#/components/schemas/NotFoundError"
|
||||||
|
"409":
|
||||||
|
description: Deletion is already in progress, continue polling
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ConflictError"
|
||||||
"412":
|
"412":
|
||||||
description: Tenant is missing, or timeline has children
|
description: Tenant is missing, or timeline has children
|
||||||
content:
|
content:
|
||||||
@@ -718,6 +722,12 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ForbiddenError"
|
$ref: "#/components/schemas/ForbiddenError"
|
||||||
|
"406":
|
||||||
|
description: Permanently unsatisfiable request, don't retry.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
"409":
|
"409":
|
||||||
description: Timeline already exists, creation skipped
|
description: Timeline already exists, creation skipped
|
||||||
content:
|
content:
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ use super::models::{
|
|||||||
TimelineCreateRequest, TimelineGcRequest, TimelineInfo,
|
TimelineCreateRequest, TimelineGcRequest, TimelineInfo,
|
||||||
};
|
};
|
||||||
use crate::context::{DownloadBehavior, RequestContext};
|
use crate::context::{DownloadBehavior, RequestContext};
|
||||||
use crate::disk_usage_eviction_task;
|
|
||||||
use crate::metrics::{StorageTimeOperation, STORAGE_TIME_GLOBAL};
|
use crate::metrics::{StorageTimeOperation, STORAGE_TIME_GLOBAL};
|
||||||
use crate::pgdatadir_mapping::LsnForTimestamp;
|
use crate::pgdatadir_mapping::LsnForTimestamp;
|
||||||
use crate::task_mgr::TaskKind;
|
use crate::task_mgr::TaskKind;
|
||||||
@@ -35,6 +34,7 @@ use crate::tenant::size::ModelInputs;
|
|||||||
use crate::tenant::storage_layer::LayerAccessStatsReset;
|
use crate::tenant::storage_layer::LayerAccessStatsReset;
|
||||||
use crate::tenant::{LogicalSizeCalculationCause, PageReconstructError, Timeline};
|
use crate::tenant::{LogicalSizeCalculationCause, PageReconstructError, Timeline};
|
||||||
use crate::{config::PageServerConf, tenant::mgr};
|
use crate::{config::PageServerConf, tenant::mgr};
|
||||||
|
use crate::{disk_usage_eviction_task, tenant};
|
||||||
use utils::{
|
use utils::{
|
||||||
auth::JwtAuth,
|
auth::JwtAuth,
|
||||||
http::{
|
http::{
|
||||||
@@ -187,6 +187,7 @@ impl From<crate::tenant::DeleteTimelineError> for ApiError {
|
|||||||
format!("Cannot delete timeline which has child timelines: {children:?}")
|
format!("Cannot delete timeline which has child timelines: {children:?}")
|
||||||
.into_boxed_str(),
|
.into_boxed_str(),
|
||||||
),
|
),
|
||||||
|
a @ AlreadyInProgress => ApiError::Conflict(a.to_string()),
|
||||||
Other(e) => ApiError::InternalServerError(e),
|
Other(e) => ApiError::InternalServerError(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,15 +328,22 @@ async fn timeline_create_handler(
|
|||||||
&ctx,
|
&ctx,
|
||||||
)
|
)
|
||||||
.await {
|
.await {
|
||||||
Ok(Some(new_timeline)) => {
|
Ok(new_timeline) => {
|
||||||
// Created. Construct a TimelineInfo for it.
|
// Created. Construct a TimelineInfo for it.
|
||||||
let timeline_info = build_timeline_info_common(&new_timeline, &ctx)
|
let timeline_info = build_timeline_info_common(&new_timeline, &ctx)
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::InternalServerError)?;
|
.map_err(ApiError::InternalServerError)?;
|
||||||
json_response(StatusCode::CREATED, timeline_info)
|
json_response(StatusCode::CREATED, timeline_info)
|
||||||
}
|
}
|
||||||
Ok(None) => json_response(StatusCode::CONFLICT, ()), // timeline already exists
|
Err(tenant::CreateTimelineError::AlreadyExists) => {
|
||||||
Err(err) => Err(ApiError::InternalServerError(err)),
|
json_response(StatusCode::CONFLICT, ())
|
||||||
|
}
|
||||||
|
Err(tenant::CreateTimelineError::AncestorLsn(err)) => {
|
||||||
|
json_response(StatusCode::NOT_ACCEPTABLE, HttpErrorBody::from_msg(
|
||||||
|
format!("{err:#}")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(tenant::CreateTimelineError::Other(err)) => Err(ApiError::InternalServerError(err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.instrument(info_span!("timeline_create", tenant = %tenant_id, timeline_id = %new_timeline_id, lsn=?request_data.ancestor_start_lsn, pg_version=?request_data.pg_version))
|
.instrument(info_span!("timeline_create", tenant = %tenant_id, timeline_id = %new_timeline_id, lsn=?request_data.ancestor_start_lsn, pg_version=?request_data.pg_version))
|
||||||
@@ -1128,8 +1136,6 @@ async fn disk_usage_eviction_run(
|
|||||||
freed_bytes: 0,
|
freed_bytes: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::task_mgr::MGMT_REQUEST_RUNTIME;
|
|
||||||
|
|
||||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
|
|
||||||
let state = get_state(&r);
|
let state = get_state(&r);
|
||||||
@@ -1147,7 +1153,7 @@ async fn disk_usage_eviction_run(
|
|||||||
let _g = cancel.drop_guard();
|
let _g = cancel.drop_guard();
|
||||||
|
|
||||||
crate::task_mgr::spawn(
|
crate::task_mgr::spawn(
|
||||||
MGMT_REQUEST_RUNTIME.handle(),
|
crate::task_mgr::BACKGROUND_RUNTIME.handle(),
|
||||||
TaskKind::DiskUsageEviction,
|
TaskKind::DiskUsageEviction,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use metrics::metric_vec_duration::DurationResultObserver;
|
||||||
use metrics::{
|
use metrics::{
|
||||||
register_counter_vec, register_histogram, register_histogram_vec, register_int_counter,
|
register_counter_vec, register_histogram, register_histogram_vec, register_int_counter,
|
||||||
register_int_counter_vec, register_int_gauge, register_int_gauge_vec, register_uint_gauge_vec,
|
register_int_counter_vec, register_int_gauge, register_int_gauge_vec, register_uint_gauge_vec,
|
||||||
@@ -203,11 +204,11 @@ pub static TENANT_STATE_METRIC: Lazy<UIntGaugeVec> = Lazy::new(|| {
|
|||||||
|
|
||||||
pub static TENANT_SYNTHETIC_SIZE_METRIC: Lazy<UIntGaugeVec> = Lazy::new(|| {
|
pub static TENANT_SYNTHETIC_SIZE_METRIC: Lazy<UIntGaugeVec> = Lazy::new(|| {
|
||||||
register_uint_gauge_vec!(
|
register_uint_gauge_vec!(
|
||||||
"pageserver_tenant_synthetic_size",
|
"pageserver_tenant_synthetic_cached_size_bytes",
|
||||||
"Synthetic size of each tenant",
|
"Synthetic size of each tenant in bytes",
|
||||||
&["tenant_id"]
|
&["tenant_id"]
|
||||||
)
|
)
|
||||||
.expect("Failed to register pageserver_tenant_synthetic_size metric")
|
.expect("Failed to register pageserver_tenant_synthetic_cached_size_bytes metric")
|
||||||
});
|
});
|
||||||
|
|
||||||
// Metrics for cloud upload. These metrics reflect data uploaded to cloud storage,
|
// Metrics for cloud upload. These metrics reflect data uploaded to cloud storage,
|
||||||
@@ -424,6 +425,27 @@ pub static SMGR_QUERY_TIME: Lazy<HistogramVec> = Lazy::new(|| {
|
|||||||
.expect("failed to define a metric")
|
.expect("failed to define a metric")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub struct BasebackupQueryTime(HistogramVec);
|
||||||
|
pub static BASEBACKUP_QUERY_TIME: Lazy<BasebackupQueryTime> = Lazy::new(|| {
|
||||||
|
BasebackupQueryTime({
|
||||||
|
register_histogram_vec!(
|
||||||
|
"pageserver_basebackup_query_seconds",
|
||||||
|
"Histogram of basebackup queries durations, by result type",
|
||||||
|
&["result"],
|
||||||
|
CRITICAL_OP_BUCKETS.into(),
|
||||||
|
)
|
||||||
|
.expect("failed to define a metric")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
impl DurationResultObserver for BasebackupQueryTime {
|
||||||
|
fn observe_result<T, E>(&self, res: &Result<T, E>, duration: std::time::Duration) {
|
||||||
|
let label_value = if res.is_ok() { "ok" } else { "error" };
|
||||||
|
let metric = self.0.get_metric_with_label_values(&[label_value]).unwrap();
|
||||||
|
metric.observe(duration.as_secs_f64());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub static LIVE_CONNECTIONS_COUNT: Lazy<IntGaugeVec> = Lazy::new(|| {
|
pub static LIVE_CONNECTIONS_COUNT: Lazy<IntGaugeVec> = Lazy::new(|| {
|
||||||
register_int_gauge_vec!(
|
register_int_gauge_vec!(
|
||||||
"pageserver_live_connections",
|
"pageserver_live_connections",
|
||||||
@@ -823,11 +845,6 @@ impl TimelineMetrics {
|
|||||||
let evictions_with_low_residence_duration =
|
let evictions_with_low_residence_duration =
|
||||||
evictions_with_low_residence_duration_builder.build(&tenant_id, &timeline_id);
|
evictions_with_low_residence_duration_builder.build(&tenant_id, &timeline_id);
|
||||||
|
|
||||||
// TODO(chi): remove this once we remove Lazy for all metrics. Otherwise this will not appear in the exporter
|
|
||||||
// and integration test will error.
|
|
||||||
MATERIALIZED_PAGE_CACHE_HIT_DIRECT.get();
|
|
||||||
MATERIALIZED_PAGE_CACHE_HIT.get();
|
|
||||||
|
|
||||||
TimelineMetrics {
|
TimelineMetrics {
|
||||||
tenant_id,
|
tenant_id,
|
||||||
timeline_id,
|
timeline_id,
|
||||||
@@ -1302,4 +1319,8 @@ pub fn preinitialize_metrics() {
|
|||||||
|
|
||||||
// Same as above for this metric, but, it's a Vec-type metric for which we don't know all the labels.
|
// Same as above for this metric, but, it's a Vec-type metric for which we don't know all the labels.
|
||||||
BACKGROUND_LOOP_PERIOD_OVERRUN_COUNT.reset();
|
BACKGROUND_LOOP_PERIOD_OVERRUN_COUNT.reset();
|
||||||
|
|
||||||
|
// Python tests need these.
|
||||||
|
MATERIALIZED_PAGE_CACHE_HIT_DIRECT.get();
|
||||||
|
MATERIALIZED_PAGE_CACHE_HIT.get();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -904,7 +904,7 @@ where
|
|||||||
|
|
||||||
self.check_permission(Some(tenant_id))?;
|
self.check_permission(Some(tenant_id))?;
|
||||||
|
|
||||||
let lsn = if params.len() == 3 {
|
let lsn = if params.len() >= 3 {
|
||||||
Some(
|
Some(
|
||||||
Lsn::from_str(params[2])
|
Lsn::from_str(params[2])
|
||||||
.with_context(|| format!("Failed to parse Lsn from {}", params[2]))?,
|
.with_context(|| format!("Failed to parse Lsn from {}", params[2]))?,
|
||||||
@@ -913,10 +913,24 @@ where
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check that the timeline exists
|
metrics::metric_vec_duration::observe_async_block_duration_by_result(
|
||||||
self.handle_basebackup_request(pgb, tenant_id, timeline_id, lsn, None, false, ctx)
|
&*crate::metrics::BASEBACKUP_QUERY_TIME,
|
||||||
.await?;
|
async move {
|
||||||
pgb.write_message_noflush(&BeMessage::CommandComplete(b"SELECT 1"))?;
|
self.handle_basebackup_request(
|
||||||
|
pgb,
|
||||||
|
tenant_id,
|
||||||
|
timeline_id,
|
||||||
|
lsn,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
pgb.write_message_noflush(&BeMessage::CommandComplete(b"SELECT 1"))?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
// return pair of prev_lsn and last_lsn
|
// return pair of prev_lsn and last_lsn
|
||||||
else if query_string.starts_with("get_last_record_rlsn ") {
|
else if query_string.starts_with("get_last_record_rlsn ") {
|
||||||
|
|||||||
@@ -887,7 +887,7 @@ impl<'a> DatadirModification<'a> {
|
|||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> Result<(), RelationError> {
|
) -> Result<(), RelationError> {
|
||||||
if rel.relnode == 0 {
|
if rel.relnode == 0 {
|
||||||
return Err(RelationError::AlreadyExists);
|
return Err(RelationError::InvalidRelnode);
|
||||||
}
|
}
|
||||||
// It's possible that this is the first rel for this db in this
|
// It's possible that this is the first rel for this db in this
|
||||||
// tablespace. Create the reldir entry for it if so.
|
// tablespace. Create the reldir entry for it if so.
|
||||||
|
|||||||
@@ -506,17 +506,17 @@ pub async fn shutdown_tasks(
|
|||||||
warn!(name = task.name, tenant_id = ?tenant_id, timeline_id = ?timeline_id, kind = ?task_kind, "stopping left-over");
|
warn!(name = task.name, tenant_id = ?tenant_id, timeline_id = ?timeline_id, kind = ?task_kind, "stopping left-over");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let completed = tokio::select! {
|
let join_handle = tokio::select! {
|
||||||
biased;
|
biased;
|
||||||
_ = &mut join_handle => { true },
|
_ = &mut join_handle => { None },
|
||||||
_ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {
|
_ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {
|
||||||
// allow some time to elapse before logging to cut down the number of log
|
// allow some time to elapse before logging to cut down the number of log
|
||||||
// lines.
|
// lines.
|
||||||
info!("waiting for {} to shut down", task.name);
|
info!("waiting for {} to shut down", task.name);
|
||||||
false
|
Some(join_handle)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if !completed {
|
if let Some(join_handle) = join_handle {
|
||||||
// we never handled this return value, but:
|
// we never handled this return value, but:
|
||||||
// - we don't deschedule which would lead to is_cancelled
|
// - we don't deschedule which would lead to is_cancelled
|
||||||
// - panics are already logged (is_panicked)
|
// - panics are already logged (is_panicked)
|
||||||
|
|||||||
@@ -440,8 +440,13 @@ pub enum GetTimelineError {
|
|||||||
pub enum DeleteTimelineError {
|
pub enum DeleteTimelineError {
|
||||||
#[error("NotFound")]
|
#[error("NotFound")]
|
||||||
NotFound,
|
NotFound,
|
||||||
|
|
||||||
#[error("HasChildren")]
|
#[error("HasChildren")]
|
||||||
HasChildren(Vec<TimelineId>),
|
HasChildren(Vec<TimelineId>),
|
||||||
|
|
||||||
|
#[error("Timeline deletion is already in progress")]
|
||||||
|
AlreadyInProgress,
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Other(#[from] anyhow::Error),
|
Other(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
@@ -496,6 +501,16 @@ impl DeletionGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum CreateTimelineError {
|
||||||
|
#[error("a timeline with the given ID already exists")]
|
||||||
|
AlreadyExists,
|
||||||
|
#[error(transparent)]
|
||||||
|
AncestorLsn(anyhow::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Other(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
impl Tenant {
|
impl Tenant {
|
||||||
/// Yet another helper for timeline initialization.
|
/// Yet another helper for timeline initialization.
|
||||||
/// Contains the common part of `load_local_timeline` and `load_remote_timeline`.
|
/// Contains the common part of `load_local_timeline` and `load_remote_timeline`.
|
||||||
@@ -585,6 +600,7 @@ impl Tenant {
|
|||||||
.layers
|
.layers
|
||||||
.read()
|
.read()
|
||||||
.await
|
.await
|
||||||
|
.0
|
||||||
.iter_historic_layers()
|
.iter_historic_layers()
|
||||||
.next()
|
.next()
|
||||||
.is_some(),
|
.is_some(),
|
||||||
@@ -1369,8 +1385,7 @@ impl Tenant {
|
|||||||
/// Returns the new timeline ID and reference to its Timeline object.
|
/// Returns the new timeline ID and reference to its Timeline object.
|
||||||
///
|
///
|
||||||
/// If the caller specified the timeline ID to use (`new_timeline_id`), and timeline with
|
/// If the caller specified the timeline ID to use (`new_timeline_id`), and timeline with
|
||||||
/// the same timeline ID already exists, returns None. If `new_timeline_id` is not given,
|
/// the same timeline ID already exists, returns CreateTimelineError::AlreadyExists.
|
||||||
/// a new unique ID is generated.
|
|
||||||
pub async fn create_timeline(
|
pub async fn create_timeline(
|
||||||
&self,
|
&self,
|
||||||
new_timeline_id: TimelineId,
|
new_timeline_id: TimelineId,
|
||||||
@@ -1379,11 +1394,12 @@ impl Tenant {
|
|||||||
pg_version: u32,
|
pg_version: u32,
|
||||||
broker_client: storage_broker::BrokerClientChannel,
|
broker_client: storage_broker::BrokerClientChannel,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<Option<Arc<Timeline>>> {
|
) -> Result<Arc<Timeline>, CreateTimelineError> {
|
||||||
anyhow::ensure!(
|
if !self.is_active() {
|
||||||
self.is_active(),
|
return Err(CreateTimelineError::Other(anyhow::anyhow!(
|
||||||
"Cannot create timelines on inactive tenant"
|
"Cannot create timelines on inactive tenant"
|
||||||
);
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(existing) = self.get_timeline(new_timeline_id, false) {
|
if let Ok(existing) = self.get_timeline(new_timeline_id, false) {
|
||||||
debug!("timeline {new_timeline_id} already exists");
|
debug!("timeline {new_timeline_id} already exists");
|
||||||
@@ -1403,7 +1419,7 @@ impl Tenant {
|
|||||||
.context("wait for timeline uploads to complete")?;
|
.context("wait for timeline uploads to complete")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(None);
|
return Err(CreateTimelineError::AlreadyExists);
|
||||||
}
|
}
|
||||||
|
|
||||||
let loaded_timeline = match ancestor_timeline_id {
|
let loaded_timeline = match ancestor_timeline_id {
|
||||||
@@ -1418,12 +1434,12 @@ impl Tenant {
|
|||||||
let ancestor_ancestor_lsn = ancestor_timeline.get_ancestor_lsn();
|
let ancestor_ancestor_lsn = ancestor_timeline.get_ancestor_lsn();
|
||||||
if ancestor_ancestor_lsn > *lsn {
|
if ancestor_ancestor_lsn > *lsn {
|
||||||
// can we safely just branch from the ancestor instead?
|
// can we safely just branch from the ancestor instead?
|
||||||
bail!(
|
return Err(CreateTimelineError::AncestorLsn(anyhow::anyhow!(
|
||||||
"invalid start lsn {} for ancestor timeline {}: less than timeline ancestor lsn {}",
|
"invalid start lsn {} for ancestor timeline {}: less than timeline ancestor lsn {}",
|
||||||
lsn,
|
lsn,
|
||||||
ancestor_timeline_id,
|
ancestor_timeline_id,
|
||||||
ancestor_ancestor_lsn,
|
ancestor_ancestor_lsn,
|
||||||
);
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the WAL to arrive and be processed on the parent branch up
|
// Wait for the WAL to arrive and be processed on the parent branch up
|
||||||
@@ -1457,7 +1473,7 @@ impl Tenant {
|
|||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(loaded_timeline))
|
Ok(loaded_timeline)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// perform one garbage collection iteration, removing old data files from disk.
|
/// perform one garbage collection iteration, removing old data files from disk.
|
||||||
@@ -1755,14 +1771,11 @@ impl Tenant {
|
|||||||
timeline = Arc::clone(timeline_entry.get());
|
timeline = Arc::clone(timeline_entry.get());
|
||||||
|
|
||||||
// Prevent two tasks from trying to delete the timeline at the same time.
|
// Prevent two tasks from trying to delete the timeline at the same time.
|
||||||
delete_lock_guard =
|
delete_lock_guard = DeletionGuard(
|
||||||
DeletionGuard(Arc::clone(&timeline.delete_lock).try_lock_owned().map_err(
|
Arc::clone(&timeline.delete_lock)
|
||||||
|_| {
|
.try_lock_owned()
|
||||||
DeleteTimelineError::Other(anyhow::anyhow!(
|
.map_err(|_| DeleteTimelineError::AlreadyInProgress)?,
|
||||||
"timeline deletion is already in progress"
|
);
|
||||||
))
|
|
||||||
},
|
|
||||||
)?);
|
|
||||||
|
|
||||||
// If another task finished the deletion just before we acquired the lock,
|
// If another task finished the deletion just before we acquired the lock,
|
||||||
// return success.
|
// return success.
|
||||||
@@ -2704,7 +2717,7 @@ impl Tenant {
|
|||||||
dst_id: TimelineId,
|
dst_id: TimelineId,
|
||||||
start_lsn: Option<Lsn>,
|
start_lsn: Option<Lsn>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<Arc<Timeline>> {
|
) -> Result<Arc<Timeline>, CreateTimelineError> {
|
||||||
let tl = self
|
let tl = self
|
||||||
.branch_timeline_impl(src_timeline, dst_id, start_lsn, ctx)
|
.branch_timeline_impl(src_timeline, dst_id, start_lsn, ctx)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -2721,7 +2734,7 @@ impl Tenant {
|
|||||||
dst_id: TimelineId,
|
dst_id: TimelineId,
|
||||||
start_lsn: Option<Lsn>,
|
start_lsn: Option<Lsn>,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
) -> anyhow::Result<Arc<Timeline>> {
|
) -> Result<Arc<Timeline>, CreateTimelineError> {
|
||||||
self.branch_timeline_impl(src_timeline, dst_id, start_lsn, ctx)
|
self.branch_timeline_impl(src_timeline, dst_id, start_lsn, ctx)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -2732,7 +2745,7 @@ impl Tenant {
|
|||||||
dst_id: TimelineId,
|
dst_id: TimelineId,
|
||||||
start_lsn: Option<Lsn>,
|
start_lsn: Option<Lsn>,
|
||||||
_ctx: &RequestContext,
|
_ctx: &RequestContext,
|
||||||
) -> anyhow::Result<Arc<Timeline>> {
|
) -> Result<Arc<Timeline>, CreateTimelineError> {
|
||||||
let src_id = src_timeline.timeline_id;
|
let src_id = src_timeline.timeline_id;
|
||||||
|
|
||||||
// If no start LSN is specified, we branch the new timeline from the source timeline's last record LSN
|
// If no start LSN is specified, we branch the new timeline from the source timeline's last record LSN
|
||||||
@@ -2772,16 +2785,17 @@ impl Tenant {
|
|||||||
.context(format!(
|
.context(format!(
|
||||||
"invalid branch start lsn: less than latest GC cutoff {}",
|
"invalid branch start lsn: less than latest GC cutoff {}",
|
||||||
*latest_gc_cutoff_lsn,
|
*latest_gc_cutoff_lsn,
|
||||||
))?;
|
))
|
||||||
|
.map_err(CreateTimelineError::AncestorLsn)?;
|
||||||
|
|
||||||
// and then the planned GC cutoff
|
// and then the planned GC cutoff
|
||||||
{
|
{
|
||||||
let gc_info = src_timeline.gc_info.read().unwrap();
|
let gc_info = src_timeline.gc_info.read().unwrap();
|
||||||
let cutoff = min(gc_info.pitr_cutoff, gc_info.horizon_cutoff);
|
let cutoff = min(gc_info.pitr_cutoff, gc_info.horizon_cutoff);
|
||||||
if start_lsn < cutoff {
|
if start_lsn < cutoff {
|
||||||
bail!(format!(
|
return Err(CreateTimelineError::AncestorLsn(anyhow::anyhow!(
|
||||||
"invalid branch start lsn: less than planned GC cutoff {cutoff}"
|
"invalid branch start lsn: less than planned GC cutoff {cutoff}"
|
||||||
));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3814,6 +3828,9 @@ mod tests {
|
|||||||
{
|
{
|
||||||
Ok(_) => panic!("branching should have failed"),
|
Ok(_) => panic!("branching should have failed"),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
let CreateTimelineError::AncestorLsn(err) = err else {
|
||||||
|
panic!("wrong error type")
|
||||||
|
};
|
||||||
assert!(err.to_string().contains("invalid branch start lsn"));
|
assert!(err.to_string().contains("invalid branch start lsn"));
|
||||||
assert!(err
|
assert!(err
|
||||||
.source()
|
.source()
|
||||||
@@ -3843,6 +3860,9 @@ mod tests {
|
|||||||
{
|
{
|
||||||
Ok(_) => panic!("branching should have failed"),
|
Ok(_) => panic!("branching should have failed"),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
let CreateTimelineError::AncestorLsn(err) = err else {
|
||||||
|
panic!("wrong error type");
|
||||||
|
};
|
||||||
assert!(&err.to_string().contains("invalid branch start lsn"));
|
assert!(&err.to_string().contains("invalid branch start lsn"));
|
||||||
assert!(&err
|
assert!(&err
|
||||||
.source()
|
.source()
|
||||||
|
|||||||
@@ -51,25 +51,23 @@ use crate::keyspace::KeyPartitioning;
|
|||||||
use crate::repository::Key;
|
use crate::repository::Key;
|
||||||
use crate::tenant::storage_layer::InMemoryLayer;
|
use crate::tenant::storage_layer::InMemoryLayer;
|
||||||
use crate::tenant::storage_layer::Layer;
|
use crate::tenant::storage_layer::Layer;
|
||||||
use anyhow::Context;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use utils::lsn::Lsn;
|
use utils::lsn::Lsn;
|
||||||
|
|
||||||
use historic_layer_coverage::BufferedHistoricLayerCoverage;
|
use historic_layer_coverage::BufferedHistoricLayerCoverage;
|
||||||
pub use historic_layer_coverage::Replacement;
|
pub use historic_layer_coverage::LayerKey;
|
||||||
|
|
||||||
use super::storage_layer::range_eq;
|
use super::storage_layer::range_eq;
|
||||||
use super::storage_layer::PersistentLayerDesc;
|
use super::storage_layer::PersistentLayerDesc;
|
||||||
use super::storage_layer::PersistentLayerKey;
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// LayerMap tracks what layers exist on a timeline.
|
/// LayerMap tracks what layers exist on a timeline.
|
||||||
///
|
///
|
||||||
pub struct LayerMap<L: ?Sized> {
|
#[derive(Default)]
|
||||||
|
pub struct LayerMap {
|
||||||
//
|
//
|
||||||
// 'open_layer' holds the current InMemoryLayer that is accepting new
|
// 'open_layer' holds the current InMemoryLayer that is accepting new
|
||||||
// records. If it is None, 'next_open_layer_at' will be set instead, indicating
|
// records. If it is None, 'next_open_layer_at' will be set instead, indicating
|
||||||
@@ -95,24 +93,6 @@ pub struct LayerMap<L: ?Sized> {
|
|||||||
/// L0 layers have key range Key::MIN..Key::MAX, and locating them using R-Tree search is very inefficient.
|
/// L0 layers have key range Key::MIN..Key::MAX, and locating them using R-Tree search is very inefficient.
|
||||||
/// So L0 layers are held in l0_delta_layers vector, in addition to the R-tree.
|
/// So L0 layers are held in l0_delta_layers vector, in addition to the R-tree.
|
||||||
l0_delta_layers: Vec<Arc<PersistentLayerDesc>>,
|
l0_delta_layers: Vec<Arc<PersistentLayerDesc>>,
|
||||||
|
|
||||||
/// Mapping from persistent layer key to the actual layer object. Currently, it stores delta, image, and
|
|
||||||
/// remote layers. In future refactors, this will be eventually moved out of LayerMap into Timeline, and
|
|
||||||
/// RemoteLayer will be removed.
|
|
||||||
mapping: HashMap<PersistentLayerKey, Arc<L>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<L: ?Sized> Default for LayerMap<L> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
open_layer: None,
|
|
||||||
next_open_layer_at: None,
|
|
||||||
frozen_layers: VecDeque::default(),
|
|
||||||
l0_delta_layers: Vec::default(),
|
|
||||||
historic: BufferedHistoricLayerCoverage::default(),
|
|
||||||
mapping: HashMap::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The primary update API for the layer map.
|
/// The primary update API for the layer map.
|
||||||
@@ -120,24 +100,21 @@ impl<L: ?Sized> Default for LayerMap<L> {
|
|||||||
/// Batching historic layer insertions and removals is good for
|
/// Batching historic layer insertions and removals is good for
|
||||||
/// performance and this struct helps us do that correctly.
|
/// performance and this struct helps us do that correctly.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub struct BatchedUpdates<'a, L: ?Sized + Layer> {
|
pub struct BatchedUpdates<'a> {
|
||||||
// While we hold this exclusive reference to the layer map the type checker
|
// While we hold this exclusive reference to the layer map the type checker
|
||||||
// will prevent us from accidentally reading any unflushed updates.
|
// will prevent us from accidentally reading any unflushed updates.
|
||||||
layer_map: &'a mut LayerMap<L>,
|
layer_map: &'a mut LayerMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provide ability to batch more updates while hiding the read
|
/// Provide ability to batch more updates while hiding the read
|
||||||
/// API so we don't accidentally read without flushing.
|
/// API so we don't accidentally read without flushing.
|
||||||
impl<L> BatchedUpdates<'_, L>
|
impl BatchedUpdates<'_> {
|
||||||
where
|
|
||||||
L: ?Sized + Layer,
|
|
||||||
{
|
|
||||||
///
|
///
|
||||||
/// Insert an on-disk layer.
|
/// Insert an on-disk layer.
|
||||||
///
|
///
|
||||||
// TODO remove the `layer` argument when `mapping` is refactored out of `LayerMap`
|
// TODO remove the `layer` argument when `mapping` is refactored out of `LayerMap`
|
||||||
pub fn insert_historic(&mut self, layer_desc: PersistentLayerDesc, layer: Arc<L>) {
|
pub fn insert_historic(&mut self, layer_desc: PersistentLayerDesc) {
|
||||||
self.layer_map.insert_historic_noflush(layer_desc, layer)
|
self.layer_map.insert_historic_noflush(layer_desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -145,31 +122,8 @@ where
|
|||||||
///
|
///
|
||||||
/// This should be called when the corresponding file on disk has been deleted.
|
/// This should be called when the corresponding file on disk has been deleted.
|
||||||
///
|
///
|
||||||
pub fn remove_historic(&mut self, layer_desc: PersistentLayerDesc, layer: Arc<L>) {
|
pub fn remove_historic(&mut self, layer_desc: PersistentLayerDesc) {
|
||||||
self.layer_map.remove_historic_noflush(layer_desc, layer)
|
self.layer_map.remove_historic_noflush(layer_desc)
|
||||||
}
|
|
||||||
|
|
||||||
/// Replaces existing layer iff it is the `expected`.
|
|
||||||
///
|
|
||||||
/// If the expected layer has been removed it will not be inserted by this function.
|
|
||||||
///
|
|
||||||
/// Returned `Replacement` describes succeeding in replacement or the reason why it could not
|
|
||||||
/// be done.
|
|
||||||
///
|
|
||||||
/// TODO replacement can be done without buffering and rebuilding layer map updates.
|
|
||||||
/// One way to do that is to add a layer of indirection for returned values, so
|
|
||||||
/// that we can replace values only by updating a hashmap.
|
|
||||||
pub fn replace_historic(
|
|
||||||
&mut self,
|
|
||||||
expected_desc: PersistentLayerDesc,
|
|
||||||
expected: &Arc<L>,
|
|
||||||
new_desc: PersistentLayerDesc,
|
|
||||||
new: Arc<L>,
|
|
||||||
) -> anyhow::Result<Replacement<Arc<L>>> {
|
|
||||||
fail::fail_point!("layermap-replace-notfound", |_| Ok(Replacement::NotFound));
|
|
||||||
|
|
||||||
self.layer_map
|
|
||||||
.replace_historic_noflush(expected_desc, expected, new_desc, new)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We will flush on drop anyway, but this method makes it
|
// We will flush on drop anyway, but this method makes it
|
||||||
@@ -185,25 +139,19 @@ where
|
|||||||
// than panic later or read without flushing.
|
// than panic later or read without flushing.
|
||||||
//
|
//
|
||||||
// TODO maybe warn if flush hasn't explicitly been called
|
// TODO maybe warn if flush hasn't explicitly been called
|
||||||
impl<L> Drop for BatchedUpdates<'_, L>
|
impl Drop for BatchedUpdates<'_> {
|
||||||
where
|
|
||||||
L: ?Sized + Layer,
|
|
||||||
{
|
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.layer_map.flush_updates();
|
self.layer_map.flush_updates();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return value of LayerMap::search
|
/// Return value of LayerMap::search
|
||||||
pub struct SearchResult<L: ?Sized> {
|
pub struct SearchResult {
|
||||||
pub layer: Arc<L>,
|
pub layer: Arc<PersistentLayerDesc>,
|
||||||
pub lsn_floor: Lsn,
|
pub lsn_floor: Lsn,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<L> LayerMap<L>
|
impl LayerMap {
|
||||||
where
|
|
||||||
L: ?Sized + Layer,
|
|
||||||
{
|
|
||||||
///
|
///
|
||||||
/// Find the latest layer (by lsn.end) that covers the given
|
/// Find the latest layer (by lsn.end) that covers the given
|
||||||
/// 'key', with lsn.start < 'end_lsn'.
|
/// 'key', with lsn.start < 'end_lsn'.
|
||||||
@@ -235,7 +183,7 @@ where
|
|||||||
/// NOTE: This only searches the 'historic' layers, *not* the
|
/// NOTE: This only searches the 'historic' layers, *not* the
|
||||||
/// 'open' and 'frozen' layers!
|
/// 'open' and 'frozen' layers!
|
||||||
///
|
///
|
||||||
pub fn search(&self, key: Key, end_lsn: Lsn) -> Option<SearchResult<L>> {
|
pub fn search(&self, key: Key, end_lsn: Lsn) -> Option<SearchResult> {
|
||||||
let version = self.historic.get().unwrap().get_version(end_lsn.0 - 1)?;
|
let version = self.historic.get().unwrap().get_version(end_lsn.0 - 1)?;
|
||||||
let latest_delta = version.delta_coverage.query(key.to_i128());
|
let latest_delta = version.delta_coverage.query(key.to_i128());
|
||||||
let latest_image = version.image_coverage.query(key.to_i128());
|
let latest_image = version.image_coverage.query(key.to_i128());
|
||||||
@@ -244,7 +192,6 @@ where
|
|||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
(None, Some(image)) => {
|
(None, Some(image)) => {
|
||||||
let lsn_floor = image.get_lsn_range().start;
|
let lsn_floor = image.get_lsn_range().start;
|
||||||
let image = self.get_layer_from_mapping(&image.key()).clone();
|
|
||||||
Some(SearchResult {
|
Some(SearchResult {
|
||||||
layer: image,
|
layer: image,
|
||||||
lsn_floor,
|
lsn_floor,
|
||||||
@@ -252,7 +199,6 @@ where
|
|||||||
}
|
}
|
||||||
(Some(delta), None) => {
|
(Some(delta), None) => {
|
||||||
let lsn_floor = delta.get_lsn_range().start;
|
let lsn_floor = delta.get_lsn_range().start;
|
||||||
let delta = self.get_layer_from_mapping(&delta.key()).clone();
|
|
||||||
Some(SearchResult {
|
Some(SearchResult {
|
||||||
layer: delta,
|
layer: delta,
|
||||||
lsn_floor,
|
lsn_floor,
|
||||||
@@ -263,7 +209,6 @@ where
|
|||||||
let image_is_newer = image.get_lsn_range().end >= delta.get_lsn_range().end;
|
let image_is_newer = image.get_lsn_range().end >= delta.get_lsn_range().end;
|
||||||
let image_exact_match = img_lsn + 1 == end_lsn;
|
let image_exact_match = img_lsn + 1 == end_lsn;
|
||||||
if image_is_newer || image_exact_match {
|
if image_is_newer || image_exact_match {
|
||||||
let image = self.get_layer_from_mapping(&image.key()).clone();
|
|
||||||
Some(SearchResult {
|
Some(SearchResult {
|
||||||
layer: image,
|
layer: image,
|
||||||
lsn_floor: img_lsn,
|
lsn_floor: img_lsn,
|
||||||
@@ -271,7 +216,6 @@ where
|
|||||||
} else {
|
} else {
|
||||||
let lsn_floor =
|
let lsn_floor =
|
||||||
std::cmp::max(delta.get_lsn_range().start, image.get_lsn_range().start + 1);
|
std::cmp::max(delta.get_lsn_range().start, image.get_lsn_range().start + 1);
|
||||||
let delta = self.get_layer_from_mapping(&delta.key()).clone();
|
|
||||||
Some(SearchResult {
|
Some(SearchResult {
|
||||||
layer: delta,
|
layer: delta,
|
||||||
lsn_floor,
|
lsn_floor,
|
||||||
@@ -282,7 +226,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Start a batch of updates, applied on drop
|
/// Start a batch of updates, applied on drop
|
||||||
pub fn batch_update(&mut self) -> BatchedUpdates<'_, L> {
|
pub fn batch_update(&mut self) -> BatchedUpdates<'_> {
|
||||||
BatchedUpdates { layer_map: self }
|
BatchedUpdates { layer_map: self }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,48 +236,32 @@ where
|
|||||||
/// Helper function for BatchedUpdates::insert_historic
|
/// Helper function for BatchedUpdates::insert_historic
|
||||||
///
|
///
|
||||||
/// TODO(chi): remove L generic so that we do not need to pass layer object.
|
/// TODO(chi): remove L generic so that we do not need to pass layer object.
|
||||||
pub(self) fn insert_historic_noflush(
|
pub(self) fn insert_historic_noflush(&mut self, layer_desc: PersistentLayerDesc) {
|
||||||
&mut self,
|
|
||||||
layer_desc: PersistentLayerDesc,
|
|
||||||
layer: Arc<L>,
|
|
||||||
) {
|
|
||||||
self.mapping.insert(layer_desc.key(), layer.clone());
|
|
||||||
|
|
||||||
// TODO: See #3869, resulting #4088, attempted fix and repro #4094
|
// TODO: See #3869, resulting #4088, attempted fix and repro #4094
|
||||||
|
|
||||||
if Self::is_l0(&layer) {
|
if Self::is_l0(&layer_desc) {
|
||||||
self.l0_delta_layers.push(layer_desc.clone().into());
|
self.l0_delta_layers.push(layer_desc.clone().into());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.historic.insert(
|
self.historic.insert(
|
||||||
historic_layer_coverage::LayerKey::from(&*layer),
|
historic_layer_coverage::LayerKey::from(&layer_desc),
|
||||||
layer_desc.into(),
|
layer_desc.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_layer_from_mapping(&self, key: &PersistentLayerKey) -> &Arc<L> {
|
|
||||||
let layer = self
|
|
||||||
.mapping
|
|
||||||
.get(key)
|
|
||||||
.with_context(|| format!("{key:?}"))
|
|
||||||
.expect("inconsistent layer mapping");
|
|
||||||
layer
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Remove an on-disk layer from the map.
|
/// Remove an on-disk layer from the map.
|
||||||
///
|
///
|
||||||
/// Helper function for BatchedUpdates::remove_historic
|
/// Helper function for BatchedUpdates::remove_historic
|
||||||
///
|
///
|
||||||
pub fn remove_historic_noflush(&mut self, layer_desc: PersistentLayerDesc, layer: Arc<L>) {
|
pub fn remove_historic_noflush(&mut self, layer_desc: PersistentLayerDesc) {
|
||||||
self.historic
|
self.historic
|
||||||
.remove(historic_layer_coverage::LayerKey::from(&*layer));
|
.remove(historic_layer_coverage::LayerKey::from(&layer_desc));
|
||||||
if Self::is_l0(&layer) {
|
let layer_key = layer_desc.key();
|
||||||
|
if Self::is_l0(&layer_desc) {
|
||||||
let len_before = self.l0_delta_layers.len();
|
let len_before = self.l0_delta_layers.len();
|
||||||
let mut l0_delta_layers = std::mem::take(&mut self.l0_delta_layers);
|
let mut l0_delta_layers = std::mem::take(&mut self.l0_delta_layers);
|
||||||
l0_delta_layers.retain(|other| {
|
l0_delta_layers.retain(|other| other.key() != layer_key);
|
||||||
!Self::compare_arced_layers(self.get_layer_from_mapping(&other.key()), &layer)
|
|
||||||
});
|
|
||||||
self.l0_delta_layers = l0_delta_layers;
|
self.l0_delta_layers = l0_delta_layers;
|
||||||
// this assertion is related to use of Arc::ptr_eq in Self::compare_arced_layers,
|
// this assertion is related to use of Arc::ptr_eq in Self::compare_arced_layers,
|
||||||
// there's a chance that the comparison fails at runtime due to it comparing (pointer,
|
// there's a chance that the comparison fails at runtime due to it comparing (pointer,
|
||||||
@@ -344,69 +272,6 @@ where
|
|||||||
"failed to locate removed historic layer from l0_delta_layers"
|
"failed to locate removed historic layer from l0_delta_layers"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.mapping.remove(&layer_desc.key());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(self) fn replace_historic_noflush(
|
|
||||||
&mut self,
|
|
||||||
expected_desc: PersistentLayerDesc,
|
|
||||||
expected: &Arc<L>,
|
|
||||||
new_desc: PersistentLayerDesc,
|
|
||||||
new: Arc<L>,
|
|
||||||
) -> anyhow::Result<Replacement<Arc<L>>> {
|
|
||||||
let key = historic_layer_coverage::LayerKey::from(&**expected);
|
|
||||||
let other = historic_layer_coverage::LayerKey::from(&*new);
|
|
||||||
|
|
||||||
let expected_l0 = Self::is_l0(expected);
|
|
||||||
let new_l0 = Self::is_l0(&new);
|
|
||||||
|
|
||||||
anyhow::ensure!(
|
|
||||||
key == other,
|
|
||||||
"expected and new must have equal LayerKeys: {key:?} != {other:?}"
|
|
||||||
);
|
|
||||||
|
|
||||||
anyhow::ensure!(
|
|
||||||
expected_l0 == new_l0,
|
|
||||||
"expected and new must both be l0 deltas or neither should be: {expected_l0} != {new_l0}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let l0_index = if expected_l0 {
|
|
||||||
// find the index in case replace worked, we need to replace that as well
|
|
||||||
let pos = self.l0_delta_layers.iter().position(|slot| {
|
|
||||||
Self::compare_arced_layers(self.get_layer_from_mapping(&slot.key()), expected)
|
|
||||||
});
|
|
||||||
|
|
||||||
if pos.is_none() {
|
|
||||||
return Ok(Replacement::NotFound);
|
|
||||||
}
|
|
||||||
pos
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_desc = Arc::new(new_desc);
|
|
||||||
let replaced = self.historic.replace(&key, new_desc.clone(), |existing| {
|
|
||||||
**existing == expected_desc
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Replacement::Replaced { .. } = &replaced {
|
|
||||||
self.mapping.remove(&expected_desc.key());
|
|
||||||
self.mapping.insert(new_desc.key(), new);
|
|
||||||
if let Some(index) = l0_index {
|
|
||||||
self.l0_delta_layers[index] = new_desc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let replaced = match replaced {
|
|
||||||
Replacement::Replaced { in_buffered } => Replacement::Replaced { in_buffered },
|
|
||||||
Replacement::NotFound => Replacement::NotFound,
|
|
||||||
Replacement::RemovalBuffered => Replacement::RemovalBuffered,
|
|
||||||
Replacement::Unexpected(x) => {
|
|
||||||
Replacement::Unexpected(self.get_layer_from_mapping(&x.key()).clone())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(replaced)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function for BatchedUpdates::drop.
|
/// Helper function for BatchedUpdates::drop.
|
||||||
@@ -454,10 +319,8 @@ where
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter_historic_layers(&self) -> impl '_ + Iterator<Item = Arc<L>> {
|
pub fn iter_historic_layers(&self) -> impl '_ + Iterator<Item = Arc<PersistentLayerDesc>> {
|
||||||
self.historic
|
self.historic.iter()
|
||||||
.iter()
|
|
||||||
.map(|x| self.get_layer_from_mapping(&x.key()).clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -472,7 +335,7 @@ where
|
|||||||
&self,
|
&self,
|
||||||
key_range: &Range<Key>,
|
key_range: &Range<Key>,
|
||||||
lsn: Lsn,
|
lsn: Lsn,
|
||||||
) -> Result<Vec<(Range<Key>, Option<Arc<L>>)>> {
|
) -> Result<Vec<(Range<Key>, Option<Arc<PersistentLayerDesc>>)>> {
|
||||||
let version = match self.historic.get().unwrap().get_version(lsn.0) {
|
let version = match self.historic.get().unwrap().get_version(lsn.0) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return Ok(vec![]),
|
None => return Ok(vec![]),
|
||||||
@@ -482,36 +345,26 @@ where
|
|||||||
let end = key_range.end.to_i128();
|
let end = key_range.end.to_i128();
|
||||||
|
|
||||||
// Initialize loop variables
|
// Initialize loop variables
|
||||||
let mut coverage: Vec<(Range<Key>, Option<Arc<L>>)> = vec![];
|
let mut coverage: Vec<(Range<Key>, Option<Arc<PersistentLayerDesc>>)> = vec![];
|
||||||
let mut current_key = start;
|
let mut current_key = start;
|
||||||
let mut current_val = version.image_coverage.query(start);
|
let mut current_val = version.image_coverage.query(start);
|
||||||
|
|
||||||
// Loop through the change events and push intervals
|
// Loop through the change events and push intervals
|
||||||
for (change_key, change_val) in version.image_coverage.range(start..end) {
|
for (change_key, change_val) in version.image_coverage.range(start..end) {
|
||||||
let kr = Key::from_i128(current_key)..Key::from_i128(change_key);
|
let kr = Key::from_i128(current_key)..Key::from_i128(change_key);
|
||||||
coverage.push((
|
coverage.push((kr, current_val.take()));
|
||||||
kr,
|
|
||||||
current_val
|
|
||||||
.take()
|
|
||||||
.map(|l| self.get_layer_from_mapping(&l.key()).clone()),
|
|
||||||
));
|
|
||||||
current_key = change_key;
|
current_key = change_key;
|
||||||
current_val = change_val.clone();
|
current_val = change_val.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the final interval
|
// Add the final interval
|
||||||
let kr = Key::from_i128(current_key)..Key::from_i128(end);
|
let kr = Key::from_i128(current_key)..Key::from_i128(end);
|
||||||
coverage.push((
|
coverage.push((kr, current_val.take()));
|
||||||
kr,
|
|
||||||
current_val
|
|
||||||
.take()
|
|
||||||
.map(|l| self.get_layer_from_mapping(&l.key()).clone()),
|
|
||||||
));
|
|
||||||
|
|
||||||
Ok(coverage)
|
Ok(coverage)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_l0(layer: &L) -> bool {
|
pub fn is_l0(layer: &PersistentLayerDesc) -> bool {
|
||||||
range_eq(&layer.get_key_range(), &(Key::MIN..Key::MAX))
|
range_eq(&layer.get_key_range(), &(Key::MIN..Key::MAX))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,7 +390,7 @@ where
|
|||||||
/// TODO The optimal number should probably be slightly higher than 1, but to
|
/// TODO The optimal number should probably be slightly higher than 1, but to
|
||||||
/// implement that we need to plumb a lot more context into this function
|
/// implement that we need to plumb a lot more context into this function
|
||||||
/// than just the current partition_range.
|
/// than just the current partition_range.
|
||||||
pub fn is_reimage_worthy(layer: &L, partition_range: &Range<Key>) -> bool {
|
pub fn is_reimage_worthy(layer: &PersistentLayerDesc, partition_range: &Range<Key>) -> bool {
|
||||||
// Case 1
|
// Case 1
|
||||||
if !Self::is_l0(layer) {
|
if !Self::is_l0(layer) {
|
||||||
return true;
|
return true;
|
||||||
@@ -595,9 +448,7 @@ where
|
|||||||
let kr = Key::from_i128(current_key)..Key::from_i128(change_key);
|
let kr = Key::from_i128(current_key)..Key::from_i128(change_key);
|
||||||
let lr = lsn.start..val.get_lsn_range().start;
|
let lr = lsn.start..val.get_lsn_range().start;
|
||||||
if !kr.is_empty() {
|
if !kr.is_empty() {
|
||||||
let base_count =
|
let base_count = Self::is_reimage_worthy(&val, key) as usize;
|
||||||
Self::is_reimage_worthy(self.get_layer_from_mapping(&val.key()), key)
|
|
||||||
as usize;
|
|
||||||
let new_limit = limit.map(|l| l - base_count);
|
let new_limit = limit.map(|l| l - base_count);
|
||||||
let max_stacked_deltas_underneath =
|
let max_stacked_deltas_underneath =
|
||||||
self.count_deltas(&kr, &lr, new_limit)?;
|
self.count_deltas(&kr, &lr, new_limit)?;
|
||||||
@@ -620,9 +471,7 @@ where
|
|||||||
let lr = lsn.start..val.get_lsn_range().start;
|
let lr = lsn.start..val.get_lsn_range().start;
|
||||||
|
|
||||||
if !kr.is_empty() {
|
if !kr.is_empty() {
|
||||||
let base_count =
|
let base_count = Self::is_reimage_worthy(&val, key) as usize;
|
||||||
Self::is_reimage_worthy(self.get_layer_from_mapping(&val.key()), key)
|
|
||||||
as usize;
|
|
||||||
let new_limit = limit.map(|l| l - base_count);
|
let new_limit = limit.map(|l| l - base_count);
|
||||||
let max_stacked_deltas_underneath = self.count_deltas(&kr, &lr, new_limit)?;
|
let max_stacked_deltas_underneath = self.count_deltas(&kr, &lr, new_limit)?;
|
||||||
max_stacked_deltas = std::cmp::max(
|
max_stacked_deltas = std::cmp::max(
|
||||||
@@ -772,12 +621,8 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return all L0 delta layers
|
/// Return all L0 delta layers
|
||||||
pub fn get_level0_deltas(&self) -> Result<Vec<Arc<L>>> {
|
pub fn get_level0_deltas(&self) -> Result<Vec<Arc<PersistentLayerDesc>>> {
|
||||||
Ok(self
|
Ok(self.l0_delta_layers.to_vec())
|
||||||
.l0_delta_layers
|
|
||||||
.iter()
|
|
||||||
.map(|x| self.get_layer_from_mapping(&x.key()).clone())
|
|
||||||
.collect())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// debugging function to print out the contents of the layer map
|
/// debugging function to print out the contents of the layer map
|
||||||
@@ -802,72 +647,51 @@ where
|
|||||||
println!("End dump LayerMap");
|
println!("End dump LayerMap");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Similar to `Arc::ptr_eq`, but only compares the object pointers, not vtables.
|
|
||||||
///
|
|
||||||
/// Returns `true` if the two `Arc` point to the same layer, false otherwise.
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn compare_arced_layers(left: &Arc<L>, right: &Arc<L>) -> bool {
|
|
||||||
// "dyn Trait" objects are "fat pointers" in that they have two components:
|
|
||||||
// - pointer to the object
|
|
||||||
// - pointer to the vtable
|
|
||||||
//
|
|
||||||
// rust does not provide a guarantee that these vtables are unique, but however
|
|
||||||
// `Arc::ptr_eq` as of writing (at least up to 1.67) uses a comparison where both the
|
|
||||||
// pointer and the vtable need to be equal.
|
|
||||||
//
|
|
||||||
// See: https://github.com/rust-lang/rust/issues/103763
|
|
||||||
//
|
|
||||||
// A future version of rust will most likely use this form below, where we cast each
|
|
||||||
// pointer into a pointer to unit, which drops the inaccessible vtable pointer, making it
|
|
||||||
// not affect the comparison.
|
|
||||||
//
|
|
||||||
// See: https://github.com/rust-lang/rust/pull/106450
|
|
||||||
let left = Arc::as_ptr(left) as *const ();
|
|
||||||
let right = Arc::as_ptr(right) as *const ();
|
|
||||||
|
|
||||||
left == right
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{LayerMap, Replacement};
|
use super::LayerMap;
|
||||||
use crate::tenant::storage_layer::{Layer, LayerDescriptor, LayerFileName};
|
use crate::tenant::storage_layer::{tests::LayerDescriptor, LayerFileName};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
mod l0_delta_layers_updated {
|
mod l0_delta_layers_updated {
|
||||||
|
|
||||||
|
use crate::tenant::{
|
||||||
|
storage_layer::{PersistentLayer, PersistentLayerDesc},
|
||||||
|
timeline::LayerFileManager,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn for_full_range_delta() {
|
fn for_full_range_delta() {
|
||||||
// l0_delta_layers are used by compaction, and should observe all buffered updates
|
// l0_delta_layers are used by compaction, and should observe all buffered updates
|
||||||
l0_delta_layers_updated_scenario(
|
l0_delta_layers_updated_scenario(
|
||||||
"000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000053423C21-0000000053424D69",
|
"000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000053423C21-0000000053424D69",
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn for_non_full_range_delta() {
|
fn for_non_full_range_delta() {
|
||||||
// has minimal uncovered areas compared to l0_delta_layers_updated_on_insert_replace_remove_for_full_range_delta
|
// has minimal uncovered areas compared to l0_delta_layers_updated_on_insert_replace_remove_for_full_range_delta
|
||||||
l0_delta_layers_updated_scenario(
|
l0_delta_layers_updated_scenario(
|
||||||
"000000000000000000000000000000000001-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE__0000000053423C21-0000000053424D69",
|
"000000000000000000000000000000000001-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE__0000000053423C21-0000000053424D69",
|
||||||
// because not full range
|
// because not full range
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn for_image() {
|
fn for_image() {
|
||||||
l0_delta_layers_updated_scenario(
|
l0_delta_layers_updated_scenario(
|
||||||
"000000000000000000000000000000000000-000000000000000000000000000000010000__0000000053424D69",
|
"000000000000000000000000000000000000-000000000000000000000000000000010000__0000000053424D69",
|
||||||
// code only checks if it is a full range layer, doesn't care about images, which must
|
// code only checks if it is a full range layer, doesn't care about images, which must
|
||||||
// mean we should in practice never have full range images
|
// mean we should in practice never have full range images
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -883,16 +707,16 @@ mod tests {
|
|||||||
let not_found = Arc::new(layer.clone());
|
let not_found = Arc::new(layer.clone());
|
||||||
let new_version = Arc::new(layer);
|
let new_version = Arc::new(layer);
|
||||||
|
|
||||||
let mut map = LayerMap::default();
|
// after the immutable storage state refactor, the replace operation
|
||||||
|
// will not use layer map any more. We keep it here for consistency in test cases
|
||||||
|
// and can remove it in the future.
|
||||||
|
let _map = LayerMap::default();
|
||||||
|
|
||||||
let res = map.batch_update().replace_historic(
|
let mut mapping = LayerFileManager::new();
|
||||||
not_found.get_persistent_layer_desc(),
|
|
||||||
¬_found,
|
|
||||||
new_version.get_persistent_layer_desc(),
|
|
||||||
new_version,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(matches!(res, Ok(Replacement::NotFound)), "{res:?}");
|
mapping
|
||||||
|
.replace_and_verify(not_found, new_version)
|
||||||
|
.unwrap_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn l0_delta_layers_updated_scenario(layer_name: &str, expected_l0: bool) {
|
fn l0_delta_layers_updated_scenario(layer_name: &str, expected_l0: bool) {
|
||||||
@@ -903,49 +727,44 @@ mod tests {
|
|||||||
let downloaded = Arc::new(skeleton);
|
let downloaded = Arc::new(skeleton);
|
||||||
|
|
||||||
let mut map = LayerMap::default();
|
let mut map = LayerMap::default();
|
||||||
|
let mut mapping = LayerFileManager::new();
|
||||||
|
|
||||||
// two disjoint Arcs in different lifecycle phases. even if it seems they must be the
|
// two disjoint Arcs in different lifecycle phases. even if it seems they must be the
|
||||||
// same layer, we use LayerMap::compare_arced_layers as the identity of layers.
|
// same layer, we use LayerMap::compare_arced_layers as the identity of layers.
|
||||||
assert!(!LayerMap::compare_arced_layers(&remote, &downloaded));
|
assert_eq!(remote.layer_desc(), downloaded.layer_desc());
|
||||||
|
|
||||||
let expected_in_counts = (1, usize::from(expected_l0));
|
let expected_in_counts = (1, usize::from(expected_l0));
|
||||||
|
|
||||||
map.batch_update()
|
map.batch_update()
|
||||||
.insert_historic(remote.get_persistent_layer_desc(), remote.clone());
|
.insert_historic(remote.layer_desc().clone());
|
||||||
assert_eq!(count_layer_in(&map, &remote), expected_in_counts);
|
mapping.insert(remote.clone());
|
||||||
|
assert_eq!(
|
||||||
let replaced = map
|
count_layer_in(&map, remote.layer_desc()),
|
||||||
.batch_update()
|
expected_in_counts
|
||||||
.replace_historic(
|
);
|
||||||
remote.get_persistent_layer_desc(),
|
|
||||||
&remote,
|
mapping
|
||||||
downloaded.get_persistent_layer_desc(),
|
.replace_and_verify(remote, downloaded.clone())
|
||||||
downloaded.clone(),
|
.expect("name derived attributes are the same");
|
||||||
)
|
assert_eq!(
|
||||||
.expect("name derived attributes are the same");
|
count_layer_in(&map, downloaded.layer_desc()),
|
||||||
assert!(
|
expected_in_counts
|
||||||
matches!(replaced, Replacement::Replaced { .. }),
|
|
||||||
"{replaced:?}"
|
|
||||||
);
|
);
|
||||||
assert_eq!(count_layer_in(&map, &downloaded), expected_in_counts);
|
|
||||||
|
|
||||||
map.batch_update()
|
map.batch_update()
|
||||||
.remove_historic(downloaded.get_persistent_layer_desc(), downloaded.clone());
|
.remove_historic(downloaded.layer_desc().clone());
|
||||||
assert_eq!(count_layer_in(&map, &downloaded), (0, 0));
|
assert_eq!(count_layer_in(&map, downloaded.layer_desc()), (0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn count_layer_in<L: Layer + ?Sized>(map: &LayerMap<L>, layer: &Arc<L>) -> (usize, usize) {
|
fn count_layer_in(map: &LayerMap, layer: &PersistentLayerDesc) -> (usize, usize) {
|
||||||
let historic = map
|
let historic = map
|
||||||
.iter_historic_layers()
|
.iter_historic_layers()
|
||||||
.filter(|x| LayerMap::compare_arced_layers(x, layer))
|
.filter(|x| x.key() == layer.key())
|
||||||
.count();
|
.count();
|
||||||
let l0s = map
|
let l0s = map
|
||||||
.get_level0_deltas()
|
.get_level0_deltas()
|
||||||
.expect("why does this return a result");
|
.expect("why does this return a result");
|
||||||
let l0 = l0s
|
let l0 = l0s.iter().filter(|x| x.key() == layer.key()).count();
|
||||||
.iter()
|
|
||||||
.filter(|x| LayerMap::compare_arced_layers(x, layer))
|
|
||||||
.count();
|
|
||||||
|
|
||||||
(historic, l0)
|
(historic, l0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ use std::ops::Range;
|
|||||||
|
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::tenant::storage_layer::PersistentLayerDesc;
|
||||||
|
|
||||||
use super::layer_coverage::LayerCoverageTuple;
|
use super::layer_coverage::LayerCoverageTuple;
|
||||||
|
|
||||||
/// Layers in this module are identified and indexed by this data.
|
/// Layers in this module are identified and indexed by this data.
|
||||||
@@ -41,8 +43,8 @@ impl Ord for LayerKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, L: crate::tenant::storage_layer::Layer + ?Sized> From<&'a L> for LayerKey {
|
impl From<&PersistentLayerDesc> for LayerKey {
|
||||||
fn from(layer: &'a L) -> Self {
|
fn from(layer: &PersistentLayerDesc) -> Self {
|
||||||
let kr = layer.get_key_range();
|
let kr = layer.get_key_range();
|
||||||
let lr = layer.get_lsn_range();
|
let lr = layer.get_lsn_range();
|
||||||
LayerKey {
|
LayerKey {
|
||||||
@@ -454,59 +456,6 @@ impl<Value: Clone> BufferedHistoricLayerCoverage<Value> {
|
|||||||
self.buffer.insert(layer_key, None);
|
self.buffer.insert(layer_key, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces a previous layer with a new layer value.
|
|
||||||
///
|
|
||||||
/// The replacement is conditional on:
|
|
||||||
/// - there is an existing `LayerKey` record
|
|
||||||
/// - there is no buffered removal for the given `LayerKey`
|
|
||||||
/// - the given closure returns true for the current `Value`
|
|
||||||
///
|
|
||||||
/// The closure is used to compare the latest value (buffered insert, or existing layer)
|
|
||||||
/// against some expectation. This allows to use `Arc::ptr_eq` or similar which would be
|
|
||||||
/// inaccessible via `PartialEq` trait.
|
|
||||||
///
|
|
||||||
/// Returns a `Replacement` value describing the outcome; only the case of
|
|
||||||
/// `Replacement::Replaced` modifies the map and requires a rebuild.
|
|
||||||
pub fn replace<F>(
|
|
||||||
&mut self,
|
|
||||||
layer_key: &LayerKey,
|
|
||||||
new: Value,
|
|
||||||
check_expected: F,
|
|
||||||
) -> Replacement<Value>
|
|
||||||
where
|
|
||||||
F: FnOnce(&Value) -> bool,
|
|
||||||
{
|
|
||||||
let (slot, in_buffered) = match self.buffer.get(layer_key) {
|
|
||||||
Some(inner @ Some(_)) => {
|
|
||||||
// we compare against the buffered version, because there will be a later
|
|
||||||
// rebuild before querying
|
|
||||||
(inner.as_ref(), true)
|
|
||||||
}
|
|
||||||
Some(None) => {
|
|
||||||
// buffer has removal for this key; it will not be equivalent by any check_expected.
|
|
||||||
return Replacement::RemovalBuffered;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// no pending modification for the key, check layers
|
|
||||||
(self.layers.get(layer_key), false)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match slot {
|
|
||||||
Some(existing) if !check_expected(existing) => {
|
|
||||||
// unfortunate clone here, but otherwise the nll borrowck grows the region of
|
|
||||||
// 'a to cover the whole function, and we could not mutate in the other
|
|
||||||
// Some(existing) branch
|
|
||||||
Replacement::Unexpected(existing.clone())
|
|
||||||
}
|
|
||||||
None => Replacement::NotFound,
|
|
||||||
Some(_existing) => {
|
|
||||||
self.insert(layer_key.to_owned(), new);
|
|
||||||
Replacement::Replaced { in_buffered }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rebuild(&mut self) {
|
pub fn rebuild(&mut self) {
|
||||||
// Find the first LSN that needs to be rebuilt
|
// Find the first LSN that needs to be rebuilt
|
||||||
let rebuild_since: u64 = match self.buffer.iter().next() {
|
let rebuild_since: u64 = match self.buffer.iter().next() {
|
||||||
@@ -575,22 +524,6 @@ impl<Value: Clone> BufferedHistoricLayerCoverage<Value> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Outcome of the replace operation.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Replacement<Value> {
|
|
||||||
/// Previous value was replaced with the new value.
|
|
||||||
Replaced {
|
|
||||||
/// Replacement happened for a scheduled insert.
|
|
||||||
in_buffered: bool,
|
|
||||||
},
|
|
||||||
/// Key was not found buffered updates or existing layers.
|
|
||||||
NotFound,
|
|
||||||
/// Key has been scheduled for removal, it was not replaced.
|
|
||||||
RemovalBuffered,
|
|
||||||
/// Previous value was rejected by the closure.
|
|
||||||
Unexpected(Value),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_retroactive_regression_1() {
|
fn test_retroactive_regression_1() {
|
||||||
let mut map = BufferedHistoricLayerCoverage::new();
|
let mut map = BufferedHistoricLayerCoverage::new();
|
||||||
@@ -699,139 +632,3 @@ fn test_retroactive_simple() {
|
|||||||
assert_eq!(version.image_coverage.query(8), Some("Image 4".to_string()));
|
assert_eq!(version.image_coverage.query(8), Some("Image 4".to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_retroactive_replacement() {
|
|
||||||
let mut map = BufferedHistoricLayerCoverage::new();
|
|
||||||
|
|
||||||
let keys = [
|
|
||||||
LayerKey {
|
|
||||||
key: 0..5,
|
|
||||||
lsn: 100..101,
|
|
||||||
is_image: true,
|
|
||||||
},
|
|
||||||
LayerKey {
|
|
||||||
key: 3..9,
|
|
||||||
lsn: 110..111,
|
|
||||||
is_image: true,
|
|
||||||
},
|
|
||||||
LayerKey {
|
|
||||||
key: 4..6,
|
|
||||||
lsn: 120..121,
|
|
||||||
is_image: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let layers = [
|
|
||||||
"Image 1".to_string(),
|
|
||||||
"Image 2".to_string(),
|
|
||||||
"Image 3".to_string(),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (key, layer) in keys.iter().zip(layers.iter()) {
|
|
||||||
map.insert(key.to_owned(), layer.to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
// rebuild is not necessary here, because replace works for both buffered updates and existing
|
|
||||||
// layers.
|
|
||||||
|
|
||||||
for (key, orig_layer) in keys.iter().zip(layers.iter()) {
|
|
||||||
let replacement = format!("Remote {orig_layer}");
|
|
||||||
|
|
||||||
// evict
|
|
||||||
let ret = map.replace(key, replacement.clone(), |l| l == orig_layer);
|
|
||||||
assert!(
|
|
||||||
matches!(ret, Replacement::Replaced { .. }),
|
|
||||||
"replace {orig_layer}: {ret:?}"
|
|
||||||
);
|
|
||||||
map.rebuild();
|
|
||||||
|
|
||||||
let at = key.lsn.end + 1;
|
|
||||||
|
|
||||||
let version = map.get().expect("rebuilt").get_version(at).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
version.image_coverage.query(4).as_deref(),
|
|
||||||
Some(replacement.as_str()),
|
|
||||||
"query for 4 at version {at} after eviction",
|
|
||||||
);
|
|
||||||
|
|
||||||
// download
|
|
||||||
let ret = map.replace(key, orig_layer.clone(), |l| l == &replacement);
|
|
||||||
assert!(
|
|
||||||
matches!(ret, Replacement::Replaced { .. }),
|
|
||||||
"replace {orig_layer} back: {ret:?}"
|
|
||||||
);
|
|
||||||
map.rebuild();
|
|
||||||
let version = map.get().expect("rebuilt").get_version(at).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
version.image_coverage.query(4).as_deref(),
|
|
||||||
Some(orig_layer.as_str()),
|
|
||||||
"query for 4 at version {at} after download",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn missing_key_is_not_inserted_with_replace() {
|
|
||||||
let mut map = BufferedHistoricLayerCoverage::new();
|
|
||||||
let key = LayerKey {
|
|
||||||
key: 0..5,
|
|
||||||
lsn: 100..101,
|
|
||||||
is_image: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
let ret = map.replace(&key, "should not replace", |_| true);
|
|
||||||
assert!(matches!(ret, Replacement::NotFound), "{ret:?}");
|
|
||||||
map.rebuild();
|
|
||||||
assert!(map
|
|
||||||
.get()
|
|
||||||
.expect("no changes to rebuild")
|
|
||||||
.get_version(102)
|
|
||||||
.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn replacing_buffered_insert_and_remove() {
|
|
||||||
let mut map = BufferedHistoricLayerCoverage::new();
|
|
||||||
let key = LayerKey {
|
|
||||||
key: 0..5,
|
|
||||||
lsn: 100..101,
|
|
||||||
is_image: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
map.insert(key.clone(), "Image 1");
|
|
||||||
let ret = map.replace(&key, "Remote Image 1", |&l| l == "Image 1");
|
|
||||||
assert!(
|
|
||||||
matches!(ret, Replacement::Replaced { in_buffered: true }),
|
|
||||||
"{ret:?}"
|
|
||||||
);
|
|
||||||
map.rebuild();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
map.get()
|
|
||||||
.expect("rebuilt")
|
|
||||||
.get_version(102)
|
|
||||||
.unwrap()
|
|
||||||
.image_coverage
|
|
||||||
.query(4),
|
|
||||||
Some("Remote Image 1")
|
|
||||||
);
|
|
||||||
|
|
||||||
map.remove(key.clone());
|
|
||||||
let ret = map.replace(&key, "should not replace", |_| true);
|
|
||||||
assert!(
|
|
||||||
matches!(ret, Replacement::RemovalBuffered),
|
|
||||||
"cannot replace after scheduled remove: {ret:?}"
|
|
||||||
);
|
|
||||||
|
|
||||||
map.rebuild();
|
|
||||||
|
|
||||||
let ret = map.replace(&key, "should not replace", |_| true);
|
|
||||||
assert!(
|
|
||||||
matches!(ret, Replacement::NotFound),
|
|
||||||
"cannot replace after remove + rebuild: {ret:?}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let at_version = map.get().expect("rebuilt").get_version(102);
|
|
||||||
assert!(at_version.is_none());
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -862,10 +862,8 @@ impl RemoteTimelineClient {
|
|||||||
"Found {} files not bound to index_file.json, proceeding with their deletion",
|
"Found {} files not bound to index_file.json, proceeding with their deletion",
|
||||||
remaining.len()
|
remaining.len()
|
||||||
);
|
);
|
||||||
for file in remaining {
|
warn!("About to remove {} files", remaining.len());
|
||||||
warn!("Removing {}", file.object_name().unwrap_or_default());
|
self.storage_impl.delete_objects(&remaining).await?;
|
||||||
self.storage_impl.delete(&file).await?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let index_file_path = timeline_storage_path.join(Path::new(IndexPart::FILE_NAME));
|
let index_file_path = timeline_storage_path.join(Path::new(IndexPart::FILE_NAME));
|
||||||
|
|||||||
@@ -176,13 +176,10 @@ impl LayerAccessStats {
|
|||||||
/// Create an empty stats object and record a [`LayerLoad`] event with the given residence status.
|
/// Create an empty stats object and record a [`LayerLoad`] event with the given residence status.
|
||||||
///
|
///
|
||||||
/// See [`record_residence_event`] for why you need to do this while holding the layer map lock.
|
/// See [`record_residence_event`] for why you need to do this while holding the layer map lock.
|
||||||
pub(crate) fn for_loading_layer<L>(
|
pub(crate) fn for_loading_layer(
|
||||||
layer_map_lock_held_witness: &BatchedUpdates<'_, L>,
|
layer_map_lock_held_witness: &BatchedUpdates<'_>,
|
||||||
status: LayerResidenceStatus,
|
status: LayerResidenceStatus,
|
||||||
) -> Self
|
) -> Self {
|
||||||
where
|
|
||||||
L: ?Sized + Layer,
|
|
||||||
{
|
|
||||||
let new = LayerAccessStats(Mutex::new(LayerAccessStatsLocked::default()));
|
let new = LayerAccessStats(Mutex::new(LayerAccessStatsLocked::default()));
|
||||||
new.record_residence_event(
|
new.record_residence_event(
|
||||||
layer_map_lock_held_witness,
|
layer_map_lock_held_witness,
|
||||||
@@ -197,14 +194,11 @@ impl LayerAccessStats {
|
|||||||
/// The `new_status` is not recorded in `self`.
|
/// The `new_status` is not recorded in `self`.
|
||||||
///
|
///
|
||||||
/// See [`record_residence_event`] for why you need to do this while holding the layer map lock.
|
/// See [`record_residence_event`] for why you need to do this while holding the layer map lock.
|
||||||
pub(crate) fn clone_for_residence_change<L>(
|
pub(crate) fn clone_for_residence_change(
|
||||||
&self,
|
&self,
|
||||||
layer_map_lock_held_witness: &BatchedUpdates<'_, L>,
|
layer_map_lock_held_witness: &BatchedUpdates<'_>,
|
||||||
new_status: LayerResidenceStatus,
|
new_status: LayerResidenceStatus,
|
||||||
) -> LayerAccessStats
|
) -> LayerAccessStats {
|
||||||
where
|
|
||||||
L: ?Sized + Layer,
|
|
||||||
{
|
|
||||||
let clone = {
|
let clone = {
|
||||||
let inner = self.0.lock().unwrap();
|
let inner = self.0.lock().unwrap();
|
||||||
inner.clone()
|
inner.clone()
|
||||||
@@ -232,14 +226,12 @@ impl LayerAccessStats {
|
|||||||
/// - Compact: Grab layer map lock, add the new L1 to layer map and remove the L0s, release layer map lock.
|
/// - Compact: Grab layer map lock, add the new L1 to layer map and remove the L0s, release layer map lock.
|
||||||
/// - Eviction: observes the new L1 layer whose only activity timestamp is the LayerCreate event.
|
/// - Eviction: observes the new L1 layer whose only activity timestamp is the LayerCreate event.
|
||||||
///
|
///
|
||||||
pub(crate) fn record_residence_event<L>(
|
pub(crate) fn record_residence_event(
|
||||||
&self,
|
&self,
|
||||||
_layer_map_lock_held_witness: &BatchedUpdates<'_, L>,
|
_layer_map_lock_held_witness: &BatchedUpdates<'_>,
|
||||||
status: LayerResidenceStatus,
|
status: LayerResidenceStatus,
|
||||||
reason: LayerResidenceEventReason,
|
reason: LayerResidenceEventReason,
|
||||||
) where
|
) {
|
||||||
L: ?Sized + Layer,
|
|
||||||
{
|
|
||||||
let mut locked = self.0.lock().unwrap();
|
let mut locked = self.0.lock().unwrap();
|
||||||
locked.iter_mut().for_each(|inner| {
|
locked.iter_mut().for_each(|inner| {
|
||||||
inner
|
inner
|
||||||
@@ -473,94 +465,125 @@ pub fn downcast_remote_layer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Holds metadata about a layer without any content. Used mostly for testing.
|
pub mod tests {
|
||||||
///
|
use super::*;
|
||||||
/// To use filenames as fixtures, parse them as [`LayerFileName`] then convert from that to a
|
|
||||||
/// LayerDescriptor.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct LayerDescriptor {
|
|
||||||
pub key: Range<Key>,
|
|
||||||
pub lsn: Range<Lsn>,
|
|
||||||
pub is_incremental: bool,
|
|
||||||
pub short_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayerDescriptor {
|
/// Holds metadata about a layer without any content. Used mostly for testing.
|
||||||
/// `LayerDescriptor` is only used for testing purpose so it does not matter whether it is image / delta,
|
///
|
||||||
/// and the tenant / timeline id does not matter.
|
/// To use filenames as fixtures, parse them as [`LayerFileName`] then convert from that to a
|
||||||
pub fn get_persistent_layer_desc(&self) -> PersistentLayerDesc {
|
/// LayerDescriptor.
|
||||||
PersistentLayerDesc::new_delta(
|
#[derive(Clone, Debug)]
|
||||||
TenantId::from_array([0; 16]),
|
pub struct LayerDescriptor {
|
||||||
TimelineId::from_array([0; 16]),
|
base: PersistentLayerDesc,
|
||||||
self.key.clone(),
|
|
||||||
self.lsn.clone(),
|
|
||||||
233,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Layer for LayerDescriptor {
|
|
||||||
fn get_key_range(&self) -> Range<Key> {
|
|
||||||
self.key.clone()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_lsn_range(&self) -> Range<Lsn> {
|
impl From<PersistentLayerDesc> for LayerDescriptor {
|
||||||
self.lsn.clone()
|
fn from(base: PersistentLayerDesc) -> Self {
|
||||||
}
|
Self { base }
|
||||||
|
|
||||||
fn is_incremental(&self) -> bool {
|
|
||||||
self.is_incremental
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_value_reconstruct_data(
|
|
||||||
&self,
|
|
||||||
_key: Key,
|
|
||||||
_lsn_range: Range<Lsn>,
|
|
||||||
_reconstruct_data: &mut ValueReconstructState,
|
|
||||||
_ctx: &RequestContext,
|
|
||||||
) -> Result<ValueReconstructResult> {
|
|
||||||
todo!("This method shouldn't be part of the Layer trait")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn short_id(&self) -> String {
|
|
||||||
self.short_id.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dump(&self, _verbose: bool, _ctx: &RequestContext) -> Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DeltaFileName> for LayerDescriptor {
|
|
||||||
fn from(value: DeltaFileName) -> Self {
|
|
||||||
let short_id = value.to_string();
|
|
||||||
LayerDescriptor {
|
|
||||||
key: value.key_range,
|
|
||||||
lsn: value.lsn_range,
|
|
||||||
is_incremental: true,
|
|
||||||
short_id,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ImageFileName> for LayerDescriptor {
|
impl Layer for LayerDescriptor {
|
||||||
fn from(value: ImageFileName) -> Self {
|
fn get_value_reconstruct_data(
|
||||||
let short_id = value.to_string();
|
&self,
|
||||||
let lsn = value.lsn_as_range();
|
_key: Key,
|
||||||
LayerDescriptor {
|
_lsn_range: Range<Lsn>,
|
||||||
key: value.key_range,
|
_reconstruct_data: &mut ValueReconstructState,
|
||||||
lsn,
|
_ctx: &RequestContext,
|
||||||
is_incremental: false,
|
) -> Result<ValueReconstructResult> {
|
||||||
short_id,
|
todo!("This method shouldn't be part of the Layer trait")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dump(&self, _verbose: bool, _ctx: &RequestContext) -> Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn get_key_range(&self) -> Range<Key> {
|
||||||
|
self.layer_desc().key_range.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn get_lsn_range(&self) -> Range<Lsn> {
|
||||||
|
self.layer_desc().lsn_range.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn is_incremental(&self) -> bool {
|
||||||
|
self.layer_desc().is_incremental
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boilerplate to implement the Layer trait, always use layer_desc for persistent layers.
|
||||||
|
fn short_id(&self) -> String {
|
||||||
|
self.layer_desc().short_id()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl From<LayerFileName> for LayerDescriptor {
|
impl PersistentLayer for LayerDescriptor {
|
||||||
fn from(value: LayerFileName) -> Self {
|
fn layer_desc(&self) -> &PersistentLayerDesc {
|
||||||
match value {
|
&self.base
|
||||||
LayerFileName::Delta(d) => Self::from(d),
|
}
|
||||||
LayerFileName::Image(i) => Self::from(i),
|
|
||||||
|
fn local_path(&self) -> Option<PathBuf> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iter(&self, _: &RequestContext) -> Result<LayerIter<'_>> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_iter(&self, _: &RequestContext) -> Result<LayerKeyIter<'_>> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_resident_layer_file(&self) -> Result<()> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn info(&self, _: LayerAccessStatsReset) -> HistoricLayerInfo {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn access_stats(&self) -> &LayerAccessStats {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DeltaFileName> for LayerDescriptor {
|
||||||
|
fn from(value: DeltaFileName) -> Self {
|
||||||
|
LayerDescriptor {
|
||||||
|
base: PersistentLayerDesc::new_delta(
|
||||||
|
TenantId::from_array([0; 16]),
|
||||||
|
TimelineId::from_array([0; 16]),
|
||||||
|
value.key_range,
|
||||||
|
value.lsn_range,
|
||||||
|
233,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ImageFileName> for LayerDescriptor {
|
||||||
|
fn from(value: ImageFileName) -> Self {
|
||||||
|
LayerDescriptor {
|
||||||
|
base: PersistentLayerDesc::new_img(
|
||||||
|
TenantId::from_array([0; 16]),
|
||||||
|
TimelineId::from_array([0; 16]),
|
||||||
|
value.key_range,
|
||||||
|
value.lsn,
|
||||||
|
false,
|
||||||
|
233,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LayerFileName> for LayerDescriptor {
|
||||||
|
fn from(value: LayerFileName) -> Self {
|
||||||
|
match value {
|
||||||
|
LayerFileName::Delta(d) => Self::from(d),
|
||||||
|
LayerFileName::Image(i) => Self::from(i),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,15 +218,12 @@ impl RemoteLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a Layer struct representing this layer, after it has been downloaded.
|
/// Create a Layer struct representing this layer, after it has been downloaded.
|
||||||
pub fn create_downloaded_layer<L>(
|
pub fn create_downloaded_layer(
|
||||||
&self,
|
&self,
|
||||||
layer_map_lock_held_witness: &BatchedUpdates<'_, L>,
|
layer_map_lock_held_witness: &BatchedUpdates<'_>,
|
||||||
conf: &'static PageServerConf,
|
conf: &'static PageServerConf,
|
||||||
file_size: u64,
|
file_size: u64,
|
||||||
) -> Arc<dyn PersistentLayer>
|
) -> Arc<dyn PersistentLayer> {
|
||||||
where
|
|
||||||
L: ?Sized + Layer,
|
|
||||||
{
|
|
||||||
if self.desc.is_delta {
|
if self.desc.is_delta {
|
||||||
let fname = self.desc.delta_file_name();
|
let fname = self.desc.delta_file_name();
|
||||||
Arc::new(DeltaLayer::new(
|
Arc::new(DeltaLayer::new(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -197,9 +197,11 @@ impl Timeline {
|
|||||||
// We don't want to hold the layer map lock during eviction.
|
// We don't want to hold the layer map lock during eviction.
|
||||||
// So, we just need to deal with this.
|
// So, we just need to deal with this.
|
||||||
let candidates: Vec<Arc<dyn PersistentLayer>> = {
|
let candidates: Vec<Arc<dyn PersistentLayer>> = {
|
||||||
let layers = self.layers.read().await;
|
let guard = self.layers.read().await;
|
||||||
|
let (layers, mapping) = &*guard;
|
||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
for hist_layer in layers.iter_historic_layers() {
|
for hist_layer in layers.iter_historic_layers() {
|
||||||
|
let hist_layer = mapping.get_from_desc(&hist_layer);
|
||||||
if hist_layer.is_remote_layer() {
|
if hist_layer.is_remote_layer() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
MODULE_big = neon
|
MODULE_big = neon
|
||||||
OBJS = \
|
OBJS = \
|
||||||
$(WIN32RES) \
|
$(WIN32RES) \
|
||||||
|
extension_server.o \
|
||||||
file_cache.o \
|
file_cache.o \
|
||||||
libpagestore.o \
|
libpagestore.o \
|
||||||
libpqwalproposer.o \
|
libpqwalproposer.o \
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
#include "port.h"
|
#include "port.h"
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
#include "utils/jsonb.h"
|
#include "utils/jsonb.h"
|
||||||
|
#include "libpq/crypt.h"
|
||||||
|
|
||||||
static ProcessUtility_hook_type PreviousProcessUtilityHook = NULL;
|
static ProcessUtility_hook_type PreviousProcessUtilityHook = NULL;
|
||||||
|
|
||||||
@@ -161,7 +162,22 @@ ConstructDeltaMessage()
|
|||||||
PushKeyValue(&state, "name", entry->name);
|
PushKeyValue(&state, "name", entry->name);
|
||||||
if (entry->password)
|
if (entry->password)
|
||||||
{
|
{
|
||||||
|
#if PG_MAJORVERSION_NUM == 14
|
||||||
|
char *logdetail;
|
||||||
|
#else
|
||||||
|
const char *logdetail;
|
||||||
|
#endif
|
||||||
PushKeyValue(&state, "password", (char *) entry->password);
|
PushKeyValue(&state, "password", (char *) entry->password);
|
||||||
|
char *encrypted_password = get_role_password(entry->name, &logdetail);
|
||||||
|
|
||||||
|
if (encrypted_password)
|
||||||
|
{
|
||||||
|
PushKeyValue(&state, "encrypted_password", encrypted_password);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
elog(ERROR, "Failed to get encrypted password: %s", logdetail);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (entry->old_name[0] != '\0')
|
if (entry->old_name[0] != '\0')
|
||||||
{
|
{
|
||||||
|
|||||||
104
pgxn/neon/extension_server.c
Normal file
104
pgxn/neon/extension_server.c
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
|
||||||
|
/*-------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* extension_server.c
|
||||||
|
* Request compute_ctl to download extension files.
|
||||||
|
*
|
||||||
|
* IDENTIFICATION
|
||||||
|
* contrib/neon/extension_server.c
|
||||||
|
*
|
||||||
|
*-------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
#include "postgres.h"
|
||||||
|
#include "tcop/pquery.h"
|
||||||
|
#include "tcop/utility.h"
|
||||||
|
#include "access/xact.h"
|
||||||
|
#include "utils/hsearch.h"
|
||||||
|
#include "utils/memutils.h"
|
||||||
|
#include "commands/defrem.h"
|
||||||
|
#include "miscadmin.h"
|
||||||
|
#include "utils/acl.h"
|
||||||
|
#include "fmgr.h"
|
||||||
|
#include "utils/guc.h"
|
||||||
|
#include "port.h"
|
||||||
|
#include "fmgr.h"
|
||||||
|
|
||||||
|
#include <curl/curl.h>
|
||||||
|
|
||||||
|
static int extension_server_port = 0;
|
||||||
|
|
||||||
|
static download_extension_file_hook_type prev_download_extension_file_hook = NULL;
|
||||||
|
|
||||||
|
// to download all SQL (and data) files for an extension:
|
||||||
|
// curl -X POST http://localhost:8080/extension_server/postgis
|
||||||
|
// it covers two possible extension files layouts:
|
||||||
|
// 1. extension_name--version--platform.sql
|
||||||
|
// 2. extension_name/extension_name--version.sql
|
||||||
|
// extension_name/extra_files.csv
|
||||||
|
//
|
||||||
|
// to download specific library file:
|
||||||
|
// curl -X POST http://localhost:8080/extension_server/postgis-3.so?is_library=true
|
||||||
|
static bool
|
||||||
|
neon_download_extension_file_http(const char *filename, bool is_library)
|
||||||
|
{
|
||||||
|
CURL *curl;
|
||||||
|
CURLcode res;
|
||||||
|
char *compute_ctl_url;
|
||||||
|
char *postdata;
|
||||||
|
bool ret = false;
|
||||||
|
|
||||||
|
if ((curl = curl_easy_init()) == NULL)
|
||||||
|
{
|
||||||
|
elog(ERROR, "Failed to initialize curl handle");
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_ctl_url = psprintf("http://localhost:%d/extension_server/%s%s",
|
||||||
|
extension_server_port, filename, is_library ? "?is_library=true" : "");
|
||||||
|
|
||||||
|
elog(LOG, "Sending request to compute_ctl: %s", compute_ctl_url);
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, compute_ctl_url);
|
||||||
|
// NOTE: 15L may be insufficient time for large extensions like postgis
|
||||||
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L /* seconds */);
|
||||||
|
|
||||||
|
if (curl)
|
||||||
|
{
|
||||||
|
/* Perform the request, res will get the return code */
|
||||||
|
res = curl_easy_perform(curl);
|
||||||
|
/* Check for errors */
|
||||||
|
if (res == CURLE_OK)
|
||||||
|
{
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Don't error here because postgres will try to find the file
|
||||||
|
// and will fail with some proper error message if it's not found.
|
||||||
|
elog(WARNING, "neon_download_extension_file_http failed: %s\n", curl_easy_strerror(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* always cleanup */
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pg_init_extension_server()
|
||||||
|
{
|
||||||
|
// Port to connect to compute_ctl on localhost
|
||||||
|
// to request extension files.
|
||||||
|
DefineCustomIntVariable("neon.extension_server_port",
|
||||||
|
"connection string to the compute_ctl",
|
||||||
|
NULL,
|
||||||
|
&extension_server_port,
|
||||||
|
0, 0, INT_MAX,
|
||||||
|
PGC_POSTMASTER,
|
||||||
|
0, /* no flags required */
|
||||||
|
NULL, NULL, NULL);
|
||||||
|
|
||||||
|
// set download_extension_file_hook
|
||||||
|
prev_download_extension_file_hook = download_extension_file_hook;
|
||||||
|
download_extension_file_hook = neon_download_extension_file_http;
|
||||||
|
}
|
||||||
1
pgxn/neon/extension_server.h
Normal file
1
pgxn/neon/extension_server.h
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -190,7 +190,7 @@ lfc_change_limit_hook(int newval, void *extra)
|
|||||||
hash_search(lfc_hash, &victim->key, HASH_REMOVE, NULL);
|
hash_search(lfc_hash, &victim->key, HASH_REMOVE, NULL);
|
||||||
lfc_ctl->used -= 1;
|
lfc_ctl->used -= 1;
|
||||||
}
|
}
|
||||||
elog(LOG, "set local file cache limit to %d", new_size);
|
elog(DEBUG1, "set local file cache limit to %d", new_size);
|
||||||
LWLockRelease(lfc_lock);
|
LWLockRelease(lfc_lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,11 @@ _PG_init(void)
|
|||||||
{
|
{
|
||||||
pg_init_libpagestore();
|
pg_init_libpagestore();
|
||||||
pg_init_walproposer();
|
pg_init_walproposer();
|
||||||
|
|
||||||
InitControlPlaneConnector();
|
InitControlPlaneConnector();
|
||||||
|
|
||||||
|
pg_init_extension_server();
|
||||||
|
|
||||||
// Important: This must happen after other parts of the extension
|
// Important: This must happen after other parts of the extension
|
||||||
// are loaded, otherwise any settings to GUCs that were set before
|
// are loaded, otherwise any settings to GUCs that were set before
|
||||||
// the extension was loaded will be removed.
|
// the extension was loaded will be removed.
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ extern char *neon_tenant;
|
|||||||
extern void pg_init_libpagestore(void);
|
extern void pg_init_libpagestore(void);
|
||||||
extern void pg_init_walproposer(void);
|
extern void pg_init_walproposer(void);
|
||||||
|
|
||||||
|
extern void pg_init_extension_server(void);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Returns true if we shouldn't do REDO on that block in record indicated by
|
* Returns true if we shouldn't do REDO on that block in record indicated by
|
||||||
* block_id; false otherwise.
|
* block_id; false otherwise.
|
||||||
|
|||||||
@@ -2675,7 +2675,6 @@ bool
|
|||||||
neon_redo_read_buffer_filter(XLogReaderState *record, uint8 block_id)
|
neon_redo_read_buffer_filter(XLogReaderState *record, uint8 block_id)
|
||||||
{
|
{
|
||||||
XLogRecPtr end_recptr = record->EndRecPtr;
|
XLogRecPtr end_recptr = record->EndRecPtr;
|
||||||
XLogRecPtr prev_end_recptr = record->ReadRecPtr - 1;
|
|
||||||
RelFileNode rnode;
|
RelFileNode rnode;
|
||||||
ForkNumber forknum;
|
ForkNumber forknum;
|
||||||
BlockNumber blkno;
|
BlockNumber blkno;
|
||||||
@@ -2719,16 +2718,15 @@ neon_redo_read_buffer_filter(XLogReaderState *record, uint8 block_id)
|
|||||||
|
|
||||||
no_redo_needed = buffer < 0;
|
no_redo_needed = buffer < 0;
|
||||||
|
|
||||||
/* we don't have the buffer in memory, update lwLsn past this record */
|
/* In both cases st lwlsn past this WAL record */
|
||||||
|
SetLastWrittenLSNForBlock(end_recptr, rnode, forknum, blkno);
|
||||||
|
|
||||||
|
/* we don't have the buffer in memory, update lwLsn past this record,
|
||||||
|
* also evict page fro file cache
|
||||||
|
*/
|
||||||
if (no_redo_needed)
|
if (no_redo_needed)
|
||||||
{
|
|
||||||
SetLastWrittenLSNForBlock(end_recptr, rnode, forknum, blkno);
|
|
||||||
lfc_evict(rnode, forknum, blkno);
|
lfc_evict(rnode, forknum, blkno);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SetLastWrittenLSNForBlock(prev_end_recptr, rnode, forknum, blkno);
|
|
||||||
}
|
|
||||||
|
|
||||||
LWLockRelease(partitionLock);
|
LWLockRelease(partitionLock);
|
||||||
|
|
||||||
@@ -2736,7 +2734,10 @@ neon_redo_read_buffer_filter(XLogReaderState *record, uint8 block_id)
|
|||||||
if (get_cached_relsize(rnode, forknum, &relsize))
|
if (get_cached_relsize(rnode, forknum, &relsize))
|
||||||
{
|
{
|
||||||
if (relsize < blkno + 1)
|
if (relsize < blkno + 1)
|
||||||
|
{
|
||||||
update_cached_relsize(rnode, forknum, blkno + 1);
|
update_cached_relsize(rnode, forknum, blkno + 1);
|
||||||
|
SetLastWrittenLSNForRelation(end_recptr, rnode, forknum);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -2768,6 +2769,7 @@ neon_redo_read_buffer_filter(XLogReaderState *record, uint8 block_id)
|
|||||||
Assert(nbresponse->n_blocks > blkno);
|
Assert(nbresponse->n_blocks > blkno);
|
||||||
|
|
||||||
set_cached_relsize(rnode, forknum, nbresponse->n_blocks);
|
set_cached_relsize(rnode, forknum, nbresponse->n_blocks);
|
||||||
|
SetLastWrittenLSNForRelation(end_recptr, rnode, forknum);
|
||||||
|
|
||||||
elog(SmgrTrace, "Set length to %d", nbresponse->n_blocks);
|
elog(SmgrTrace, "Set length to %d", nbresponse->n_blocks);
|
||||||
}
|
}
|
||||||
|
|||||||
123
poetry.lock
generated
123
poetry.lock
generated
@@ -1654,71 +1654,74 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "psycopg2-binary"
|
name = "psycopg2-binary"
|
||||||
version = "2.9.3"
|
version = "2.9.6"
|
||||||
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
files = [
|
files = [
|
||||||
{file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"},
|
{file = "psycopg2-binary-2.9.6.tar.gz", hash = "sha256:1f64dcfb8f6e0c014c7f55e51c9759f024f70ea572fbdef123f85318c297947c"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"},
|
{file = "psycopg2_binary-2.9.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d26e0342183c762de3276cca7a530d574d4e25121ca7d6e4a98e4f05cb8e4df7"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f2534ab7dc7e776a263b463a16e189eb30e85ec9bbe1bff9e78dae802608932"},
|
{file = "psycopg2_binary-2.9.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c48d8f2db17f27d41fb0e2ecd703ea41984ee19362cbce52c097963b3a1b4365"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"},
|
{file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe9dc0a884a8848075e576c1de0290d85a533a9f6e9c4e564f19adf8f6e54a7"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"},
|
{file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a76e027f87753f9bd1ab5f7c9cb8c7628d1077ef927f5e2446477153a602f2c"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"},
|
{file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6460c7a99fc939b849431f1e73e013d54aa54293f30f1109019c56a0b2b2ec2f"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9"},
|
{file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae102a98c547ee2288637af07393dd33f440c25e5cd79556b04e3fca13325e5f"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4"},
|
{file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9972aad21f965599ed0106f65334230ce826e5ae69fda7cbd688d24fa922415e"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa"},
|
{file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7a40c00dbe17c0af5bdd55aafd6ff6679f94a9be9513a4c7e071baf3d7d22a70"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e"},
|
{file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:cacbdc5839bdff804dfebc058fe25684cae322987f7a38b0168bc1b2df703fb1"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42"},
|
{file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7f0438fa20fb6c7e202863e0d5ab02c246d35efb1d164e052f2f3bfe2b152bd0"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-win32.whl", hash = "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029"},
|
{file = "psycopg2_binary-2.9.6-cp310-cp310-win32.whl", hash = "sha256:b6c8288bb8a84b47e07013bb4850f50538aa913d487579e1921724631d02ea1b"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53"},
|
{file = "psycopg2_binary-2.9.6-cp310-cp310-win_amd64.whl", hash = "sha256:61b047a0537bbc3afae10f134dc6393823882eb263088c271331602b672e52e9"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e"},
|
{file = "psycopg2_binary-2.9.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:964b4dfb7c1c1965ac4c1978b0f755cc4bd698e8aa2b7667c575fb5f04ebe06b"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1"},
|
{file = "psycopg2_binary-2.9.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afe64e9b8ea66866a771996f6ff14447e8082ea26e675a295ad3bdbffdd72afb"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460"},
|
{file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15e2ee79e7cf29582ef770de7dab3d286431b01c3bb598f8e05e09601b890081"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281"},
|
{file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfa74c903a3c1f0d9b1c7e7b53ed2d929a4910e272add6700c38f365a6002820"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f"},
|
{file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b83456c2d4979e08ff56180a76429263ea254c3f6552cd14ada95cff1dec9bb8"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca"},
|
{file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0645376d399bfd64da57148694d78e1f431b1e1ee1054872a5713125681cf1be"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af"},
|
{file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e99e34c82309dd78959ba3c1590975b5d3c862d6f279f843d47d26ff89d7d7e1"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57"},
|
{file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4ea29fc3ad9d91162c52b578f211ff1c931d8a38e1f58e684c45aa470adf19e2"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d"},
|
{file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4ac30da8b4f57187dbf449294d23b808f8f53cad6b1fc3623fa8a6c11d176dd0"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b"},
|
{file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e78e6e2a00c223e164c417628572a90093c031ed724492c763721c2e0bc2a8df"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2"},
|
{file = "psycopg2_binary-2.9.6-cp311-cp311-win32.whl", hash = "sha256:1876843d8e31c89c399e31b97d4b9725a3575bb9c2af92038464231ec40f9edb"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1"},
|
{file = "psycopg2_binary-2.9.6-cp311-cp311-win_amd64.whl", hash = "sha256:b4b24f75d16a89cc6b4cdff0eb6a910a966ecd476d1e73f7ce5985ff1328e9a6"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33"},
|
{file = "psycopg2_binary-2.9.6-cp36-cp36m-win32.whl", hash = "sha256:498807b927ca2510baea1b05cc91d7da4718a0f53cb766c154c417a39f1820a0"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae"},
|
{file = "psycopg2_binary-2.9.6-cp36-cp36m-win_amd64.whl", hash = "sha256:0d236c2825fa656a2d98bbb0e52370a2e852e5a0ec45fc4f402977313329174d"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094"},
|
{file = "psycopg2_binary-2.9.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:34b9ccdf210cbbb1303c7c4db2905fa0319391bd5904d32689e6dd5c963d2ea8"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063"},
|
{file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d2222e61f313c4848ff05353653bf5f5cf6ce34df540e4274516880d9c3763"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef"},
|
{file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30637a20623e2a2eacc420059be11527f4458ef54352d870b8181a4c3020ae6b"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb"},
|
{file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8122cfc7cae0da9a3077216528b8bb3629c43b25053284cc868744bfe71eb141"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232"},
|
{file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38601cbbfe600362c43714482f43b7c110b20cb0f8172422c616b09b85a750c5"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554"},
|
{file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c7e62ab8b332147a7593a385d4f368874d5fe4ad4e341770d4983442d89603e3"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"},
|
{file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2ab652e729ff4ad76d400df2624d223d6e265ef81bb8aa17fbd63607878ecbee"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"},
|
{file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c83a74b68270028dc8ee74d38ecfaf9c90eed23c8959fca95bd703d25b82c88e"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"},
|
{file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d4e6036decf4b72d6425d5b29bbd3e8f0ff1059cda7ac7b96d6ac5ed34ffbacd"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e6aa71ae45f952a2205377773e76f4e3f27951df38e69a4c95440c779e013560"},
|
{file = "psycopg2_binary-2.9.6-cp37-cp37m-win32.whl", hash = "sha256:a8c28fd40a4226b4a84bdf2d2b5b37d2c7bd49486b5adcc200e8c7ec991dfa7e"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"},
|
{file = "psycopg2_binary-2.9.6-cp37-cp37m-win_amd64.whl", hash = "sha256:51537e3d299be0db9137b321dfb6a5022caaab275775680e0c3d281feefaca6b"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"},
|
{file = "psycopg2_binary-2.9.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4499e0a83b7b7edcb8dabecbd8501d0d3a5ef66457200f77bde3d210d5debb"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"},
|
{file = "psycopg2_binary-2.9.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7e13a5a2c01151f1208d5207e42f33ba86d561b7a89fca67c700b9486a06d0e2"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c"},
|
{file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e0f754d27fddcfd74006455b6e04e6705d6c31a612ec69ddc040a5468e44b4e"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"},
|
{file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d57c3fd55d9058645d26ae37d76e61156a27722097229d32a9e73ed54819982a"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004"},
|
{file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71f14375d6f73b62800530b581aed3ada394039877818b2d5f7fc77e3bb6894d"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1"},
|
{file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:441cc2f8869a4f0f4bb408475e5ae0ee1f3b55b33f350406150277f7f35384fc"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307"},
|
{file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:65bee1e49fa6f9cf327ce0e01c4c10f39165ee76d35c846ade7cb0ec6683e303"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"},
|
{file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:af335bac6b666cc6aea16f11d486c3b794029d9df029967f9938a4bed59b6a19"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"},
|
{file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cfec476887aa231b8548ece2e06d28edc87c1397ebd83922299af2e051cf2827"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"},
|
{file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:65c07febd1936d63bfde78948b76cd4c2a411572a44ac50719ead41947d0f26b"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b3a24a1982ae56461cc24f6680604fffa2c1b818e9dc55680da038792e004d18"},
|
{file = "psycopg2_binary-2.9.6-cp38-cp38-win32.whl", hash = "sha256:4dfb4be774c4436a4526d0c554af0cc2e02082c38303852a36f6456ece7b3503"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"},
|
{file = "psycopg2_binary-2.9.6-cp38-cp38-win_amd64.whl", hash = "sha256:02c6e3cf3439e213e4ee930308dc122d6fb4d4bea9aef4a12535fbd605d1a2fe"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"},
|
{file = "psycopg2_binary-2.9.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e9182eb20f41417ea1dd8e8f7888c4d7c6e805f8a7c98c1081778a3da2bee3e4"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"},
|
{file = "psycopg2_binary-2.9.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8a6979cf527e2603d349a91060f428bcb135aea2be3201dff794813256c274f1"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4"},
|
{file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8338a271cb71d8da40b023a35d9c1e919eba6cbd8fa20a54b748a332c355d896"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35"},
|
{file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3ed340d2b858d6e6fb5083f87c09996506af483227735de6964a6100b4e6a54"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb"},
|
{file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f81e65376e52f03422e1fb475c9514185669943798ed019ac50410fb4c4df232"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7"},
|
{file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfb13af3c5dd3a9588000910178de17010ebcccd37b4f9794b00595e3a8ddad3"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8"},
|
{file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4c727b597c6444a16e9119386b59388f8a424223302d0c06c676ec8b4bc1f963"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"},
|
{file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4d67fbdaf177da06374473ef6f7ed8cc0a9dc640b01abfe9e8a2ccb1b1402c1f"},
|
||||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"},
|
{file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0892ef645c2fabb0c75ec32d79f4252542d0caec1d5d949630e7d242ca4681a3"},
|
||||||
|
{file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:02c0f3757a4300cf379eb49f543fb7ac527fb00144d39246ee40e1df684ab514"},
|
||||||
|
{file = "psycopg2_binary-2.9.6-cp39-cp39-win32.whl", hash = "sha256:c3dba7dab16709a33a847e5cd756767271697041fbe3fe97c215b1fc1f5c9848"},
|
||||||
|
{file = "psycopg2_binary-2.9.6-cp39-cp39-win_amd64.whl", hash = "sha256:f6a88f384335bb27812293fdb11ac6aee2ca3f51d3c7820fe03de0a304ab6249"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ license.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
atty.workspace = true
|
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
bstr.workspace = true
|
bstr.workspace = true
|
||||||
bytes = { workspace = true, features = ["serde"] }
|
bytes = { workspace = true, features = ["serde"] }
|
||||||
@@ -38,6 +37,7 @@ rand.workspace = true
|
|||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
reqwest = { workspace = true, features = ["json"] }
|
reqwest = { workspace = true, features = ["json"] }
|
||||||
reqwest-middleware.workspace = true
|
reqwest-middleware.workspace = true
|
||||||
|
reqwest-retry.workspace = true
|
||||||
reqwest-tracing.workspace = true
|
reqwest-tracing.workspace = true
|
||||||
routerify.workspace = true
|
routerify.workspace = true
|
||||||
rustls-pemfile.workspace = true
|
rustls-pemfile.workspace = true
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use postgres_backend::{self, AuthType, PostgresBackend, PostgresBackendTCP, Quer
|
|||||||
use pq_proto::{BeMessage, SINGLE_COL_ROWDESC};
|
use pq_proto::{BeMessage, SINGLE_COL_ROWDESC};
|
||||||
use std::future;
|
use std::future;
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tracing::{error, info, info_span};
|
use tracing::{error, info, info_span, Instrument};
|
||||||
|
|
||||||
static CPLANE_WAITERS: Lazy<Waiters<ComputeReady>> = Lazy::new(Default::default);
|
static CPLANE_WAITERS: Lazy<Waiters<ComputeReady>> = Lazy::new(Default::default);
|
||||||
|
|
||||||
@@ -44,19 +44,30 @@ pub async fn task_main(listener: TcpListener) -> anyhow::Result<()> {
|
|||||||
.set_nodelay(true)
|
.set_nodelay(true)
|
||||||
.context("failed to set client socket option")?;
|
.context("failed to set client socket option")?;
|
||||||
|
|
||||||
tokio::task::spawn(async move {
|
let span = info_span!("mgmt", peer = %peer_addr);
|
||||||
let span = info_span!("mgmt", peer = %peer_addr);
|
|
||||||
let _enter = span.enter();
|
|
||||||
|
|
||||||
info!("started a new console management API thread");
|
tokio::task::spawn(
|
||||||
scopeguard::defer! {
|
async move {
|
||||||
info!("console management API thread is about to finish");
|
info!("serving a new console management API connection");
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = handle_connection(socket).await {
|
// these might be long running connections, have a separate logging for cancelling
|
||||||
error!("thread failed with an error: {e}");
|
// on shutdown and other ways of stopping.
|
||||||
|
let cancelled = scopeguard::guard(tracing::Span::current(), |span| {
|
||||||
|
let _e = span.entered();
|
||||||
|
info!("console management API task cancelled");
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(e) = handle_connection(socket).await {
|
||||||
|
error!("serving failed with an error: {e}");
|
||||||
|
} else {
|
||||||
|
info!("serving completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// we can no longer get dropped
|
||||||
|
scopeguard::ScopeGuard::into_inner(cancelled);
|
||||||
}
|
}
|
||||||
});
|
.instrument(span),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,14 +88,14 @@ impl postgres_backend::Handler<tokio::net::TcpStream> for MgmtHandler {
|
|||||||
pgb: &mut PostgresBackendTCP,
|
pgb: &mut PostgresBackendTCP,
|
||||||
query: &str,
|
query: &str,
|
||||||
) -> Result<(), QueryError> {
|
) -> Result<(), QueryError> {
|
||||||
try_process_query(pgb, query).await.map_err(|e| {
|
try_process_query(pgb, query).map_err(|e| {
|
||||||
error!("failed to process response: {e:?}");
|
error!("failed to process response: {e:?}");
|
||||||
e
|
e
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn try_process_query(pgb: &mut PostgresBackendTCP, query: &str) -> Result<(), QueryError> {
|
fn try_process_query(pgb: &mut PostgresBackendTCP, query: &str) -> Result<(), QueryError> {
|
||||||
let resp: KickSession = serde_json::from_str(query).context("Failed to parse query as json")?;
|
let resp: KickSession = serde_json::from_str(query).context("Failed to parse query as json")?;
|
||||||
|
|
||||||
let span = info_span!("event", session_id = resp.session_id);
|
let span = info_span!("event", session_id = resp.session_id);
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ pub mod server;
|
|||||||
pub mod sql_over_http;
|
pub mod sql_over_http;
|
||||||
pub mod websocket;
|
pub mod websocket;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
pub use reqwest::{Request, Response, StatusCode};
|
pub use reqwest::{Request, Response, StatusCode};
|
||||||
pub use reqwest_middleware::{ClientWithMiddleware, Error};
|
pub use reqwest_middleware::{ClientWithMiddleware, Error};
|
||||||
|
pub use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||||
|
|
||||||
use crate::url::ApiUrl;
|
use crate::url::ApiUrl;
|
||||||
use reqwest_middleware::RequestBuilder;
|
use reqwest_middleware::RequestBuilder;
|
||||||
@@ -21,6 +24,24 @@ pub fn new_client() -> ClientWithMiddleware {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_client_with_timeout(default_timout: Duration) -> ClientWithMiddleware {
|
||||||
|
let timeout_client = reqwest::ClientBuilder::new()
|
||||||
|
.timeout(default_timout)
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create http client with timeout");
|
||||||
|
|
||||||
|
let retry_policy =
|
||||||
|
ExponentialBackoff::builder().build_with_total_retry_duration(default_timout);
|
||||||
|
|
||||||
|
reqwest_middleware::ClientBuilder::new(timeout_client)
|
||||||
|
.with(reqwest_tracing::TracingMiddleware::default())
|
||||||
|
// As per docs, "This middleware always errors when given requests with streaming bodies".
|
||||||
|
// That's all right because we only use this client to send `serde_json::RawValue`, which
|
||||||
|
// is not a stream.
|
||||||
|
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
/// Thin convenience wrapper for an API provided by an http endpoint.
|
/// Thin convenience wrapper for an API provided by an http endpoint.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Endpoint {
|
pub struct Endpoint {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub async fn init() -> anyhow::Result<LoggingGuard> {
|
|||||||
.from_env_lossy();
|
.from_env_lossy();
|
||||||
|
|
||||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||||
.with_ansi(atty::is(atty::Stream::Stderr))
|
.with_ansi(false)
|
||||||
.with_writer(std::io::stderr)
|
.with_writer(std::io::stderr)
|
||||||
.with_target(false);
|
.with_target(false);
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ use crate::{config::MetricCollectionConfig, http};
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use consumption_metrics::{idempotency_key, Event, EventChunk, EventType, CHUNK_SIZE};
|
use consumption_metrics::{idempotency_key, Event, EventChunk, EventType, CHUNK_SIZE};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, time::Duration};
|
||||||
use tracing::{error, info, instrument, trace, warn};
|
use tracing::{error, info, instrument, trace, warn};
|
||||||
|
|
||||||
const PROXY_IO_BYTES_PER_CLIENT: &str = "proxy_io_bytes_per_client";
|
const PROXY_IO_BYTES_PER_CLIENT: &str = "proxy_io_bytes_per_client";
|
||||||
|
|
||||||
|
const DEFAULT_HTTP_REPORTING_TIMEOUT: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Key that uniquely identifies the object, this metric describes.
|
/// Key that uniquely identifies the object, this metric describes.
|
||||||
/// Currently, endpoint_id is enough, but this may change later,
|
/// Currently, endpoint_id is enough, but this may change later,
|
||||||
@@ -30,7 +32,7 @@ pub async fn task_main(config: &MetricCollectionConfig) -> anyhow::Result<()> {
|
|||||||
info!("metrics collector has shut down");
|
info!("metrics collector has shut down");
|
||||||
}
|
}
|
||||||
|
|
||||||
let http_client = http::new_client();
|
let http_client = http::new_client_with_timeout(DEFAULT_HTTP_REPORTING_TIMEOUT);
|
||||||
let mut cached_metrics: HashMap<Ids, (u64, DateTime<Utc>)> = HashMap::new();
|
let mut cached_metrics: HashMap<Ids, (u64, DateTime<Utc>)> = HashMap::new();
|
||||||
let hostname = hostname::get()?.as_os_str().to_string_lossy().into_owned();
|
let hostname = hostname::get()?.as_os_str().to_string_lossy().into_owned();
|
||||||
|
|
||||||
@@ -182,36 +184,36 @@ async fn collect_metrics_iteration(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if res.status().is_success() {
|
if !res.status().is_success() {
|
||||||
// update cached metrics after they were sent successfully
|
|
||||||
for send_metric in chunk {
|
|
||||||
let stop_time = match send_metric.kind {
|
|
||||||
EventType::Incremental { stop_time, .. } => stop_time,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
cached_metrics
|
|
||||||
.entry(Ids {
|
|
||||||
endpoint_id: send_metric.extra.endpoint_id.clone(),
|
|
||||||
branch_id: send_metric.extra.branch_id.clone(),
|
|
||||||
})
|
|
||||||
// update cached value (add delta) and time
|
|
||||||
.and_modify(|e| {
|
|
||||||
e.0 = e.0.saturating_add(send_metric.value);
|
|
||||||
e.1 = stop_time
|
|
||||||
})
|
|
||||||
// cache new metric
|
|
||||||
.or_insert((send_metric.value, stop_time));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error!("metrics endpoint refused the sent metrics: {:?}", res);
|
error!("metrics endpoint refused the sent metrics: {:?}", res);
|
||||||
for metric in chunk.iter() {
|
for metric in chunk.iter().filter(|metric| metric.value > (1u64 << 40)) {
|
||||||
// Report if the metric value is suspiciously large
|
// Report if the metric value is suspiciously large
|
||||||
if metric.value > (1u64 << 40) {
|
error!("potentially abnormal metric value: {:?}", metric);
|
||||||
error!("potentially abnormal metric value: {:?}", metric);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// update cached metrics after they were sent
|
||||||
|
// (to avoid sending the same metrics twice)
|
||||||
|
// see the relevant discussion on why to do so even if the status is not success:
|
||||||
|
// https://github.com/neondatabase/neon/pull/4563#discussion_r1246710956
|
||||||
|
for send_metric in chunk {
|
||||||
|
let stop_time = match send_metric.kind {
|
||||||
|
EventType::Incremental { stop_time, .. } => stop_time,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cached_metrics
|
||||||
|
.entry(Ids {
|
||||||
|
endpoint_id: send_metric.extra.endpoint_id.clone(),
|
||||||
|
branch_id: send_metric.extra.branch_id.clone(),
|
||||||
|
})
|
||||||
|
// update cached value (add delta) and time
|
||||||
|
.and_modify(|e| {
|
||||||
|
e.0 = e.0.saturating_add(send_metric.value);
|
||||||
|
e.1 = stop_time
|
||||||
|
})
|
||||||
|
// cache new metric
|
||||||
|
.or_insert((send_metric.value, stop_time));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ authors = []
|
|||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.9"
|
python = "^3.9"
|
||||||
pytest = "^7.3.1"
|
pytest = "^7.3.1"
|
||||||
psycopg2-binary = "^2.9.1"
|
psycopg2-binary = "^2.9.6"
|
||||||
typing-extensions = "^4.6.1"
|
typing-extensions = "^4.6.1"
|
||||||
PyJWT = {version = "^2.1.0", extras = ["crypto"]}
|
PyJWT = {version = "^2.1.0", extras = ["crypto"]}
|
||||||
requests = "^2.31.0"
|
requests = "^2.31.0"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.68.2"
|
channel = "1.70.0"
|
||||||
profile = "default"
|
profile = "default"
|
||||||
# The default profile includes rustc, rust-std, cargo, rust-docs, rustfmt and clippy.
|
# The default profile includes rustc, rust-std, cargo, rust-docs, rustfmt and clippy.
|
||||||
# https://rust-lang.github.io/rustup/concepts/profiles.html
|
# https://rust-lang.github.io/rustup/concepts/profiles.html
|
||||||
|
|||||||
@@ -144,17 +144,24 @@ const reportSummary = async (params) => {
|
|||||||
}
|
}
|
||||||
summary += `- \`${testName}\`: ${links.join(", ")}\n`
|
summary += `- \`${testName}\`: ${links.join(", ")}\n`
|
||||||
}
|
}
|
||||||
|
|
||||||
const testsToRerun = Object.values(failedTests[pgVersion]).map(x => x[0].name)
|
|
||||||
const command = `DEFAULT_PG_VERSION=${pgVersion} scripts/pytest -k "${testsToRerun.join(" or ")}"`
|
|
||||||
|
|
||||||
summary += "```\n"
|
|
||||||
summary += `# Run failed on Postgres ${pgVersion} tests locally:\n`
|
|
||||||
summary += `${command}\n`
|
|
||||||
summary += "```\n"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (failedTestsCount > 0) {
|
||||||
|
const testsToRerun = []
|
||||||
|
for (const pgVersion of Object.keys(failedTests)) {
|
||||||
|
for (const testName of Object.keys(failedTests[pgVersion])) {
|
||||||
|
testsToRerun.push(...failedTests[pgVersion][testName].map(test => test.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const command = `scripts/pytest -vv -n $(nproc) -k "${testsToRerun.join(" or ")}"`
|
||||||
|
|
||||||
|
summary += "```\n"
|
||||||
|
summary += `# Run all failed tests locally:\n`
|
||||||
|
summary += `${command}\n`
|
||||||
|
summary += "```\n"
|
||||||
|
}
|
||||||
|
|
||||||
if (flakyTestsCount > 0) {
|
if (flakyTestsCount > 0) {
|
||||||
summary += `<details>\n<summary>Flaky tests (${flakyTestsCount})</summary>\n\n`
|
summary += `<details>\n<summary>Flaky tests (${flakyTestsCount})</summary>\n\n`
|
||||||
for (const pgVersion of Array.from(pgVersions).sort().reverse()) {
|
for (const pgVersion of Array.from(pgVersions).sort().reverse()) {
|
||||||
@@ -164,8 +171,7 @@ const reportSummary = async (params) => {
|
|||||||
const links = []
|
const links = []
|
||||||
for (const test of tests) {
|
for (const test of tests) {
|
||||||
const allureLink = `${reportUrl}#suites/${test.parentUid}/${test.uid}/retries`
|
const allureLink = `${reportUrl}#suites/${test.parentUid}/${test.uid}/retries`
|
||||||
const status = test.status === "passed" ? ":white_check_mark:" : ":x:"
|
links.push(`[${test.buildType}](${allureLink})`)
|
||||||
links.push(`[${status} ${test.buildType}](${allureLink})`)
|
|
||||||
}
|
}
|
||||||
summary += `- \`${testName}\`: ${links.join(", ")}\n`
|
summary += `- \`${testName}\`: ${links.join(", ")}\n`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pytest_plugins = (
|
pytest_plugins = (
|
||||||
"fixtures.pg_version",
|
"fixtures.pg_version",
|
||||||
"fixtures.allure",
|
"fixtures.parametrize",
|
||||||
"fixtures.neon_fixtures",
|
"fixtures.neon_fixtures",
|
||||||
"fixtures.benchmark_fixture",
|
"fixtures.benchmark_fixture",
|
||||||
"fixtures.pg_stats",
|
"fixtures.pg_stats",
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from fixtures.pg_version import DEFAULT_VERSION, PgVersion
|
|
||||||
|
|
||||||
"""
|
|
||||||
Set of utilities to make Allure report more informative.
|
|
||||||
|
|
||||||
- It adds BUILD_TYPE and DEFAULT_PG_VERSION to the test names (only in test_runner/regress)
|
|
||||||
to make tests distinguishable in Allure report.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function", autouse=True)
|
|
||||||
def allure_noop():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_generate_tests(metafunc):
|
|
||||||
if "test_runner/regress" in metafunc.definition._nodeid:
|
|
||||||
build_type = os.environ.get("BUILD_TYPE", "DEBUG").lower()
|
|
||||||
pg_version = PgVersion(os.environ.get("DEFAULT_PG_VERSION", DEFAULT_VERSION))
|
|
||||||
|
|
||||||
metafunc.parametrize("allure_noop", [f"{build_type}-pg{pg_version}"])
|
|
||||||
@@ -62,6 +62,7 @@ PAGESERVER_GLOBAL_METRICS: Tuple[str, ...] = (
|
|||||||
"pageserver_getpage_reconstruct_seconds_bucket",
|
"pageserver_getpage_reconstruct_seconds_bucket",
|
||||||
"pageserver_getpage_reconstruct_seconds_count",
|
"pageserver_getpage_reconstruct_seconds_count",
|
||||||
"pageserver_getpage_reconstruct_seconds_sum",
|
"pageserver_getpage_reconstruct_seconds_sum",
|
||||||
|
*[f"pageserver_basebackup_query_seconds_{x}" for x in ["bucket", "count", "sum"]],
|
||||||
)
|
)
|
||||||
|
|
||||||
PAGESERVER_PER_TENANT_METRICS: Tuple[str, ...] = (
|
PAGESERVER_PER_TENANT_METRICS: Tuple[str, ...] = (
|
||||||
|
|||||||
@@ -102,8 +102,8 @@ def base_dir() -> Iterator[Path]:
|
|||||||
yield base_dir
|
yield base_dir
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="function")
|
||||||
def neon_binpath(base_dir: Path) -> Iterator[Path]:
|
def neon_binpath(base_dir: Path, build_type: str) -> Iterator[Path]:
|
||||||
if os.getenv("REMOTE_ENV"):
|
if os.getenv("REMOTE_ENV"):
|
||||||
# we are in remote env and do not have neon binaries locally
|
# we are in remote env and do not have neon binaries locally
|
||||||
# this is the case for benchmarks run on self-hosted runner
|
# this is the case for benchmarks run on self-hosted runner
|
||||||
@@ -113,7 +113,6 @@ def neon_binpath(base_dir: Path) -> Iterator[Path]:
|
|||||||
if env_neon_bin := os.environ.get("NEON_BIN"):
|
if env_neon_bin := os.environ.get("NEON_BIN"):
|
||||||
binpath = Path(env_neon_bin)
|
binpath = Path(env_neon_bin)
|
||||||
else:
|
else:
|
||||||
build_type = os.environ.get("BUILD_TYPE", "debug")
|
|
||||||
binpath = base_dir / "target" / build_type
|
binpath = base_dir / "target" / build_type
|
||||||
log.info(f"neon_binpath is {binpath}")
|
log.info(f"neon_binpath is {binpath}")
|
||||||
|
|
||||||
@@ -123,7 +122,7 @@ def neon_binpath(base_dir: Path) -> Iterator[Path]:
|
|||||||
yield binpath
|
yield binpath
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="function")
|
||||||
def pg_distrib_dir(base_dir: Path) -> Iterator[Path]:
|
def pg_distrib_dir(base_dir: Path) -> Iterator[Path]:
|
||||||
if env_postgres_bin := os.environ.get("POSTGRES_DISTRIB_DIR"):
|
if env_postgres_bin := os.environ.get("POSTGRES_DISTRIB_DIR"):
|
||||||
distrib_dir = Path(env_postgres_bin).resolve()
|
distrib_dir = Path(env_postgres_bin).resolve()
|
||||||
@@ -147,7 +146,7 @@ def top_output_dir(base_dir: Path) -> Iterator[Path]:
|
|||||||
yield output_dir
|
yield output_dir
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="function")
|
||||||
def versioned_pg_distrib_dir(pg_distrib_dir: Path, pg_version: PgVersion) -> Iterator[Path]:
|
def versioned_pg_distrib_dir(pg_distrib_dir: Path, pg_version: PgVersion) -> Iterator[Path]:
|
||||||
versioned_dir = pg_distrib_dir / pg_version.v_prefixed
|
versioned_dir = pg_distrib_dir / pg_version.v_prefixed
|
||||||
|
|
||||||
@@ -174,7 +173,23 @@ def shareable_scope(fixture_name: str, config: Config) -> Literal["session", "fu
|
|||||||
def myfixture(...)
|
def myfixture(...)
|
||||||
...
|
...
|
||||||
"""
|
"""
|
||||||
return "function" if os.environ.get("TEST_SHARED_FIXTURES") is None else "session"
|
scope: Literal["session", "function"]
|
||||||
|
|
||||||
|
if os.environ.get("TEST_SHARED_FIXTURES") is None:
|
||||||
|
# Create the environment in the per-test output directory
|
||||||
|
scope = "function"
|
||||||
|
elif (
|
||||||
|
os.environ.get("BUILD_TYPE") is not None
|
||||||
|
and os.environ.get("DEFAULT_PG_VERSION") is not None
|
||||||
|
):
|
||||||
|
scope = "session"
|
||||||
|
else:
|
||||||
|
pytest.fail(
|
||||||
|
"Shared environment(TEST_SHARED_FIXTURES) requires BUILD_TYPE and DEFAULT_PG_VERSION to be set",
|
||||||
|
pytrace=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
return scope
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
@@ -514,6 +529,16 @@ def available_remote_storages() -> List[RemoteStorageKind]:
|
|||||||
return remote_storages
|
return remote_storages
|
||||||
|
|
||||||
|
|
||||||
|
def available_s3_storages() -> List[RemoteStorageKind]:
|
||||||
|
remote_storages = [RemoteStorageKind.MOCK_S3]
|
||||||
|
if os.getenv("ENABLE_REAL_S3_REMOTE_STORAGE") is not None:
|
||||||
|
remote_storages.append(RemoteStorageKind.REAL_S3)
|
||||||
|
log.info("Enabling real s3 storage for tests")
|
||||||
|
else:
|
||||||
|
log.info("Using mock implementations to test remote storage")
|
||||||
|
return remote_storages
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LocalFsStorage:
|
class LocalFsStorage:
|
||||||
root: Path
|
root: Path
|
||||||
@@ -534,6 +559,16 @@ class S3Storage:
|
|||||||
"AWS_SECRET_ACCESS_KEY": self.secret_key,
|
"AWS_SECRET_ACCESS_KEY": self.secret_key,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def to_string(self) -> str:
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"bucket": self.bucket_name,
|
||||||
|
"region": self.bucket_region,
|
||||||
|
"endpoint": self.endpoint,
|
||||||
|
"prefix": self.prefix_in_bucket,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
RemoteStorage = Union[LocalFsStorage, S3Storage]
|
RemoteStorage = Union[LocalFsStorage, S3Storage]
|
||||||
|
|
||||||
@@ -600,10 +635,12 @@ class NeonEnvBuilder:
|
|||||||
self.rust_log_override = rust_log_override
|
self.rust_log_override = rust_log_override
|
||||||
self.port_distributor = port_distributor
|
self.port_distributor = port_distributor
|
||||||
self.remote_storage = remote_storage
|
self.remote_storage = remote_storage
|
||||||
|
self.ext_remote_storage: Optional[S3Storage] = None
|
||||||
|
self.remote_storage_client: Optional[Any] = None
|
||||||
self.remote_storage_users = remote_storage_users
|
self.remote_storage_users = remote_storage_users
|
||||||
self.broker = broker
|
self.broker = broker
|
||||||
self.run_id = run_id
|
self.run_id = run_id
|
||||||
self.mock_s3_server = mock_s3_server
|
self.mock_s3_server: MockS3Server = mock_s3_server
|
||||||
self.pageserver_config_override = pageserver_config_override
|
self.pageserver_config_override = pageserver_config_override
|
||||||
self.num_safekeepers = num_safekeepers
|
self.num_safekeepers = num_safekeepers
|
||||||
self.safekeepers_id_start = safekeepers_id_start
|
self.safekeepers_id_start = safekeepers_id_start
|
||||||
@@ -651,15 +688,24 @@ class NeonEnvBuilder:
|
|||||||
remote_storage_kind: RemoteStorageKind,
|
remote_storage_kind: RemoteStorageKind,
|
||||||
test_name: str,
|
test_name: str,
|
||||||
force_enable: bool = True,
|
force_enable: bool = True,
|
||||||
|
enable_remote_extensions: bool = False,
|
||||||
):
|
):
|
||||||
if remote_storage_kind == RemoteStorageKind.NOOP:
|
if remote_storage_kind == RemoteStorageKind.NOOP:
|
||||||
return
|
return
|
||||||
elif remote_storage_kind == RemoteStorageKind.LOCAL_FS:
|
elif remote_storage_kind == RemoteStorageKind.LOCAL_FS:
|
||||||
self.enable_local_fs_remote_storage(force_enable=force_enable)
|
self.enable_local_fs_remote_storage(force_enable=force_enable)
|
||||||
elif remote_storage_kind == RemoteStorageKind.MOCK_S3:
|
elif remote_storage_kind == RemoteStorageKind.MOCK_S3:
|
||||||
self.enable_mock_s3_remote_storage(bucket_name=test_name, force_enable=force_enable)
|
self.enable_mock_s3_remote_storage(
|
||||||
|
bucket_name=test_name,
|
||||||
|
force_enable=force_enable,
|
||||||
|
enable_remote_extensions=enable_remote_extensions,
|
||||||
|
)
|
||||||
elif remote_storage_kind == RemoteStorageKind.REAL_S3:
|
elif remote_storage_kind == RemoteStorageKind.REAL_S3:
|
||||||
self.enable_real_s3_remote_storage(test_name=test_name, force_enable=force_enable)
|
self.enable_real_s3_remote_storage(
|
||||||
|
test_name=test_name,
|
||||||
|
force_enable=force_enable,
|
||||||
|
enable_remote_extensions=enable_remote_extensions,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f"Unknown storage type: {remote_storage_kind}")
|
raise RuntimeError(f"Unknown storage type: {remote_storage_kind}")
|
||||||
|
|
||||||
@@ -673,11 +719,15 @@ class NeonEnvBuilder:
|
|||||||
assert force_enable or self.remote_storage is None, "remote storage is enabled already"
|
assert force_enable or self.remote_storage is None, "remote storage is enabled already"
|
||||||
self.remote_storage = LocalFsStorage(Path(self.repo_dir / "local_fs_remote_storage"))
|
self.remote_storage = LocalFsStorage(Path(self.repo_dir / "local_fs_remote_storage"))
|
||||||
|
|
||||||
def enable_mock_s3_remote_storage(self, bucket_name: str, force_enable: bool = True):
|
def enable_mock_s3_remote_storage(
|
||||||
|
self, bucket_name: str, force_enable: bool = True, enable_remote_extensions: bool = False
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Sets up the pageserver to use the S3 mock server, creates the bucket, if it's not present already.
|
Sets up the pageserver to use the S3 mock server, creates the bucket, if it's not present already.
|
||||||
Starts up the mock server, if that does not run yet.
|
Starts up the mock server, if that does not run yet.
|
||||||
Errors, if the pageserver has some remote storage configuration already, unless `force_enable` is not set to `True`.
|
Errors, if the pageserver has some remote storage configuration already, unless `force_enable` is not set to `True`.
|
||||||
|
|
||||||
|
Also creates the bucket for extensions, self.ext_remote_storage bucket
|
||||||
"""
|
"""
|
||||||
assert force_enable or self.remote_storage is None, "remote storage is enabled already"
|
assert force_enable or self.remote_storage is None, "remote storage is enabled already"
|
||||||
mock_endpoint = self.mock_s3_server.endpoint()
|
mock_endpoint = self.mock_s3_server.endpoint()
|
||||||
@@ -698,9 +748,22 @@ class NeonEnvBuilder:
|
|||||||
bucket_region=mock_region,
|
bucket_region=mock_region,
|
||||||
access_key=self.mock_s3_server.access_key(),
|
access_key=self.mock_s3_server.access_key(),
|
||||||
secret_key=self.mock_s3_server.secret_key(),
|
secret_key=self.mock_s3_server.secret_key(),
|
||||||
|
prefix_in_bucket="pageserver",
|
||||||
)
|
)
|
||||||
|
|
||||||
def enable_real_s3_remote_storage(self, test_name: str, force_enable: bool = True):
|
if enable_remote_extensions:
|
||||||
|
self.ext_remote_storage = S3Storage(
|
||||||
|
bucket_name=bucket_name,
|
||||||
|
endpoint=mock_endpoint,
|
||||||
|
bucket_region=mock_region,
|
||||||
|
access_key=self.mock_s3_server.access_key(),
|
||||||
|
secret_key=self.mock_s3_server.secret_key(),
|
||||||
|
prefix_in_bucket="ext",
|
||||||
|
)
|
||||||
|
|
||||||
|
def enable_real_s3_remote_storage(
|
||||||
|
self, test_name: str, force_enable: bool = True, enable_remote_extensions: bool = False
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Sets up configuration to use real s3 endpoint without mock server
|
Sets up configuration to use real s3 endpoint without mock server
|
||||||
"""
|
"""
|
||||||
@@ -740,6 +803,15 @@ class NeonEnvBuilder:
|
|||||||
prefix_in_bucket=self.remote_storage_prefix,
|
prefix_in_bucket=self.remote_storage_prefix,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if enable_remote_extensions:
|
||||||
|
self.ext_remote_storage = S3Storage(
|
||||||
|
bucket_name="neon-dev-extensions",
|
||||||
|
bucket_region="eu-central-1",
|
||||||
|
access_key=access_key,
|
||||||
|
secret_key=secret_key,
|
||||||
|
prefix_in_bucket="5555",
|
||||||
|
)
|
||||||
|
|
||||||
def cleanup_local_storage(self):
|
def cleanup_local_storage(self):
|
||||||
if self.preserve_database_files:
|
if self.preserve_database_files:
|
||||||
return
|
return
|
||||||
@@ -773,6 +845,7 @@ class NeonEnvBuilder:
|
|||||||
# `self.remote_storage_prefix` is coupled with `S3Storage` storage type,
|
# `self.remote_storage_prefix` is coupled with `S3Storage` storage type,
|
||||||
# so this line effectively a no-op
|
# so this line effectively a no-op
|
||||||
assert isinstance(self.remote_storage, S3Storage)
|
assert isinstance(self.remote_storage, S3Storage)
|
||||||
|
assert self.remote_storage_client is not None
|
||||||
|
|
||||||
if self.keep_remote_storage_contents:
|
if self.keep_remote_storage_contents:
|
||||||
log.info("keep_remote_storage_contents skipping remote storage cleanup")
|
log.info("keep_remote_storage_contents skipping remote storage cleanup")
|
||||||
@@ -902,6 +975,8 @@ class NeonEnv:
|
|||||||
self.neon_binpath = config.neon_binpath
|
self.neon_binpath = config.neon_binpath
|
||||||
self.pg_distrib_dir = config.pg_distrib_dir
|
self.pg_distrib_dir = config.pg_distrib_dir
|
||||||
self.endpoint_counter = 0
|
self.endpoint_counter = 0
|
||||||
|
self.remote_storage_client = config.remote_storage_client
|
||||||
|
self.ext_remote_storage = config.ext_remote_storage
|
||||||
|
|
||||||
# generate initial tenant ID here instead of letting 'neon init' generate it,
|
# generate initial tenant ID here instead of letting 'neon init' generate it,
|
||||||
# so that we don't need to dig it out of the config file afterwards.
|
# so that we don't need to dig it out of the config file afterwards.
|
||||||
@@ -1488,6 +1563,7 @@ class NeonCli(AbstractNeonCli):
|
|||||||
safekeepers: Optional[List[int]] = None,
|
safekeepers: Optional[List[int]] = None,
|
||||||
tenant_id: Optional[TenantId] = None,
|
tenant_id: Optional[TenantId] = None,
|
||||||
lsn: Optional[Lsn] = None,
|
lsn: Optional[Lsn] = None,
|
||||||
|
remote_ext_config: Optional[str] = None,
|
||||||
) -> "subprocess.CompletedProcess[str]":
|
) -> "subprocess.CompletedProcess[str]":
|
||||||
args = [
|
args = [
|
||||||
"endpoint",
|
"endpoint",
|
||||||
@@ -1497,6 +1573,8 @@ class NeonCli(AbstractNeonCli):
|
|||||||
"--pg-version",
|
"--pg-version",
|
||||||
self.env.pg_version,
|
self.env.pg_version,
|
||||||
]
|
]
|
||||||
|
if remote_ext_config is not None:
|
||||||
|
args.extend(["--remote-ext-config", remote_ext_config])
|
||||||
if lsn is not None:
|
if lsn is not None:
|
||||||
args.append(f"--lsn={lsn}")
|
args.append(f"--lsn={lsn}")
|
||||||
args.extend(["--pg-port", str(pg_port)])
|
args.extend(["--pg-port", str(pg_port)])
|
||||||
@@ -2358,7 +2436,7 @@ class Endpoint(PgProtocol):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def start(self) -> "Endpoint":
|
def start(self, remote_ext_config: Optional[str] = None) -> "Endpoint":
|
||||||
"""
|
"""
|
||||||
Start the Postgres instance.
|
Start the Postgres instance.
|
||||||
Returns self.
|
Returns self.
|
||||||
@@ -2374,6 +2452,7 @@ class Endpoint(PgProtocol):
|
|||||||
http_port=self.http_port,
|
http_port=self.http_port,
|
||||||
tenant_id=self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
safekeepers=self.active_safekeepers,
|
safekeepers=self.active_safekeepers,
|
||||||
|
remote_ext_config=remote_ext_config,
|
||||||
)
|
)
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
@@ -2463,6 +2542,7 @@ class Endpoint(PgProtocol):
|
|||||||
hot_standby: bool = False,
|
hot_standby: bool = False,
|
||||||
lsn: Optional[Lsn] = None,
|
lsn: Optional[Lsn] = None,
|
||||||
config_lines: Optional[List[str]] = None,
|
config_lines: Optional[List[str]] = None,
|
||||||
|
remote_ext_config: Optional[str] = None,
|
||||||
) -> "Endpoint":
|
) -> "Endpoint":
|
||||||
"""
|
"""
|
||||||
Create an endpoint, apply config, and start Postgres.
|
Create an endpoint, apply config, and start Postgres.
|
||||||
@@ -2477,7 +2557,7 @@ class Endpoint(PgProtocol):
|
|||||||
config_lines=config_lines,
|
config_lines=config_lines,
|
||||||
hot_standby=hot_standby,
|
hot_standby=hot_standby,
|
||||||
lsn=lsn,
|
lsn=lsn,
|
||||||
).start()
|
).start(remote_ext_config=remote_ext_config)
|
||||||
|
|
||||||
log.info(f"Postgres startup took {time.time() - started_at} seconds")
|
log.info(f"Postgres startup took {time.time() - started_at} seconds")
|
||||||
|
|
||||||
@@ -2511,6 +2591,7 @@ class EndpointFactory:
|
|||||||
lsn: Optional[Lsn] = None,
|
lsn: Optional[Lsn] = None,
|
||||||
hot_standby: bool = False,
|
hot_standby: bool = False,
|
||||||
config_lines: Optional[List[str]] = None,
|
config_lines: Optional[List[str]] = None,
|
||||||
|
remote_ext_config: Optional[str] = None,
|
||||||
) -> Endpoint:
|
) -> Endpoint:
|
||||||
ep = Endpoint(
|
ep = Endpoint(
|
||||||
self.env,
|
self.env,
|
||||||
@@ -2527,6 +2608,7 @@ class EndpointFactory:
|
|||||||
hot_standby=hot_standby,
|
hot_standby=hot_standby,
|
||||||
config_lines=config_lines,
|
config_lines=config_lines,
|
||||||
lsn=lsn,
|
lsn=lsn,
|
||||||
|
remote_ext_config=remote_ext_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
def create(
|
def create(
|
||||||
|
|||||||
@@ -21,6 +21,18 @@ class PageserverApiException(Exception):
|
|||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
|
|
||||||
|
|
||||||
|
class TimelineCreate406(PageserverApiException):
|
||||||
|
def __init__(self, res: requests.Response):
|
||||||
|
assert res.status_code == 406
|
||||||
|
super().__init__(res.json()["msg"], res.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
class TimelineCreate409(PageserverApiException):
|
||||||
|
def __init__(self, res: requests.Response):
|
||||||
|
assert res.status_code == 409
|
||||||
|
super().__init__("", res.status_code)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class InMemoryLayerInfo:
|
class InMemoryLayerInfo:
|
||||||
kind: str
|
kind: str
|
||||||
@@ -309,9 +321,12 @@ class PageserverHttpClient(requests.Session):
|
|||||||
res = self.post(
|
res = self.post(
|
||||||
f"http://localhost:{self.port}/v1/tenant/{tenant_id}/timeline", json=body, **kwargs
|
f"http://localhost:{self.port}/v1/tenant/{tenant_id}/timeline", json=body, **kwargs
|
||||||
)
|
)
|
||||||
self.verbose_error(res)
|
|
||||||
if res.status_code == 409:
|
if res.status_code == 409:
|
||||||
raise Exception(f"could not create timeline: already exists for id {new_timeline_id}")
|
raise TimelineCreate409(res)
|
||||||
|
if res.status_code == 406:
|
||||||
|
raise TimelineCreate406(res)
|
||||||
|
|
||||||
|
self.verbose_error(res)
|
||||||
|
|
||||||
res_json = res.json()
|
res_json = res.json()
|
||||||
assert isinstance(res_json, dict)
|
assert isinstance(res_json, dict)
|
||||||
|
|||||||
50
test_runner/fixtures/parametrize.py
Normal file
50
test_runner/fixtures/parametrize.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from _pytest.fixtures import FixtureRequest
|
||||||
|
from _pytest.python import Metafunc
|
||||||
|
|
||||||
|
from fixtures.pg_version import PgVersion
|
||||||
|
|
||||||
|
"""
|
||||||
|
Dynamically parametrize tests by Postgres version and build type (debug/release/remote)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function", autouse=True)
|
||||||
|
def pg_version(request: FixtureRequest) -> Optional[PgVersion]:
|
||||||
|
# Do not parametrize performance tests yet, we need to prepare grafana charts first
|
||||||
|
if "test_runner/performance" in str(request.node.path):
|
||||||
|
v = os.environ.get("DEFAULT_PG_VERSION")
|
||||||
|
return PgVersion(v)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function", autouse=True)
|
||||||
|
def build_type(request: FixtureRequest) -> Optional[str]:
|
||||||
|
# Do not parametrize performance tests yet, we need to prepare grafana charts first
|
||||||
|
if "test_runner/performance" in str(request.node.path):
|
||||||
|
return os.environ.get("BUILD_TYPE", "").lower()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_generate_tests(metafunc: Metafunc):
|
||||||
|
# Do not parametrize performance tests yet, we need to prepare grafana charts first
|
||||||
|
if "test_runner/performance" in metafunc.definition._nodeid:
|
||||||
|
return
|
||||||
|
|
||||||
|
if (v := os.environ.get("DEFAULT_PG_VERSION")) is None:
|
||||||
|
pg_versions = [version for version in PgVersion if version != PgVersion.NOT_SET]
|
||||||
|
else:
|
||||||
|
pg_versions = [PgVersion(v)]
|
||||||
|
|
||||||
|
if (bt := os.environ.get("BUILD_TYPE")) is None:
|
||||||
|
build_types = ["debug", "release"]
|
||||||
|
else:
|
||||||
|
build_types = [bt.lower()]
|
||||||
|
|
||||||
|
metafunc.parametrize("build_type", build_types)
|
||||||
|
metafunc.parametrize("pg_version", pg_versions, ids=map(lambda v: f"pg{v}", pg_versions))
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
import enum
|
import enum
|
||||||
import os
|
import os
|
||||||
from typing import Iterator, Optional
|
from typing import Optional
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest.config import Config
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from pytest import FixtureRequest
|
|
||||||
|
|
||||||
from fixtures.log_helper import log
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
This fixture is used to determine which version of Postgres to use for tests.
|
This fixture is used to determine which version of Postgres to use for tests.
|
||||||
@@ -75,18 +73,10 @@ def pytest_addoption(parser: Parser):
|
|||||||
"--pg-version",
|
"--pg-version",
|
||||||
action="store",
|
action="store",
|
||||||
type=PgVersion,
|
type=PgVersion,
|
||||||
help="Postgres version to use for tests",
|
help="DEPRECATED: Postgres version to use for tests",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
def pytest_configure(config: Config):
|
||||||
def pg_version(request: FixtureRequest) -> Iterator[PgVersion]:
|
if config.getoption("--pg-version"):
|
||||||
if v := request.config.getoption("--pg-version"):
|
raise Exception("--pg-version is deprecated, use DEFAULT_PG_VERSION env var instead")
|
||||||
version, source = v, "from --pg-version command-line argument"
|
|
||||||
elif v := os.environ.get("DEFAULT_PG_VERSION"):
|
|
||||||
version, source = PgVersion(v), "from DEFAULT_PG_VERSION environment variable"
|
|
||||||
else:
|
|
||||||
version, source = DEFAULT_VERSION, "default version"
|
|
||||||
|
|
||||||
log.info(f"pg_version is {version} ({source})")
|
|
||||||
yield version
|
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ class TenantId(Id):
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f'`TenantId("{self.id.hex()}")'
|
return f'`TenantId("{self.id.hex()}")'
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.id.hex()
|
||||||
|
|
||||||
|
|
||||||
class TimelineId(Id):
|
class TimelineId(Id):
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
|||||||
BIN
test_runner/regress/data/extension_test/v14/anon.tar.gz
Normal file
BIN
test_runner/regress/data/extension_test/v14/anon.tar.gz
Normal file
Binary file not shown.
BIN
test_runner/regress/data/extension_test/v14/embedding.tar.gz
Normal file
BIN
test_runner/regress/data/extension_test/v14/embedding.tar.gz
Normal file
Binary file not shown.
14
test_runner/regress/data/extension_test/v14/ext_index.json
Normal file
14
test_runner/regress/data/extension_test/v14/ext_index.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"enabled_extensions": {
|
||||||
|
"123454321": [
|
||||||
|
"anon"
|
||||||
|
],
|
||||||
|
"public": [
|
||||||
|
"embedding"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"control_data": {
|
||||||
|
"embedding": "comment = 'hnsw index' \ndefault_version = '0.1.0' \nmodule_pathname = '$libdir/embedding' \nrelocatable = true \ntrusted = true",
|
||||||
|
"anon": "# PostgreSQL Anonymizer (anon) extension \ncomment = 'Data anonymization tools' \ndefault_version = '1.1.0' \ndirectory='extension/anon' \nrelocatable = false \nrequires = 'pgcrypto' \nsuperuser = false \nmodule_pathname = '$libdir/anon' \ntrusted = true \n"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
test_runner/regress/data/extension_test/v15/anon.tar.gz
Normal file
BIN
test_runner/regress/data/extension_test/v15/anon.tar.gz
Normal file
Binary file not shown.
BIN
test_runner/regress/data/extension_test/v15/embedding.tar.gz
Normal file
BIN
test_runner/regress/data/extension_test/v15/embedding.tar.gz
Normal file
Binary file not shown.
14
test_runner/regress/data/extension_test/v15/ext_index.json
Normal file
14
test_runner/regress/data/extension_test/v15/ext_index.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"enabled_extensions": {
|
||||||
|
"123454321": [
|
||||||
|
"anon"
|
||||||
|
],
|
||||||
|
"public": [
|
||||||
|
"embedding"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"control_data": {
|
||||||
|
"embedding": "comment = 'hnsw index' \ndefault_version = '0.1.0' \nmodule_pathname = '$libdir/embedding' \nrelocatable = true \ntrusted = true",
|
||||||
|
"anon": "# PostgreSQL Anonymizer (anon) extension \ncomment = 'Data anonymization tools' \ndefault_version = '1.1.0' \ndirectory='extension/anon' \nrelocatable = false \nrequires = 'pgcrypto' \nsuperuser = false \nmodule_pathname = '$libdir/anon' \ntrusted = true \n"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import time
|
|||||||
import pytest
|
import pytest
|
||||||
from fixtures.log_helper import log
|
from fixtures.log_helper import log
|
||||||
from fixtures.neon_fixtures import NeonEnv
|
from fixtures.neon_fixtures import NeonEnv
|
||||||
from fixtures.types import Lsn
|
from fixtures.pageserver.http import TimelineCreate406
|
||||||
|
from fixtures.types import Lsn, TimelineId
|
||||||
from fixtures.utils import query_scalar
|
from fixtures.utils import query_scalar
|
||||||
|
|
||||||
|
|
||||||
@@ -173,5 +174,12 @@ def test_branch_creation_before_gc(neon_simple_env: NeonEnv):
|
|||||||
# The starting LSN is invalid as the corresponding record is scheduled to be removed by in-queue GC.
|
# The starting LSN is invalid as the corresponding record is scheduled to be removed by in-queue GC.
|
||||||
with pytest.raises(Exception, match="invalid branch start lsn: .*"):
|
with pytest.raises(Exception, match="invalid branch start lsn: .*"):
|
||||||
env.neon_cli.create_branch("b1", "b0", tenant_id=tenant, ancestor_start_lsn=lsn)
|
env.neon_cli.create_branch("b1", "b0", tenant_id=tenant, ancestor_start_lsn=lsn)
|
||||||
|
# retry the same with the HTTP API, so that we can inspect the status code
|
||||||
|
with pytest.raises(TimelineCreate406):
|
||||||
|
new_timeline_id = TimelineId.generate()
|
||||||
|
log.info(
|
||||||
|
f"Expecting failure for branch behind gc'ing LSN, new_timeline_id={new_timeline_id}"
|
||||||
|
)
|
||||||
|
pageserver_http_client.timeline_create(env.pg_version, tenant, new_timeline_id, b0, lsn)
|
||||||
|
|
||||||
thread.join()
|
thread.join()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from fixtures.log_helper import log
|
from fixtures.log_helper import log
|
||||||
from fixtures.neon_fixtures import NeonEnvBuilder
|
from fixtures.neon_fixtures import NeonEnvBuilder
|
||||||
|
from fixtures.pageserver.http import TimelineCreate406
|
||||||
from fixtures.types import Lsn, TimelineId
|
from fixtures.types import Lsn, TimelineId
|
||||||
from fixtures.utils import print_gc_result, query_scalar
|
from fixtures.utils import print_gc_result, query_scalar
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ def test_branch_behind(neon_env_builder: NeonEnvBuilder):
|
|||||||
env.pageserver.allowed_errors.append(".*invalid start lsn .* for ancestor timeline.*")
|
env.pageserver.allowed_errors.append(".*invalid start lsn .* for ancestor timeline.*")
|
||||||
|
|
||||||
# Branch at the point where only 100 rows were inserted
|
# Branch at the point where only 100 rows were inserted
|
||||||
env.neon_cli.create_branch("test_branch_behind")
|
branch_behind_timeline_id = env.neon_cli.create_branch("test_branch_behind")
|
||||||
endpoint_main = env.endpoints.create_start("test_branch_behind")
|
endpoint_main = env.endpoints.create_start("test_branch_behind")
|
||||||
log.info("postgres is running on 'test_branch_behind' branch")
|
log.info("postgres is running on 'test_branch_behind' branch")
|
||||||
|
|
||||||
@@ -89,6 +90,7 @@ def test_branch_behind(neon_env_builder: NeonEnvBuilder):
|
|||||||
assert query_scalar(main_cur, "SELECT count(*) FROM foo") == 400100
|
assert query_scalar(main_cur, "SELECT count(*) FROM foo") == 400100
|
||||||
|
|
||||||
# Check bad lsn's for branching
|
# Check bad lsn's for branching
|
||||||
|
pageserver_http = env.pageserver.http_client()
|
||||||
|
|
||||||
# branch at segment boundary
|
# branch at segment boundary
|
||||||
env.neon_cli.create_branch(
|
env.neon_cli.create_branch(
|
||||||
@@ -97,27 +99,52 @@ def test_branch_behind(neon_env_builder: NeonEnvBuilder):
|
|||||||
endpoint = env.endpoints.create_start("test_branch_segment_boundary")
|
endpoint = env.endpoints.create_start("test_branch_segment_boundary")
|
||||||
assert endpoint.safe_psql("SELECT 1")[0][0] == 1
|
assert endpoint.safe_psql("SELECT 1")[0][0] == 1
|
||||||
|
|
||||||
# branch at pre-initdb lsn
|
# branch at pre-initdb lsn (from main branch)
|
||||||
with pytest.raises(Exception, match="invalid branch start lsn: .*"):
|
with pytest.raises(Exception, match="invalid branch start lsn: .*"):
|
||||||
env.neon_cli.create_branch("test_branch_preinitdb", ancestor_start_lsn=Lsn("0/42"))
|
env.neon_cli.create_branch("test_branch_preinitdb", ancestor_start_lsn=Lsn("0/42"))
|
||||||
|
# retry the same with the HTTP API, so that we can inspect the status code
|
||||||
|
with pytest.raises(TimelineCreate406):
|
||||||
|
new_timeline_id = TimelineId.generate()
|
||||||
|
log.info(f"Expecting failure for branch pre-initdb LSN, new_timeline_id={new_timeline_id}")
|
||||||
|
pageserver_http.timeline_create(
|
||||||
|
env.pg_version, env.initial_tenant, new_timeline_id, env.initial_timeline, Lsn("0/42")
|
||||||
|
)
|
||||||
|
|
||||||
# branch at pre-ancestor lsn
|
# branch at pre-ancestor lsn
|
||||||
with pytest.raises(Exception, match="less than timeline ancestor lsn"):
|
with pytest.raises(Exception, match="less than timeline ancestor lsn"):
|
||||||
env.neon_cli.create_branch(
|
env.neon_cli.create_branch(
|
||||||
"test_branch_preinitdb", "test_branch_behind", ancestor_start_lsn=Lsn("0/42")
|
"test_branch_preinitdb", "test_branch_behind", ancestor_start_lsn=Lsn("0/42")
|
||||||
)
|
)
|
||||||
|
# retry the same with the HTTP API, so that we can inspect the status code
|
||||||
|
with pytest.raises(TimelineCreate406):
|
||||||
|
new_timeline_id = TimelineId.generate()
|
||||||
|
log.info(
|
||||||
|
f"Expecting failure for branch pre-ancestor LSN, new_timeline_id={new_timeline_id}"
|
||||||
|
)
|
||||||
|
pageserver_http.timeline_create(
|
||||||
|
env.pg_version,
|
||||||
|
env.initial_tenant,
|
||||||
|
new_timeline_id,
|
||||||
|
branch_behind_timeline_id,
|
||||||
|
Lsn("0/42"),
|
||||||
|
)
|
||||||
|
|
||||||
# check that we cannot create branch based on garbage collected data
|
# check that we cannot create branch based on garbage collected data
|
||||||
with env.pageserver.http_client() as pageserver_http:
|
pageserver_http.timeline_checkpoint(env.initial_tenant, timeline)
|
||||||
pageserver_http.timeline_checkpoint(env.initial_tenant, timeline)
|
gc_result = pageserver_http.timeline_gc(env.initial_tenant, timeline, 0)
|
||||||
gc_result = pageserver_http.timeline_gc(env.initial_tenant, timeline, 0)
|
print_gc_result(gc_result)
|
||||||
print_gc_result(gc_result)
|
|
||||||
|
|
||||||
with pytest.raises(Exception, match="invalid branch start lsn: .*"):
|
with pytest.raises(Exception, match="invalid branch start lsn: .*"):
|
||||||
# this gced_lsn is pretty random, so if gc is disabled this woudln't fail
|
# this gced_lsn is pretty random, so if gc is disabled this woudln't fail
|
||||||
env.neon_cli.create_branch(
|
env.neon_cli.create_branch(
|
||||||
"test_branch_create_fail", "test_branch_behind", ancestor_start_lsn=gced_lsn
|
"test_branch_create_fail", "test_branch_behind", ancestor_start_lsn=gced_lsn
|
||||||
)
|
)
|
||||||
|
# retry the same with the HTTP API, so that we can inspect the status code
|
||||||
|
with pytest.raises(TimelineCreate406):
|
||||||
|
new_timeline_id = TimelineId.generate()
|
||||||
|
log.info(f"Expecting failure for branch behind gc'd LSN, new_timeline_id={new_timeline_id}")
|
||||||
|
pageserver_http.timeline_create(
|
||||||
|
env.pg_version, env.initial_tenant, new_timeline_id, branch_behind_timeline_id, gced_lsn
|
||||||
|
)
|
||||||
|
|
||||||
# check that after gc everything is still there
|
# check that after gc everything is still there
|
||||||
assert query_scalar(hundred_cur, "SELECT count(*) FROM foo") == 100
|
assert query_scalar(hundred_cur, "SELECT count(*) FROM foo") == 100
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import copy
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
@@ -448,7 +449,7 @@ def dump_differs(first: Path, second: Path, output: Path) -> bool:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
with output.open("w") as stdout:
|
with output.open("w") as stdout:
|
||||||
rv = subprocess.run(
|
res = subprocess.run(
|
||||||
[
|
[
|
||||||
"diff",
|
"diff",
|
||||||
"--unified", # Make diff output more readable
|
"--unified", # Make diff output more readable
|
||||||
@@ -460,4 +461,53 @@ def dump_differs(first: Path, second: Path, output: Path) -> bool:
|
|||||||
stdout=stdout,
|
stdout=stdout,
|
||||||
)
|
)
|
||||||
|
|
||||||
return rv.returncode != 0
|
differs = res.returncode != 0
|
||||||
|
|
||||||
|
# TODO: Remove after https://github.com/neondatabase/neon/pull/4425 is merged, and a couple of releases are made
|
||||||
|
if differs:
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w") as tmp:
|
||||||
|
tmp.write(PR4425_ALLOWED_DIFF)
|
||||||
|
tmp.flush()
|
||||||
|
|
||||||
|
allowed = subprocess.run(
|
||||||
|
[
|
||||||
|
"diff",
|
||||||
|
"--unified", # Make diff output more readable
|
||||||
|
r"--ignore-matching-lines=^---", # Ignore diff headers
|
||||||
|
r"--ignore-matching-lines=^\+\+\+", # Ignore diff headers
|
||||||
|
"--ignore-matching-lines=^@@", # Ignore diff blocks location
|
||||||
|
"--ignore-matching-lines=^ *$", # Ignore lines with only spaces
|
||||||
|
"--ignore-matching-lines=^ --.*", # Ignore the " --" lines for compatibility with PG14
|
||||||
|
"--ignore-blank-lines",
|
||||||
|
str(output),
|
||||||
|
str(tmp.name),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
differs = allowed.returncode != 0
|
||||||
|
|
||||||
|
return differs
|
||||||
|
|
||||||
|
|
||||||
|
PR4425_ALLOWED_DIFF = """
|
||||||
|
--- /tmp/test_output/test_backward_compatibility[release-pg15]/compatibility_snapshot/dump.sql 2023-06-08 18:12:45.000000000 +0000
|
||||||
|
+++ /tmp/test_output/test_backward_compatibility[release-pg15]/dump.sql 2023-06-13 07:25:35.211733653 +0000
|
||||||
|
@@ -13,12 +13,20 @@
|
||||||
|
|
||||||
|
CREATE ROLE cloud_admin;
|
||||||
|
ALTER ROLE cloud_admin WITH SUPERUSER INHERIT CREATEROLE CREATEDB LOGIN REPLICATION BYPASSRLS;
|
||||||
|
+CREATE ROLE neon_superuser;
|
||||||
|
+ALTER ROLE neon_superuser WITH NOSUPERUSER INHERIT CREATEROLE CREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- User Configurations
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
+--
|
||||||
|
+-- Role memberships
|
||||||
|
+--
|
||||||
|
+
|
||||||
|
+GRANT pg_read_all_data TO neon_superuser GRANTED BY cloud_admin;
|
||||||
|
+GRANT pg_write_all_data TO neon_superuser GRANTED BY cloud_admin;
|
||||||
|
"""
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ def handle_role(dbs, roles, operation):
|
|||||||
dbs[db] = operation["name"]
|
dbs[db] = operation["name"]
|
||||||
if "password" in operation:
|
if "password" in operation:
|
||||||
roles[operation["name"]] = operation["password"]
|
roles[operation["name"]] = operation["password"]
|
||||||
|
assert "encrypted_password" in operation
|
||||||
elif operation["op"] == "del":
|
elif operation["op"] == "del":
|
||||||
if "old_name" in operation:
|
if "old_name" in operation:
|
||||||
roles.pop(operation["old_name"])
|
roles.pop(operation["old_name"])
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, Tuple
|
from typing import Dict, Tuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -428,14 +427,14 @@ def poor_mans_du(
|
|||||||
largest_layer = 0
|
largest_layer = 0
|
||||||
smallest_layer = None
|
smallest_layer = None
|
||||||
for tenant_id, timeline_id in timelines:
|
for tenant_id, timeline_id in timelines:
|
||||||
dir = Path(env.repo_dir) / "tenants" / str(tenant_id) / "timelines" / str(timeline_id)
|
timeline_dir = env.timeline_dir(tenant_id, timeline_id)
|
||||||
assert dir.exists(), f"timeline dir does not exist: {dir}"
|
assert timeline_dir.exists(), f"timeline dir does not exist: {timeline_dir}"
|
||||||
sum = 0
|
total = 0
|
||||||
for file in dir.iterdir():
|
for file in timeline_dir.iterdir():
|
||||||
if "__" not in file.name:
|
if "__" not in file.name:
|
||||||
continue
|
continue
|
||||||
size = file.stat().st_size
|
size = file.stat().st_size
|
||||||
sum += size
|
total += size
|
||||||
largest_layer = max(largest_layer, size)
|
largest_layer = max(largest_layer, size)
|
||||||
if smallest_layer:
|
if smallest_layer:
|
||||||
smallest_layer = min(smallest_layer, size)
|
smallest_layer = min(smallest_layer, size)
|
||||||
@@ -443,8 +442,8 @@ def poor_mans_du(
|
|||||||
smallest_layer = size
|
smallest_layer = size
|
||||||
log.info(f"{tenant_id}/{timeline_id} => {file.name} {size}")
|
log.info(f"{tenant_id}/{timeline_id} => {file.name} {size}")
|
||||||
|
|
||||||
log.info(f"{tenant_id}/{timeline_id}: sum {sum}")
|
log.info(f"{tenant_id}/{timeline_id}: sum {total}")
|
||||||
total_on_disk += sum
|
total_on_disk += total
|
||||||
|
|
||||||
assert smallest_layer is not None or total_on_disk == 0 and largest_layer == 0
|
assert smallest_layer is not None or total_on_disk == 0 and largest_layer == 0
|
||||||
return (total_on_disk, largest_layer, smallest_layer or 0)
|
return (total_on_disk, largest_layer, smallest_layer or 0)
|
||||||
|
|||||||
122
test_runner/regress/test_download_extensions.py
Normal file
122
test_runner/regress/test_download_extensions.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fixtures.log_helper import log
|
||||||
|
from fixtures.neon_fixtures import (
|
||||||
|
NeonEnvBuilder,
|
||||||
|
RemoteStorageKind,
|
||||||
|
available_s3_storages,
|
||||||
|
)
|
||||||
|
from fixtures.pg_version import PgVersion
|
||||||
|
|
||||||
|
# Generate mock extension files and upload them to the mock bucket.
|
||||||
|
#
|
||||||
|
# NOTE: You must have appropriate AWS credentials to run REAL_S3 test.
|
||||||
|
# It may also be necessary to set the following environment variables for MOCK_S3 test:
|
||||||
|
# export AWS_ACCESS_KEY_ID='test' # export AWS_SECRET_ACCESS_KEY='test'
|
||||||
|
# export AWS_SECURITY_TOKEN='test' # export AWS_SESSION_TOKEN='test'
|
||||||
|
# export AWS_DEFAULT_REGION='us-east-1'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("remote_storage_kind", available_s3_storages())
|
||||||
|
def test_remote_extensions(
|
||||||
|
neon_env_builder: NeonEnvBuilder,
|
||||||
|
remote_storage_kind: RemoteStorageKind,
|
||||||
|
pg_version: PgVersion,
|
||||||
|
):
|
||||||
|
neon_env_builder.enable_remote_storage(
|
||||||
|
remote_storage_kind=remote_storage_kind,
|
||||||
|
test_name="test_remote_extensions",
|
||||||
|
enable_remote_extensions=True,
|
||||||
|
)
|
||||||
|
neon_env_builder.num_safekeepers = 3
|
||||||
|
env = neon_env_builder.init_start()
|
||||||
|
tenant_id, _ = env.neon_cli.create_tenant()
|
||||||
|
env.neon_cli.create_timeline("test_remote_extensions", tenant_id=tenant_id)
|
||||||
|
|
||||||
|
# For MOCK_S3 we upload test files.
|
||||||
|
# For REAL_S3 we use the files already in the bucket
|
||||||
|
if remote_storage_kind == RemoteStorageKind.MOCK_S3:
|
||||||
|
log.info("Uploading test files to mock bucket")
|
||||||
|
|
||||||
|
def upload_test_file(from_path, to_path):
|
||||||
|
assert env.ext_remote_storage is not None # satisfy mypy
|
||||||
|
assert env.remote_storage_client is not None # satisfy mypy
|
||||||
|
with open(
|
||||||
|
f"test_runner/regress/data/extension_test/v{pg_version}/{from_path}", "rb"
|
||||||
|
) as f:
|
||||||
|
env.remote_storage_client.upload_fileobj(
|
||||||
|
f,
|
||||||
|
env.ext_remote_storage.bucket_name,
|
||||||
|
f"ext/v{pg_version}/{to_path}",
|
||||||
|
)
|
||||||
|
|
||||||
|
upload_test_file("ext_index.json", "ext_index.json")
|
||||||
|
upload_test_file("anon.tar.gz", "extensions/anon.tar.gz")
|
||||||
|
upload_test_file("embedding.tar.gz", "extensions/embedding.tar.gz")
|
||||||
|
|
||||||
|
assert env.ext_remote_storage is not None # satisfy mypy
|
||||||
|
assert env.remote_storage_client is not None # satisfy mypy
|
||||||
|
try:
|
||||||
|
# Start a compute node and check that it can download the extensions
|
||||||
|
# and use them to CREATE EXTENSION and LOAD
|
||||||
|
endpoint = env.endpoints.create_start(
|
||||||
|
"test_remote_extensions",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
remote_ext_config=env.ext_remote_storage.to_string(),
|
||||||
|
# config_lines=["log_min_messages=debug3"],
|
||||||
|
)
|
||||||
|
with closing(endpoint.connect()) as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
# Check that appropriate control files were downloaded
|
||||||
|
cur.execute("SELECT * FROM pg_available_extensions")
|
||||||
|
all_extensions = [x[0] for x in cur.fetchall()]
|
||||||
|
log.info(all_extensions)
|
||||||
|
assert "anon" in all_extensions
|
||||||
|
assert "embedding" in all_extensions
|
||||||
|
# TODO: check that we cant't download custom extensions for other tenant ids
|
||||||
|
|
||||||
|
# check that we can download public extension
|
||||||
|
cur.execute("CREATE EXTENSION embedding")
|
||||||
|
cur.execute("SELECT extname FROM pg_extension")
|
||||||
|
assert "embedding" in [x[0] for x in cur.fetchall()]
|
||||||
|
|
||||||
|
# check that we can download private extension
|
||||||
|
try:
|
||||||
|
cur.execute("CREATE EXTENSION anon")
|
||||||
|
except Exception as err:
|
||||||
|
log.info("error creating anon extension")
|
||||||
|
assert "pgcrypto" in str(err), "unexpected error creating anon extension"
|
||||||
|
|
||||||
|
# TODO: try to load libraries as well
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleaning up downloaded files is important for local tests
|
||||||
|
# or else one test could reuse the files from another test or another test run
|
||||||
|
cleanup_files = [
|
||||||
|
"lib/postgresql/anon.so",
|
||||||
|
"lib/postgresql/embedding.so",
|
||||||
|
"share/postgresql/extension/anon.control",
|
||||||
|
"share/postgresql/extension/embedding--0.1.0.sql",
|
||||||
|
"share/postgresql/extension/embedding.control",
|
||||||
|
]
|
||||||
|
cleanup_files = [f"pg_install/v{pg_version}/" + x for x in cleanup_files]
|
||||||
|
cleanup_folders = [
|
||||||
|
"extensions",
|
||||||
|
f"pg_install/v{pg_version}/share/postgresql/extension/anon",
|
||||||
|
f"pg_install/v{pg_version}/extensions",
|
||||||
|
]
|
||||||
|
for file in cleanup_files:
|
||||||
|
try:
|
||||||
|
os.remove(file)
|
||||||
|
log.info(f"removed file {file}")
|
||||||
|
except Exception as err:
|
||||||
|
log.info(f"error removing file {file}: {err}")
|
||||||
|
for folder in cleanup_folders:
|
||||||
|
try:
|
||||||
|
shutil.rmtree(folder)
|
||||||
|
log.info(f"removed folder {folder}")
|
||||||
|
except Exception as err:
|
||||||
|
log.info(f"error removing file {file}: {err}")
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fixtures.neon_fixtures import NeonEnv
|
from fixtures.neon_fixtures import NeonEnv
|
||||||
|
|
||||||
@@ -10,9 +12,10 @@ def test_hot_standby(neon_simple_env: NeonEnv):
|
|||||||
branch_name="main",
|
branch_name="main",
|
||||||
endpoint_id="primary",
|
endpoint_id="primary",
|
||||||
) as primary:
|
) as primary:
|
||||||
|
time.sleep(1)
|
||||||
with env.endpoints.new_replica_start(origin=primary, endpoint_id="secondary") as secondary:
|
with env.endpoints.new_replica_start(origin=primary, endpoint_id="secondary") as secondary:
|
||||||
primary_lsn = None
|
primary_lsn = None
|
||||||
cought_up = False
|
caught_up = False
|
||||||
queries = [
|
queries = [
|
||||||
"SHOW neon.timeline_id",
|
"SHOW neon.timeline_id",
|
||||||
"SHOW neon.tenant_id",
|
"SHOW neon.tenant_id",
|
||||||
@@ -56,7 +59,7 @@ def test_hot_standby(neon_simple_env: NeonEnv):
|
|||||||
res = s_cur.fetchone()
|
res = s_cur.fetchone()
|
||||||
assert res is not None
|
assert res is not None
|
||||||
|
|
||||||
while not cought_up:
|
while not caught_up:
|
||||||
with s_con.cursor() as secondary_cursor:
|
with s_con.cursor() as secondary_cursor:
|
||||||
secondary_cursor.execute("SELECT pg_last_wal_replay_lsn()")
|
secondary_cursor.execute("SELECT pg_last_wal_replay_lsn()")
|
||||||
res = secondary_cursor.fetchone()
|
res = secondary_cursor.fetchone()
|
||||||
@@ -66,7 +69,7 @@ def test_hot_standby(neon_simple_env: NeonEnv):
|
|||||||
# due to e.g. autovacuum, but that shouldn't impact the content
|
# due to e.g. autovacuum, but that shouldn't impact the content
|
||||||
# of the tables, so we check whether we've replayed up to at
|
# of the tables, so we check whether we've replayed up to at
|
||||||
# least after the commit of the `test` table.
|
# least after the commit of the `test` table.
|
||||||
cought_up = secondary_lsn >= primary_lsn
|
caught_up = secondary_lsn >= primary_lsn
|
||||||
|
|
||||||
# Explicit commit to flush any transient transaction-level state.
|
# Explicit commit to flush any transient transaction-level state.
|
||||||
s_con.commit()
|
s_con.commit()
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ def test_basic_eviction(
|
|||||||
for sk in env.safekeepers:
|
for sk in env.safekeepers:
|
||||||
sk.stop()
|
sk.stop()
|
||||||
|
|
||||||
timeline_path = env.repo_dir / "tenants" / str(tenant_id) / "timelines" / str(timeline_id)
|
timeline_path = env.timeline_dir(tenant_id, timeline_id)
|
||||||
initial_local_layers = sorted(
|
initial_local_layers = sorted(
|
||||||
list(filter(lambda path: path.name != "metadata", timeline_path.glob("*")))
|
list(filter(lambda path: path.name != "metadata", timeline_path.glob("*")))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -713,9 +713,7 @@ def test_ondemand_download_failure_to_replace(
|
|||||||
# error message is not useful
|
# error message is not useful
|
||||||
pageserver_http.timeline_detail(tenant_id, timeline_id, True, timeout=2)
|
pageserver_http.timeline_detail(tenant_id, timeline_id, True, timeout=2)
|
||||||
|
|
||||||
actual_message = (
|
actual_message = ".* ERROR .*layermap-replace-notfound"
|
||||||
".* ERROR .*replacing downloaded layer into layermap failed because layer was not found"
|
|
||||||
)
|
|
||||||
assert env.pageserver.log_contains(actual_message) is not None
|
assert env.pageserver.log_contains(actual_message) is not None
|
||||||
env.pageserver.allowed_errors.append(actual_message)
|
env.pageserver.allowed_errors.append(actual_message)
|
||||||
|
|
||||||
|
|||||||
@@ -535,7 +535,7 @@ def test_timeline_deletion_with_files_stuck_in_upload_queue(
|
|||||||
"pitr_interval": "0s",
|
"pitr_interval": "0s",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
timeline_path = env.repo_dir / "tenants" / str(tenant_id) / "timelines" / str(timeline_id)
|
timeline_path = env.timeline_dir(tenant_id, timeline_id)
|
||||||
|
|
||||||
client = env.pageserver.http_client()
|
client = env.pageserver.http_client()
|
||||||
|
|
||||||
|
|||||||
@@ -632,14 +632,14 @@ def test_ignored_tenant_download_missing_layers(
|
|||||||
|
|
||||||
# ignore the tenant and remove its layers
|
# ignore the tenant and remove its layers
|
||||||
pageserver_http.tenant_ignore(tenant_id)
|
pageserver_http.tenant_ignore(tenant_id)
|
||||||
tenant_timeline_dir = env.repo_dir / "tenants" / str(tenant_id) / "timelines" / str(timeline_id)
|
timeline_dir = env.timeline_dir(tenant_id, timeline_id)
|
||||||
layers_removed = False
|
layers_removed = False
|
||||||
for dir_entry in tenant_timeline_dir.iterdir():
|
for dir_entry in timeline_dir.iterdir():
|
||||||
if dir_entry.name.startswith("00000"):
|
if dir_entry.name.startswith("00000"):
|
||||||
# Looks like a layer file. Remove it
|
# Looks like a layer file. Remove it
|
||||||
dir_entry.unlink()
|
dir_entry.unlink()
|
||||||
layers_removed = True
|
layers_removed = True
|
||||||
assert layers_removed, f"Found no layers for tenant {tenant_timeline_dir}"
|
assert layers_removed, f"Found no layers for tenant {timeline_dir}"
|
||||||
|
|
||||||
# now, load it from the local files and expect it to work due to remote storage restoration
|
# now, load it from the local files and expect it to work due to remote storage restoration
|
||||||
pageserver_http.tenant_load(tenant_id=tenant_id)
|
pageserver_http.tenant_load(tenant_id=tenant_id)
|
||||||
@@ -688,14 +688,14 @@ def test_ignored_tenant_stays_broken_without_metadata(
|
|||||||
|
|
||||||
# ignore the tenant and remove its metadata
|
# ignore the tenant and remove its metadata
|
||||||
pageserver_http.tenant_ignore(tenant_id)
|
pageserver_http.tenant_ignore(tenant_id)
|
||||||
tenant_timeline_dir = env.repo_dir / "tenants" / str(tenant_id) / "timelines" / str(timeline_id)
|
timeline_dir = env.timeline_dir(tenant_id, timeline_id)
|
||||||
metadata_removed = False
|
metadata_removed = False
|
||||||
for dir_entry in tenant_timeline_dir.iterdir():
|
for dir_entry in timeline_dir.iterdir():
|
||||||
if dir_entry.name == "metadata":
|
if dir_entry.name == "metadata":
|
||||||
# Looks like a layer file. Remove it
|
# Looks like a layer file. Remove it
|
||||||
dir_entry.unlink()
|
dir_entry.unlink()
|
||||||
metadata_removed = True
|
metadata_removed = True
|
||||||
assert metadata_removed, f"Failed to find metadata file in {tenant_timeline_dir}"
|
assert metadata_removed, f"Failed to find metadata file in {timeline_dir}"
|
||||||
|
|
||||||
env.pageserver.allowed_errors.append(
|
env.pageserver.allowed_errors.append(
|
||||||
f".*{tenant_id}.*: load failed.*: failed to load metadata.*"
|
f".*{tenant_id}.*: load failed.*: failed to load metadata.*"
|
||||||
|
|||||||
@@ -214,9 +214,7 @@ def switch_pg_to_new_pageserver(
|
|||||||
|
|
||||||
endpoint.start()
|
endpoint.start()
|
||||||
|
|
||||||
timeline_to_detach_local_path = (
|
timeline_to_detach_local_path = env.timeline_dir(tenant_id, timeline_id)
|
||||||
env.repo_dir / "tenants" / str(tenant_id) / "timelines" / str(timeline_id)
|
|
||||||
)
|
|
||||||
files_before_detach = os.listdir(timeline_to_detach_local_path)
|
files_before_detach = os.listdir(timeline_to_detach_local_path)
|
||||||
assert (
|
assert (
|
||||||
"metadata" in files_before_detach
|
"metadata" in files_before_detach
|
||||||
@@ -419,8 +417,6 @@ def test_tenant_relocation(
|
|||||||
new_pageserver_http.tenant_attach(tenant_id)
|
new_pageserver_http.tenant_attach(tenant_id)
|
||||||
|
|
||||||
# wait for tenant to finish attaching
|
# wait for tenant to finish attaching
|
||||||
tenant_status = new_pageserver_http.tenant_status(tenant_id=tenant_id)
|
|
||||||
assert tenant_status["state"]["slug"] in ["Attaching", "Active"]
|
|
||||||
wait_until(
|
wait_until(
|
||||||
number_of_iterations=10,
|
number_of_iterations=10,
|
||||||
interval=1,
|
interval=1,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from fixtures.pg_version import PgVersion, xfail_on_postgres
|
|||||||
from fixtures.types import Lsn, TenantId, TimelineId
|
from fixtures.types import Lsn, TenantId, TimelineId
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail
|
||||||
def test_empty_tenant_size(neon_simple_env: NeonEnv, test_output_dir: Path):
|
def test_empty_tenant_size(neon_simple_env: NeonEnv, test_output_dir: Path):
|
||||||
env = neon_simple_env
|
env = neon_simple_env
|
||||||
(tenant_id, _) = env.neon_cli.create_tenant()
|
(tenant_id, _) = env.neon_cli.create_tenant()
|
||||||
@@ -44,12 +45,16 @@ def test_empty_tenant_size(neon_simple_env: NeonEnv, test_output_dir: Path):
|
|||||||
# we've disabled the autovacuum and checkpoint
|
# we've disabled the autovacuum and checkpoint
|
||||||
# so background processes should not change the size.
|
# so background processes should not change the size.
|
||||||
# If this test will flake we should probably loosen the check
|
# If this test will flake we should probably loosen the check
|
||||||
assert size == initial_size, "starting idle compute should not change the tenant size"
|
assert (
|
||||||
|
size == initial_size
|
||||||
|
), f"starting idle compute should not change the tenant size (Currently {size}, expected {initial_size})"
|
||||||
|
|
||||||
# the size should be the same, until we increase the size over the
|
# the size should be the same, until we increase the size over the
|
||||||
# gc_horizon
|
# gc_horizon
|
||||||
size, inputs = http_client.tenant_size_and_modelinputs(tenant_id)
|
size, inputs = http_client.tenant_size_and_modelinputs(tenant_id)
|
||||||
assert size == initial_size, "tenant_size should not be affected by shutdown of compute"
|
assert (
|
||||||
|
size == initial_size
|
||||||
|
), f"tenant_size should not be affected by shutdown of compute (Currently {size}, expected {initial_size})"
|
||||||
|
|
||||||
expected_inputs = {
|
expected_inputs = {
|
||||||
"segments": [
|
"segments": [
|
||||||
@@ -318,6 +323,7 @@ def test_only_heads_within_horizon(neon_simple_env: NeonEnv, test_output_dir: Pa
|
|||||||
size_debug_file.write(size_debug)
|
size_debug_file.write(size_debug)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail
|
||||||
def test_single_branch_get_tenant_size_grows(
|
def test_single_branch_get_tenant_size_grows(
|
||||||
neon_env_builder: NeonEnvBuilder, test_output_dir: Path, pg_version: PgVersion
|
neon_env_builder: NeonEnvBuilder, test_output_dir: Path, pg_version: PgVersion
|
||||||
):
|
):
|
||||||
@@ -333,13 +339,13 @@ def test_single_branch_get_tenant_size_grows(
|
|||||||
# inserts is larger than gc_horizon. for example 0x20000 here hid the fact
|
# inserts is larger than gc_horizon. for example 0x20000 here hid the fact
|
||||||
# that there next_gc_cutoff could be smaller than initdb_lsn, which will
|
# that there next_gc_cutoff could be smaller than initdb_lsn, which will
|
||||||
# obviously lead to issues when calculating the size.
|
# obviously lead to issues when calculating the size.
|
||||||
gc_horizon = 0x38000
|
gc_horizon = 0x3BA00
|
||||||
|
|
||||||
# it's a bit of a hack, but different versions of postgres have different
|
# it's a bit of a hack, but different versions of postgres have different
|
||||||
# amount of WAL generated for the same amount of data. so we need to
|
# amount of WAL generated for the same amount of data. so we need to
|
||||||
# adjust the gc_horizon accordingly.
|
# adjust the gc_horizon accordingly.
|
||||||
if pg_version == PgVersion.V14:
|
if pg_version == PgVersion.V14:
|
||||||
gc_horizon = 0x40000
|
gc_horizon = 0x4A000
|
||||||
|
|
||||||
neon_env_builder.pageserver_config_override = f"tenant_config={{compaction_period='0s', gc_period='0s', pitr_interval='0sec', gc_horizon={gc_horizon}}}"
|
neon_env_builder.pageserver_config_override = f"tenant_config={{compaction_period='0s', gc_period='0s', pitr_interval='0sec', gc_horizon={gc_horizon}}}"
|
||||||
|
|
||||||
@@ -360,11 +366,11 @@ def test_single_branch_get_tenant_size_grows(
|
|||||||
if current_lsn - initdb_lsn >= gc_horizon:
|
if current_lsn - initdb_lsn >= gc_horizon:
|
||||||
assert (
|
assert (
|
||||||
size >= prev_size
|
size >= prev_size
|
||||||
), "tenant_size may grow or not grow, because we only add gc_horizon amount of WAL to initial snapshot size"
|
), f"tenant_size may grow or not grow, because we only add gc_horizon amount of WAL to initial snapshot size (Currently at: {current_lsn}, Init at: {initdb_lsn})"
|
||||||
else:
|
else:
|
||||||
assert (
|
assert (
|
||||||
size > prev_size
|
size > prev_size
|
||||||
), "tenant_size should grow, because we continue to add WAL to initial snapshot size"
|
), f"tenant_size should grow, because we continue to add WAL to initial snapshot size (Currently at: {current_lsn}, Init at: {initdb_lsn})"
|
||||||
|
|
||||||
def get_current_consistent_size(
|
def get_current_consistent_size(
|
||||||
env: NeonEnv,
|
env: NeonEnv,
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ def test_tenant_redownloads_truncated_file_on_startup(
|
|||||||
env.endpoints.stop_all()
|
env.endpoints.stop_all()
|
||||||
env.pageserver.stop()
|
env.pageserver.stop()
|
||||||
|
|
||||||
timeline_dir = Path(env.repo_dir) / "tenants" / str(tenant_id) / "timelines" / str(timeline_id)
|
timeline_dir = env.timeline_dir(tenant_id, timeline_id)
|
||||||
local_layer_truncated = None
|
local_layer_truncated = None
|
||||||
for path in Path.iterdir(timeline_dir):
|
for path in Path.iterdir(timeline_dir):
|
||||||
if path.name.startswith("00000"):
|
if path.name.startswith("00000"):
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ def assert_prefix_empty(neon_env_builder: NeonEnvBuilder, prefix: Optional[str]
|
|||||||
assert isinstance(neon_env_builder.remote_storage, S3Storage)
|
assert isinstance(neon_env_builder.remote_storage, S3Storage)
|
||||||
|
|
||||||
# Note that this doesnt use pagination, so list is not guaranteed to be exhaustive.
|
# Note that this doesnt use pagination, so list is not guaranteed to be exhaustive.
|
||||||
|
assert neon_env_builder.remote_storage_client is not None
|
||||||
response = neon_env_builder.remote_storage_client.list_objects_v2(
|
response = neon_env_builder.remote_storage_client.list_objects_v2(
|
||||||
Bucket=neon_env_builder.remote_storage.bucket_name,
|
Bucket=neon_env_builder.remote_storage.bucket_name,
|
||||||
Prefix=prefix or neon_env_builder.remote_storage.prefix_in_bucket or "",
|
Prefix=prefix or neon_env_builder.remote_storage.prefix_in_bucket or "",
|
||||||
@@ -463,10 +464,10 @@ def test_concurrent_timeline_delete_stuck_on(
|
|||||||
|
|
||||||
# make the second call and assert behavior
|
# make the second call and assert behavior
|
||||||
log.info("second call start")
|
log.info("second call start")
|
||||||
error_msg_re = "timeline deletion is already in progress"
|
error_msg_re = "Timeline deletion is already in progress"
|
||||||
with pytest.raises(PageserverApiException, match=error_msg_re) as second_call_err:
|
with pytest.raises(PageserverApiException, match=error_msg_re) as second_call_err:
|
||||||
ps_http.timeline_delete(env.initial_tenant, child_timeline_id)
|
ps_http.timeline_delete(env.initial_tenant, child_timeline_id)
|
||||||
assert second_call_err.value.status_code == 500
|
assert second_call_err.value.status_code == 409
|
||||||
env.pageserver.allowed_errors.append(f".*{child_timeline_id}.*{error_msg_re}.*")
|
env.pageserver.allowed_errors.append(f".*{child_timeline_id}.*{error_msg_re}.*")
|
||||||
# the second call will try to transition the timeline into Stopping state as well
|
# the second call will try to transition the timeline into Stopping state as well
|
||||||
env.pageserver.allowed_errors.append(
|
env.pageserver.allowed_errors.append(
|
||||||
@@ -518,9 +519,9 @@ def test_delete_timeline_client_hangup(neon_env_builder: NeonEnvBuilder):
|
|||||||
ps_http.timeline_delete(env.initial_tenant, child_timeline_id, timeout=2)
|
ps_http.timeline_delete(env.initial_tenant, child_timeline_id, timeout=2)
|
||||||
|
|
||||||
env.pageserver.allowed_errors.append(
|
env.pageserver.allowed_errors.append(
|
||||||
f".*{child_timeline_id}.*timeline deletion is already in progress.*"
|
f".*{child_timeline_id}.*Timeline deletion is already in progress.*"
|
||||||
)
|
)
|
||||||
with pytest.raises(PageserverApiException, match="timeline deletion is already in progress"):
|
with pytest.raises(PageserverApiException, match="Timeline deletion is already in progress"):
|
||||||
ps_http.timeline_delete(env.initial_tenant, child_timeline_id, timeout=2)
|
ps_http.timeline_delete(env.initial_tenant, child_timeline_id, timeout=2)
|
||||||
|
|
||||||
# make sure the timeout was due to the failpoint
|
# make sure the timeout was due to the failpoint
|
||||||
@@ -628,7 +629,7 @@ def test_timeline_delete_works_for_remote_smoke(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# for some reason the check above doesnt immediately take effect for the below.
|
# for some reason the check above doesnt immediately take effect for the below.
|
||||||
# Assume it is mock server incosistency and check twice.
|
# Assume it is mock server inconsistency and check twice.
|
||||||
wait_until(
|
wait_until(
|
||||||
2,
|
2,
|
||||||
0.5,
|
0.5,
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ def test_timeline_physical_size_post_compaction(
|
|||||||
wait_for_last_flush_lsn(env, endpoint, env.initial_tenant, new_timeline_id)
|
wait_for_last_flush_lsn(env, endpoint, env.initial_tenant, new_timeline_id)
|
||||||
|
|
||||||
# shutdown safekeepers to prevent new data from coming in
|
# shutdown safekeepers to prevent new data from coming in
|
||||||
|
endpoint.stop() # We can't gracefully stop after safekeepers die
|
||||||
for sk in env.safekeepers:
|
for sk in env.safekeepers:
|
||||||
sk.stop()
|
sk.stop()
|
||||||
|
|
||||||
|
|||||||
@@ -1210,6 +1210,10 @@ def test_delete_force(neon_env_builder: NeonEnvBuilder, auth_enabled: bool):
|
|||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("INSERT INTO t (key) VALUES (1)")
|
cur.execute("INSERT INTO t (key) VALUES (1)")
|
||||||
|
|
||||||
|
# Stop all computes gracefully before safekeepers stop responding to them
|
||||||
|
endpoint_1.stop_and_destroy()
|
||||||
|
endpoint_3.stop_and_destroy()
|
||||||
|
|
||||||
# Remove initial tenant's br1 (active)
|
# Remove initial tenant's br1 (active)
|
||||||
assert sk_http.timeline_delete_force(tenant_id, timeline_id_1)["dir_existed"]
|
assert sk_http.timeline_delete_force(tenant_id, timeline_id_1)["dir_existed"]
|
||||||
assert not (sk_data_dir / str(tenant_id) / str(timeline_id_1)).exists()
|
assert not (sk_data_dir / str(tenant_id) / str(timeline_id_1)).exists()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user