mirror of
https://github.com/neondatabase/neon.git
synced 2026-08-18 03:58:19 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 136ed19387 |
@@ -1,2 +1,2 @@
|
|||||||
[profile.default]
|
[profile.default]
|
||||||
slow-timeout = { period = "60s", terminate-after = 3 }
|
slow-timeout = { period = "20s", terminate-after = 3 }
|
||||||
|
|||||||
+1
-4
@@ -8,7 +8,6 @@
|
|||||||
!scripts/combine_control_files.py
|
!scripts/combine_control_files.py
|
||||||
!scripts/ninstall.sh
|
!scripts/ninstall.sh
|
||||||
!vm-cgconfig.conf
|
!vm-cgconfig.conf
|
||||||
!docker-compose/run-tests.sh
|
|
||||||
|
|
||||||
# Directories
|
# Directories
|
||||||
!.cargo/
|
!.cargo/
|
||||||
@@ -18,13 +17,11 @@
|
|||||||
!libs/
|
!libs/
|
||||||
!neon_local/
|
!neon_local/
|
||||||
!pageserver/
|
!pageserver/
|
||||||
!patches/
|
|
||||||
!pgxn/
|
!pgxn/
|
||||||
!proxy/
|
!proxy/
|
||||||
!storage_scrubber/
|
!s3_scrubber/
|
||||||
!safekeeper/
|
!safekeeper/
|
||||||
!storage_broker/
|
!storage_broker/
|
||||||
!storage_controller/
|
|
||||||
!trace/
|
!trace/
|
||||||
!vendor/postgres-*/
|
!vendor/postgres-*/
|
||||||
!workspace_hack/
|
!workspace_hack/
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
self-hosted-runner:
|
self-hosted-runner:
|
||||||
labels:
|
labels:
|
||||||
- arm64
|
- arm64
|
||||||
|
- dev
|
||||||
- gen3
|
- gen3
|
||||||
- large
|
- large
|
||||||
- large-arm64
|
# Remove `macos-14` from the list after https://github.com/rhysd/actionlint/pull/392 is merged.
|
||||||
|
- macos-14
|
||||||
- small
|
- small
|
||||||
- small-arm64
|
|
||||||
- us-east-2
|
- us-east-2
|
||||||
config-variables:
|
config-variables:
|
||||||
- REMOTE_STORAGE_AZURE_CONTAINER
|
- REMOTE_STORAGE_AZURE_CONTAINER
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ runs:
|
|||||||
|
|
||||||
# Use aws s3 cp (instead of aws s3 sync) to keep files from previous runs to make old URLs work,
|
# Use aws s3 cp (instead of aws s3 sync) to keep files from previous runs to make old URLs work,
|
||||||
# and to keep files on the host to upload them to the database
|
# and to keep files on the host to upload them to the database
|
||||||
time s5cmd --log error cp "${WORKDIR}/report/*" "s3://${BUCKET}/${REPORT_PREFIX}/${GITHUB_RUN_ID}/"
|
time aws s3 cp --recursive --only-show-errors "${WORKDIR}/report" "s3://${BUCKET}/${REPORT_PREFIX}/${GITHUB_RUN_ID}"
|
||||||
|
|
||||||
# Generate redirect
|
# Generate redirect
|
||||||
cat <<EOF > ${WORKDIR}/index.html
|
cat <<EOF > ${WORKDIR}/index.html
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ description: 'Create Branch using API'
|
|||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
api_key:
|
api_key:
|
||||||
description: 'Neon API key'
|
desctiption: 'Neon API key'
|
||||||
required: true
|
required: true
|
||||||
project_id:
|
project_id:
|
||||||
description: 'ID of the Project to create Branch in'
|
desctiption: 'ID of the Project to create Branch in'
|
||||||
required: true
|
required: true
|
||||||
api_host:
|
api_host:
|
||||||
description: 'Neon API host'
|
desctiption: 'Neon API host'
|
||||||
default: console-stage.neon.build
|
default: console.stage.neon.tech
|
||||||
outputs:
|
outputs:
|
||||||
dsn:
|
dsn:
|
||||||
description: 'Created Branch DSN (for main database)'
|
description: 'Created Branch DSN (for main database)'
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ description: 'Delete Branch using API'
|
|||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
api_key:
|
api_key:
|
||||||
description: 'Neon API key'
|
desctiption: 'Neon API key'
|
||||||
required: true
|
required: true
|
||||||
project_id:
|
project_id:
|
||||||
description: 'ID of the Project which should be deleted'
|
desctiption: 'ID of the Project which should be deleted'
|
||||||
required: true
|
required: true
|
||||||
branch_id:
|
branch_id:
|
||||||
description: 'ID of the branch to delete'
|
desctiption: 'ID of the branch to delete'
|
||||||
required: true
|
required: true
|
||||||
api_host:
|
api_host:
|
||||||
description: 'Neon API host'
|
desctiption: 'Neon API host'
|
||||||
default: console-stage.neon.build
|
default: console.stage.neon.tech
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
|
|||||||
@@ -3,22 +3,22 @@ description: 'Create Neon Project using API'
|
|||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
api_key:
|
api_key:
|
||||||
description: 'Neon API key'
|
desctiption: 'Neon API key'
|
||||||
required: true
|
required: true
|
||||||
region_id:
|
region_id:
|
||||||
description: 'Region ID, if not set the project will be created in the default region'
|
desctiption: 'Region ID, if not set the project will be created in the default region'
|
||||||
default: aws-us-east-2
|
default: aws-us-east-2
|
||||||
postgres_version:
|
postgres_version:
|
||||||
description: 'Postgres version; default is 15'
|
desctiption: 'Postgres version; default is 15'
|
||||||
default: '15'
|
default: 15
|
||||||
api_host:
|
api_host:
|
||||||
description: 'Neon API host'
|
desctiption: 'Neon API host'
|
||||||
default: console-stage.neon.build
|
default: console.stage.neon.tech
|
||||||
provisioner:
|
provisioner:
|
||||||
description: 'k8s-pod or k8s-neonvm'
|
desctiption: 'k8s-pod or k8s-neonvm'
|
||||||
default: 'k8s-pod'
|
default: 'k8s-pod'
|
||||||
compute_units:
|
compute_units:
|
||||||
description: '[Min, Max] compute units; Min and Max are used for k8s-neonvm with autoscaling, for k8s-pod values Min and Max should be equal'
|
desctiption: '[Min, Max] compute units; Min and Max are used for k8s-neonvm with autoscaling, for k8s-pod values Min and Max should be equal'
|
||||||
default: '[1, 1]'
|
default: '[1, 1]'
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ description: 'Delete Neon Project using API'
|
|||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
api_key:
|
api_key:
|
||||||
description: 'Neon API key'
|
desctiption: 'Neon API key'
|
||||||
required: true
|
required: true
|
||||||
project_id:
|
project_id:
|
||||||
description: 'ID of the Project to delete'
|
desctiption: 'ID of the Project to delete'
|
||||||
required: true
|
required: true
|
||||||
api_host:
|
api_host:
|
||||||
description: 'Neon API host'
|
desctiption: 'Neon API host'
|
||||||
default: console-stage.neon.build
|
default: console.stage.neon.tech
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
|
|||||||
@@ -183,7 +183,8 @@ runs:
|
|||||||
|
|
||||||
# Run the tests.
|
# Run the tests.
|
||||||
#
|
#
|
||||||
# --alluredir saves test results in Allure format (in a specified directory)
|
# The junit.xml file allows CI tools to display more fine-grained test information
|
||||||
|
# in its "Tests" tab in the results page.
|
||||||
# --verbose prints name of each test (helpful when there are
|
# --verbose prints name of each test (helpful when there are
|
||||||
# multiple tests in one file)
|
# multiple tests in one file)
|
||||||
# -rA prints summary in the end
|
# -rA prints summary in the end
|
||||||
@@ -192,6 +193,7 @@ runs:
|
|||||||
#
|
#
|
||||||
mkdir -p $TEST_OUTPUT/allure/results
|
mkdir -p $TEST_OUTPUT/allure/results
|
||||||
"${cov_prefix[@]}" ./scripts/pytest \
|
"${cov_prefix[@]}" ./scripts/pytest \
|
||||||
|
--junitxml=$TEST_OUTPUT/junit.xml \
|
||||||
--alluredir=$TEST_OUTPUT/allure/results \
|
--alluredir=$TEST_OUTPUT/allure/results \
|
||||||
--tb=short \
|
--tb=short \
|
||||||
--verbose \
|
--verbose \
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
|
|
||||||
actionlint:
|
actionlint:
|
||||||
needs: [ check-permissions ]
|
needs: [ check-permissions ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: reviewdog/action-actionlint@v1
|
- uses: reviewdog/action-actionlint@v1
|
||||||
@@ -36,16 +36,3 @@ jobs:
|
|||||||
fail_on_error: true
|
fail_on_error: true
|
||||||
filter_mode: nofilter
|
filter_mode: nofilter
|
||||||
level: error
|
level: error
|
||||||
|
|
||||||
- name: Disallow 'ubuntu-latest' runners
|
|
||||||
run: |
|
|
||||||
PAT='^\s*runs-on:.*-latest'
|
|
||||||
if grep -ERq $PAT .github/workflows; then
|
|
||||||
grep -ERl $PAT .github/workflows |\
|
|
||||||
while read -r f
|
|
||||||
do
|
|
||||||
l=$(grep -nE $PAT .github/workflows/release.yml | awk -F: '{print $1}' | head -1)
|
|
||||||
echo "::error file=$f,line=$l::Please use 'ubuntu-22.04' instead of 'ubuntu-latest'"
|
|
||||||
done
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ on:
|
|||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -44,7 +43,7 @@ jobs:
|
|||||||
contains(fromJSON('["opened", "synchronize", "reopened", "closed"]'), github.event.action) &&
|
contains(fromJSON('["opened", "synchronize", "reopened", "closed"]'), github.event.action) &&
|
||||||
contains(github.event.pull_request.labels.*.name, 'approved-for-ci-run')
|
contains(github.event.pull_request.labels.*.name, 'approved-for-ci-run')
|
||||||
|
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- run: gh pr --repo "${GITHUB_REPOSITORY}" edit "${PR_NUMBER}" --remove-label "approved-for-ci-run"
|
- run: gh pr --repo "${GITHUB_REPOSITORY}" edit "${PR_NUMBER}" --remove-label "approved-for-ci-run"
|
||||||
@@ -60,7 +59,7 @@ jobs:
|
|||||||
github.event.action == 'labeled' &&
|
github.event.action == 'labeled' &&
|
||||||
contains(github.event.pull_request.labels.*.name, 'approved-for-ci-run')
|
contains(github.event.pull_request.labels.*.name, 'approved-for-ci-run')
|
||||||
|
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- run: gh pr --repo "${GITHUB_REPOSITORY}" edit "${PR_NUMBER}" --remove-label "approved-for-ci-run"
|
- run: gh pr --repo "${GITHUB_REPOSITORY}" edit "${PR_NUMBER}" --remove-label "approved-for-ci-run"
|
||||||
@@ -69,41 +68,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
token: ${{ secrets.CI_ACCESS_TOKEN }}
|
token: ${{ secrets.CI_ACCESS_TOKEN }}
|
||||||
|
|
||||||
- name: Look for existing PR
|
|
||||||
id: get-pr
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
|
||||||
run: |
|
|
||||||
ALREADY_CREATED="$(gh pr --repo ${GITHUB_REPOSITORY} list --head ${BRANCH} --base main --json number --jq '.[].number')"
|
|
||||||
echo "ALREADY_CREATED=${ALREADY_CREATED}" >> ${GITHUB_OUTPUT}
|
|
||||||
|
|
||||||
- name: Get changed labels
|
|
||||||
id: get-labels
|
|
||||||
if: steps.get-pr.outputs.ALREADY_CREATED != ''
|
|
||||||
env:
|
|
||||||
ALREADY_CREATED: ${{ steps.get-pr.outputs.ALREADY_CREATED }}
|
|
||||||
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
|
||||||
run: |
|
|
||||||
LABELS_TO_REMOVE=$(comm -23 <(gh pr --repo ${GITHUB_REPOSITORY} view ${ALREADY_CREATED} --json labels --jq '.labels.[].name'| ( grep -E '^run' || true ) | sort) \
|
|
||||||
<(gh pr --repo ${GITHUB_REPOSITORY} view ${PR_NUMBER} --json labels --jq '.labels.[].name' | ( grep -E '^run' || true ) | sort ) |\
|
|
||||||
( grep -v run-e2e-tests-in-draft || true ) | paste -sd , -)
|
|
||||||
LABELS_TO_ADD=$(comm -13 <(gh pr --repo ${GITHUB_REPOSITORY} view ${ALREADY_CREATED} --json labels --jq '.labels.[].name'| ( grep -E '^run' || true ) |sort) \
|
|
||||||
<(gh pr --repo ${GITHUB_REPOSITORY} view ${PR_NUMBER} --json labels --jq '.labels.[].name' | ( grep -E '^run' || true ) | sort ) |\
|
|
||||||
paste -sd , -)
|
|
||||||
echo "LABELS_TO_ADD=${LABELS_TO_ADD}" >> ${GITHUB_OUTPUT}
|
|
||||||
echo "LABELS_TO_REMOVE=${LABELS_TO_REMOVE}" >> ${GITHUB_OUTPUT}
|
|
||||||
|
|
||||||
- run: gh pr checkout "${PR_NUMBER}"
|
- run: gh pr checkout "${PR_NUMBER}"
|
||||||
|
|
||||||
- run: git checkout -b "${BRANCH}"
|
- run: git checkout -b "${BRANCH}"
|
||||||
|
|
||||||
- run: git push --force origin "${BRANCH}"
|
- run: git push --force origin "${BRANCH}"
|
||||||
if: steps.get-pr.outputs.ALREADY_CREATED == ''
|
|
||||||
|
|
||||||
- name: Create a Pull Request for CI run (if required)
|
- name: Create a Pull Request for CI run (if required)
|
||||||
if: steps.get-pr.outputs.ALREADY_CREATED == ''
|
env:
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
cat << EOF > body.md
|
cat << EOF > body.md
|
||||||
@@ -114,33 +87,16 @@ jobs:
|
|||||||
Feel free to review/comment/discuss the original PR #${PR_NUMBER}.
|
Feel free to review/comment/discuss the original PR #${PR_NUMBER}.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
LABELS=$( (gh pr --repo "${GITHUB_REPOSITORY}" view ${PR_NUMBER} --json labels --jq '.labels.[].name'; echo run-e2e-tests-in-draft )| \
|
ALREADY_CREATED="$(gh pr --repo ${GITHUB_REPOSITORY} list --head ${BRANCH} --base main --json number --jq '.[].number')"
|
||||||
grep -E '^run' | paste -sd , -)
|
if [ -z "${ALREADY_CREATED}" ]; then
|
||||||
gh pr --repo "${GITHUB_REPOSITORY}" create --title "CI run for PR #${PR_NUMBER}" \
|
gh pr --repo "${GITHUB_REPOSITORY}" create --title "CI run for PR #${PR_NUMBER}" \
|
||||||
--body-file "body.md" \
|
--body-file "body.md" \
|
||||||
--head "${BRANCH}" \
|
--head "${BRANCH}" \
|
||||||
--base "main" \
|
--base "main" \
|
||||||
--label ${LABELS} \
|
--label "run-e2e-tests-in-draft" \
|
||||||
--draft
|
--draft
|
||||||
- name: Modify the existing pull request (if required)
|
|
||||||
if: steps.get-pr.outputs.ALREADY_CREATED != ''
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
LABELS_TO_ADD: ${{ steps.get-labels.outputs.LABELS_TO_ADD }}
|
|
||||||
LABELS_TO_REMOVE: ${{ steps.get-labels.outputs.LABELS_TO_REMOVE }}
|
|
||||||
ALREADY_CREATED: ${{ steps.get-pr.outputs.ALREADY_CREATED }}
|
|
||||||
run: |
|
|
||||||
ADD_CMD=
|
|
||||||
REMOVE_CMD=
|
|
||||||
[ -z "${LABELS_TO_ADD}" ] || ADD_CMD="--add-label ${LABELS_TO_ADD}"
|
|
||||||
[ -z "${LABELS_TO_REMOVE}" ] || REMOVE_CMD="--remove-label ${LABELS_TO_REMOVE}"
|
|
||||||
if [ -n "${ADD_CMD}" ] || [ -n "${REMOVE_CMD}" ]; then
|
|
||||||
gh pr --repo "${GITHUB_REPOSITORY}" edit ${ALREADY_CREATED} ${ADD_CMD} ${REMOVE_CMD}
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- run: git push --force origin "${BRANCH}"
|
|
||||||
if: steps.get-pr.outputs.ALREADY_CREATED != ''
|
|
||||||
|
|
||||||
cleanup:
|
cleanup:
|
||||||
# Close PRs and delete branchs if the original PR is closed.
|
# Close PRs and delete branchs if the original PR is closed.
|
||||||
|
|
||||||
@@ -152,7 +108,7 @@ jobs:
|
|||||||
github.event.action == 'closed' &&
|
github.event.action == 'closed' &&
|
||||||
github.event.pull_request.head.repo.full_name != github.repository
|
github.event.pull_request.head.repo.full_name != github.repository
|
||||||
|
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Close PR and delete `ci-run/pr-${{ env.PR_NUMBER }}` branch
|
- name: Close PR and delete `ci-run/pr-${{ env.PR_NUMBER }}` branch
|
||||||
|
|||||||
@@ -38,11 +38,6 @@ on:
|
|||||||
description: 'AWS-RDS and AWS-AURORA normally only run on Saturday. Set this to true to run them on every workflow_dispatch'
|
description: 'AWS-RDS and AWS-AURORA normally only run on Saturday. Set this to true to run them on every workflow_dispatch'
|
||||||
required: false
|
required: false
|
||||||
default: false
|
default: false
|
||||||
run_only_pgvector_tests:
|
|
||||||
type: boolean
|
|
||||||
description: 'Run pgvector tests but no other tests. If not set, all tests including pgvector tests will be run'
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -55,7 +50,6 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bench:
|
bench:
|
||||||
if: ${{ github.event.inputs.run_only_pgvector_tests == 'false' || github.event.inputs.run_only_pgvector_tests == null }}
|
|
||||||
env:
|
env:
|
||||||
TEST_PG_BENCH_DURATIONS_MATRIX: "300"
|
TEST_PG_BENCH_DURATIONS_MATRIX: "300"
|
||||||
TEST_PG_BENCH_SCALES_MATRIX: "10,100"
|
TEST_PG_BENCH_SCALES_MATRIX: "10,100"
|
||||||
@@ -99,7 +93,7 @@ jobs:
|
|||||||
# Set --sparse-ordering option of pytest-order plugin
|
# Set --sparse-ordering option of pytest-order plugin
|
||||||
# to ensure tests are running in order of appears in the file.
|
# to ensure tests are running in order of appears in the file.
|
||||||
# It's important for test_perf_pgbench.py::test_pgbench_remote_* tests
|
# It's important for test_perf_pgbench.py::test_pgbench_remote_* tests
|
||||||
extra_params: -m remote_cluster --sparse-ordering --timeout 5400 --ignore test_runner/performance/test_perf_olap.py --ignore test_runner/performance/test_perf_pgvector_queries.py
|
extra_params: -m remote_cluster --sparse-ordering --timeout 5400 --ignore test_runner/performance/test_perf_olap.py
|
||||||
env:
|
env:
|
||||||
BENCHMARK_CONNSTR: ${{ steps.create-neon-project.outputs.dsn }}
|
BENCHMARK_CONNSTR: ${{ steps.create-neon-project.outputs.dsn }}
|
||||||
VIP_VAP_ACCESS_TOKEN: "${{ secrets.VIP_VAP_ACCESS_TOKEN }}"
|
VIP_VAP_ACCESS_TOKEN: "${{ secrets.VIP_VAP_ACCESS_TOKEN }}"
|
||||||
@@ -126,7 +120,6 @@ jobs:
|
|||||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||||
|
|
||||||
generate-matrices:
|
generate-matrices:
|
||||||
if: ${{ github.event.inputs.run_only_pgvector_tests == 'false' || github.event.inputs.run_only_pgvector_tests == null }}
|
|
||||||
# Create matrices for the benchmarking jobs, so we run benchmarks on rds only once a week (on Saturday)
|
# Create matrices for the benchmarking jobs, so we run benchmarks on rds only once a week (on Saturday)
|
||||||
#
|
#
|
||||||
# Available platforms:
|
# Available platforms:
|
||||||
@@ -137,7 +130,7 @@ jobs:
|
|||||||
# - rds-postgres: RDS Postgres db.m5.large instance (2 vCPU, 8 GiB) with gp3 EBS storage
|
# - rds-postgres: RDS Postgres db.m5.large instance (2 vCPU, 8 GiB) with gp3 EBS storage
|
||||||
env:
|
env:
|
||||||
RUN_AWS_RDS_AND_AURORA: ${{ github.event.inputs.run_AWS_RDS_AND_AURORA || 'false' }}
|
RUN_AWS_RDS_AND_AURORA: ${{ github.event.inputs.run_AWS_RDS_AND_AURORA || 'false' }}
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
pgbench-compare-matrix: ${{ steps.pgbench-compare-matrix.outputs.matrix }}
|
pgbench-compare-matrix: ${{ steps.pgbench-compare-matrix.outputs.matrix }}
|
||||||
olap-compare-matrix: ${{ steps.olap-compare-matrix.outputs.matrix }}
|
olap-compare-matrix: ${{ steps.olap-compare-matrix.outputs.matrix }}
|
||||||
@@ -154,16 +147,15 @@ jobs:
|
|||||||
"neonvm-captest-new"
|
"neonvm-captest-new"
|
||||||
],
|
],
|
||||||
"db_size": [ "10gb" ],
|
"db_size": [ "10gb" ],
|
||||||
"include": [{ "platform": "neon-captest-freetier", "db_size": "3gb" },
|
"include": [{ "platform": "neon-captest-freetier", "db_size": "3gb" },
|
||||||
{ "platform": "neon-captest-new", "db_size": "50gb" },
|
{ "platform": "neon-captest-new", "db_size": "50gb" },
|
||||||
{ "platform": "neonvm-captest-freetier", "db_size": "3gb" },
|
{ "platform": "neonvm-captest-freetier", "db_size": "3gb" },
|
||||||
{ "platform": "neonvm-captest-new", "db_size": "50gb" },
|
{ "platform": "neonvm-captest-new", "db_size": "50gb" }]
|
||||||
{ "platform": "neonvm-captest-sharding-reuse", "db_size": "50gb" }]
|
|
||||||
}'
|
}'
|
||||||
|
|
||||||
if [ "$(date +%A)" = "Saturday" ]; then
|
if [ "$(date +%A)" = "Saturday" ]; then
|
||||||
matrix=$(echo "$matrix" | jq '.include += [{ "platform": "rds-postgres", "db_size": "10gb"},
|
matrix=$(echo "$matrix" | jq '.include += [{ "platform": "rds-postgres", "db_size": "10gb"},
|
||||||
{ "platform": "rds-aurora", "db_size": "50gb"}]')
|
{ "platform": "rds-aurora", "db_size": "50gb"}]')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "matrix=$(echo "$matrix" | jq --compact-output '.')" >> $GITHUB_OUTPUT
|
echo "matrix=$(echo "$matrix" | jq --compact-output '.')" >> $GITHUB_OUTPUT
|
||||||
@@ -179,7 +171,7 @@ jobs:
|
|||||||
|
|
||||||
if [ "$(date +%A)" = "Saturday" ] || [ ${RUN_AWS_RDS_AND_AURORA} = "true" ]; then
|
if [ "$(date +%A)" = "Saturday" ] || [ ${RUN_AWS_RDS_AND_AURORA} = "true" ]; then
|
||||||
matrix=$(echo "$matrix" | jq '.include += [{ "platform": "rds-postgres" },
|
matrix=$(echo "$matrix" | jq '.include += [{ "platform": "rds-postgres" },
|
||||||
{ "platform": "rds-aurora" }]')
|
{ "platform": "rds-aurora" }]')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "matrix=$(echo "$matrix" | jq --compact-output '.')" >> $GITHUB_OUTPUT
|
echo "matrix=$(echo "$matrix" | jq --compact-output '.')" >> $GITHUB_OUTPUT
|
||||||
@@ -198,13 +190,12 @@ jobs:
|
|||||||
|
|
||||||
if [ "$(date +%A)" = "Saturday" ] || [ ${RUN_AWS_RDS_AND_AURORA} = "true" ]; then
|
if [ "$(date +%A)" = "Saturday" ] || [ ${RUN_AWS_RDS_AND_AURORA} = "true" ]; then
|
||||||
matrix=$(echo "$matrix" | jq '.include += [{ "platform": "rds-postgres", "scale": "10" },
|
matrix=$(echo "$matrix" | jq '.include += [{ "platform": "rds-postgres", "scale": "10" },
|
||||||
{ "platform": "rds-aurora", "scale": "10" }]')
|
{ "platform": "rds-aurora", "scale": "10" }]')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "matrix=$(echo "$matrix" | jq --compact-output '.')" >> $GITHUB_OUTPUT
|
echo "matrix=$(echo "$matrix" | jq --compact-output '.')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
pgbench-compare:
|
pgbench-compare:
|
||||||
if: ${{ github.event.inputs.run_only_pgvector_tests == 'false' || github.event.inputs.run_only_pgvector_tests == null }}
|
|
||||||
needs: [ generate-matrices ]
|
needs: [ generate-matrices ]
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
@@ -262,9 +253,6 @@ jobs:
|
|||||||
neon-captest-reuse)
|
neon-captest-reuse)
|
||||||
CONNSTR=${{ secrets.BENCHMARK_CAPTEST_CONNSTR }}
|
CONNSTR=${{ secrets.BENCHMARK_CAPTEST_CONNSTR }}
|
||||||
;;
|
;;
|
||||||
neonvm-captest-sharding-reuse)
|
|
||||||
CONNSTR=${{ secrets.BENCHMARK_CAPTEST_SHARDING_CONNSTR }}
|
|
||||||
;;
|
|
||||||
neon-captest-new | neon-captest-freetier | neonvm-captest-new | neonvm-captest-freetier)
|
neon-captest-new | neon-captest-freetier | neonvm-captest-new | neonvm-captest-freetier)
|
||||||
CONNSTR=${{ steps.create-neon-project.outputs.dsn }}
|
CONNSTR=${{ steps.create-neon-project.outputs.dsn }}
|
||||||
;;
|
;;
|
||||||
@@ -282,15 +270,11 @@ jobs:
|
|||||||
|
|
||||||
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
QUERIES=("SELECT version()")
|
QUERY="SELECT version();"
|
||||||
if [[ "${PLATFORM}" = "neon"* ]]; then
|
if [[ "${PLATFORM}" = "neon"* ]]; then
|
||||||
QUERIES+=("SHOW neon.tenant_id")
|
QUERY="${QUERY} SHOW neon.tenant_id; SHOW neon.timeline_id;"
|
||||||
QUERIES+=("SHOW neon.timeline_id")
|
|
||||||
fi
|
fi
|
||||||
|
psql ${CONNSTR} -c "${QUERY}"
|
||||||
for q in "${QUERIES[@]}"; do
|
|
||||||
psql ${CONNSTR} -c "${q}"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Benchmark init
|
- name: Benchmark init
|
||||||
uses: ./.github/actions/run-python-test-set
|
uses: ./.github/actions/run-python-test-set
|
||||||
@@ -351,92 +335,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||||
|
|
||||||
pgbench-pgvector:
|
|
||||||
env:
|
|
||||||
TEST_PG_BENCH_DURATIONS_MATRIX: "15m"
|
|
||||||
TEST_PG_BENCH_SCALES_MATRIX: "1"
|
|
||||||
POSTGRES_DISTRIB_DIR: /tmp/neon/pg_install
|
|
||||||
DEFAULT_PG_VERSION: 16
|
|
||||||
TEST_OUTPUT: /tmp/test_output
|
|
||||||
BUILD_TYPE: remote
|
|
||||||
SAVE_PERF_REPORT: ${{ github.event.inputs.save_perf_report || ( github.ref_name == 'main' ) }}
|
|
||||||
PLATFORM: "neon-captest-pgvector"
|
|
||||||
|
|
||||||
runs-on: [ self-hosted, us-east-2, x64 ]
|
|
||||||
container:
|
|
||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/build-tools:pinned
|
|
||||||
options: --init
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download Neon artifact
|
|
||||||
uses: ./.github/actions/download
|
|
||||||
with:
|
|
||||||
name: neon-${{ runner.os }}-release-artifact
|
|
||||||
path: /tmp/neon/
|
|
||||||
prefix: latest
|
|
||||||
|
|
||||||
- name: Add Postgres binaries to PATH
|
|
||||||
run: |
|
|
||||||
${POSTGRES_DISTRIB_DIR}/v${DEFAULT_PG_VERSION}/bin/pgbench --version
|
|
||||||
echo "${POSTGRES_DISTRIB_DIR}/v${DEFAULT_PG_VERSION}/bin" >> $GITHUB_PATH
|
|
||||||
|
|
||||||
- name: Set up Connection String
|
|
||||||
id: set-up-connstr
|
|
||||||
run: |
|
|
||||||
CONNSTR=${{ secrets.BENCHMARK_PGVECTOR_CONNSTR }}
|
|
||||||
|
|
||||||
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
QUERIES=("SELECT version()")
|
|
||||||
QUERIES+=("SHOW neon.tenant_id")
|
|
||||||
QUERIES+=("SHOW neon.timeline_id")
|
|
||||||
|
|
||||||
for q in "${QUERIES[@]}"; do
|
|
||||||
psql ${CONNSTR} -c "${q}"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Benchmark pgvector hnsw indexing
|
|
||||||
uses: ./.github/actions/run-python-test-set
|
|
||||||
with:
|
|
||||||
build_type: ${{ env.BUILD_TYPE }}
|
|
||||||
test_selection: performance/test_perf_olap.py
|
|
||||||
run_in_parallel: false
|
|
||||||
save_perf_report: ${{ env.SAVE_PERF_REPORT }}
|
|
||||||
extra_params: -m remote_cluster --timeout 21600 -k test_pgvector_indexing
|
|
||||||
env:
|
|
||||||
VIP_VAP_ACCESS_TOKEN: "${{ secrets.VIP_VAP_ACCESS_TOKEN }}"
|
|
||||||
PERF_TEST_RESULT_CONNSTR: "${{ secrets.PERF_TEST_RESULT_CONNSTR }}"
|
|
||||||
BENCHMARK_CONNSTR: ${{ steps.set-up-connstr.outputs.connstr }}
|
|
||||||
|
|
||||||
- name: Benchmark pgvector queries
|
|
||||||
uses: ./.github/actions/run-python-test-set
|
|
||||||
with:
|
|
||||||
build_type: ${{ env.BUILD_TYPE }}
|
|
||||||
test_selection: performance/test_perf_pgvector_queries.py
|
|
||||||
run_in_parallel: false
|
|
||||||
save_perf_report: ${{ env.SAVE_PERF_REPORT }}
|
|
||||||
extra_params: -m remote_cluster --timeout 21600
|
|
||||||
env:
|
|
||||||
BENCHMARK_CONNSTR: ${{ steps.set-up-connstr.outputs.connstr }}
|
|
||||||
VIP_VAP_ACCESS_TOKEN: "${{ secrets.VIP_VAP_ACCESS_TOKEN }}"
|
|
||||||
PERF_TEST_RESULT_CONNSTR: "${{ secrets.PERF_TEST_RESULT_CONNSTR }}"
|
|
||||||
|
|
||||||
- name: Create Allure report
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
uses: ./.github/actions/allure-report-generate
|
|
||||||
|
|
||||||
- name: Post to a Slack channel
|
|
||||||
if: ${{ github.event.schedule && failure() }}
|
|
||||||
uses: slackapi/slack-github-action@v1
|
|
||||||
with:
|
|
||||||
channel-id: "C033QLM5P7D" # dev-staging-stream
|
|
||||||
slack-message: "Periodic perf testing neon-captest-pgvector: ${{ job.status }}\n${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
||||||
env:
|
|
||||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
|
||||||
|
|
||||||
|
|
||||||
clickbench-compare:
|
clickbench-compare:
|
||||||
# ClichBench DB for rds-aurora and rds-Postgres deployed to the same clusters
|
# ClichBench DB for rds-aurora and rds-Postgres deployed to the same clusters
|
||||||
# we use for performance testing in pgbench-compare.
|
# we use for performance testing in pgbench-compare.
|
||||||
@@ -445,7 +343,7 @@ jobs:
|
|||||||
#
|
#
|
||||||
# *_CLICKBENCH_CONNSTR: Genuine ClickBench DB with ~100M rows
|
# *_CLICKBENCH_CONNSTR: Genuine ClickBench DB with ~100M rows
|
||||||
# *_CLICKBENCH_10M_CONNSTR: DB with the first 10M rows of ClickBench DB
|
# *_CLICKBENCH_10M_CONNSTR: DB with the first 10M rows of ClickBench DB
|
||||||
if: ${{ !cancelled() && (github.event.inputs.run_only_pgvector_tests == 'false' || github.event.inputs.run_only_pgvector_tests == null) }}
|
if: ${{ !cancelled() }}
|
||||||
needs: [ generate-matrices, pgbench-compare ]
|
needs: [ generate-matrices, pgbench-compare ]
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
@@ -503,15 +401,11 @@ jobs:
|
|||||||
|
|
||||||
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
QUERIES=("SELECT version()")
|
QUERY="SELECT version();"
|
||||||
if [[ "${PLATFORM}" = "neon"* ]]; then
|
if [[ "${PLATFORM}" = "neon"* ]]; then
|
||||||
QUERIES+=("SHOW neon.tenant_id")
|
QUERY="${QUERY} SHOW neon.tenant_id; SHOW neon.timeline_id;"
|
||||||
QUERIES+=("SHOW neon.timeline_id")
|
|
||||||
fi
|
fi
|
||||||
|
psql ${CONNSTR} -c "${QUERY}"
|
||||||
for q in "${QUERIES[@]}"; do
|
|
||||||
psql ${CONNSTR} -c "${q}"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: ClickBench benchmark
|
- name: ClickBench benchmark
|
||||||
uses: ./.github/actions/run-python-test-set
|
uses: ./.github/actions/run-python-test-set
|
||||||
@@ -549,7 +443,7 @@ jobs:
|
|||||||
# We might change it after https://github.com/neondatabase/neon/issues/2900.
|
# We might change it after https://github.com/neondatabase/neon/issues/2900.
|
||||||
#
|
#
|
||||||
# *_TPCH_S10_CONNSTR: DB generated with scale factor 10 (~10 GB)
|
# *_TPCH_S10_CONNSTR: DB generated with scale factor 10 (~10 GB)
|
||||||
if: ${{ !cancelled() && (github.event.inputs.run_only_pgvector_tests == 'false' || github.event.inputs.run_only_pgvector_tests == null) }}
|
if: ${{ !cancelled() }}
|
||||||
needs: [ generate-matrices, clickbench-compare ]
|
needs: [ generate-matrices, clickbench-compare ]
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
@@ -613,15 +507,11 @@ jobs:
|
|||||||
|
|
||||||
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
QUERIES=("SELECT version()")
|
QUERY="SELECT version();"
|
||||||
if [[ "${PLATFORM}" = "neon"* ]]; then
|
if [[ "${PLATFORM}" = "neon"* ]]; then
|
||||||
QUERIES+=("SHOW neon.tenant_id")
|
QUERY="${QUERY} SHOW neon.tenant_id; SHOW neon.timeline_id;"
|
||||||
QUERIES+=("SHOW neon.timeline_id")
|
|
||||||
fi
|
fi
|
||||||
|
psql ${CONNSTR} -c "${QUERY}"
|
||||||
for q in "${QUERIES[@]}"; do
|
|
||||||
psql ${CONNSTR} -c "${q}"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Run TPC-H benchmark
|
- name: Run TPC-H benchmark
|
||||||
uses: ./.github/actions/run-python-test-set
|
uses: ./.github/actions/run-python-test-set
|
||||||
@@ -651,7 +541,7 @@ jobs:
|
|||||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||||
|
|
||||||
user-examples-compare:
|
user-examples-compare:
|
||||||
if: ${{ !cancelled() && (github.event.inputs.run_only_pgvector_tests == 'false' || github.event.inputs.run_only_pgvector_tests == null) }}
|
if: ${{ !cancelled() }}
|
||||||
needs: [ generate-matrices, tpch-compare ]
|
needs: [ generate-matrices, tpch-compare ]
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
@@ -707,15 +597,11 @@ jobs:
|
|||||||
|
|
||||||
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
QUERIES=("SELECT version()")
|
QUERY="SELECT version();"
|
||||||
if [[ "${PLATFORM}" = "neon"* ]]; then
|
if [[ "${PLATFORM}" = "neon"* ]]; then
|
||||||
QUERIES+=("SHOW neon.tenant_id")
|
QUERY="${QUERY} SHOW neon.tenant_id; SHOW neon.timeline_id;"
|
||||||
QUERIES+=("SHOW neon.timeline_id")
|
|
||||||
fi
|
fi
|
||||||
|
psql ${CONNSTR} -c "${QUERY}"
|
||||||
for q in "${QUERIES[@]}"; do
|
|
||||||
psql ${CONNSTR} -c "${q}"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Run user examples
|
- name: Run user examples
|
||||||
uses: ./.github/actions/run-python-test-set
|
uses: ./.github/actions/run-python-test-set
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ defaults:
|
|||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: build-build-tools-image-${{ inputs.image-tag }}
|
group: build-build-tools-image-${{ inputs.image-tag }}
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
# No permission for GITHUB_TOKEN by default; the **minimal required** set of permissions should be granted in each job.
|
# No permission for GITHUB_TOKEN by default; the **minimal required** set of permissions should be granted in each job.
|
||||||
permissions: {}
|
permissions: {}
|
||||||
@@ -30,6 +29,7 @@ jobs:
|
|||||||
check-image:
|
check-image:
|
||||||
uses: ./.github/workflows/check-build-tools-image.yml
|
uses: ./.github/workflows/check-build-tools-image.yml
|
||||||
|
|
||||||
|
# This job uses older version of GitHub Actions because it's run on gen2 runners, which don't support node 20 (for newer versions)
|
||||||
build-image:
|
build-image:
|
||||||
needs: [ check-image ]
|
needs: [ check-image ]
|
||||||
if: needs.check-image.outputs.found == 'false'
|
if: needs.check-image.outputs.found == 'false'
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
arch: [ x64, arm64 ]
|
arch: [ x64, arm64 ]
|
||||||
|
|
||||||
runs-on: ${{ fromJson(format('["self-hosted", "gen3", "{0}"]', matrix.arch == 'arm64' && 'large-arm64' || 'large')) }}
|
runs-on: ${{ fromJson(format('["self-hosted", "dev", "{0}"]', matrix.arch)) }}
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE_TAG: ${{ inputs.image-tag }}
|
IMAGE_TAG: ${{ inputs.image-tag }}
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
# Use custom DOCKER_CONFIG directory to avoid conflicts with default settings
|
# Use custom DOCKER_CONFIG directory to avoid conflicts with default settings
|
||||||
# The default value is ~/.docker
|
# The default value is ~/.docker
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
|
|
||||||
merge-images:
|
merge-images:
|
||||||
needs: [ build-image ]
|
needs: [ build-image ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE_TAG: ${{ inputs.image-tag }}
|
IMAGE_TAG: ${{ inputs.image-tag }}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
cancel-previous-e2e-tests:
|
cancel-previous-e2e-tests:
|
||||||
needs: [ check-permissions ]
|
needs: [ check-permissions ]
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Cancel previous e2e-tests runs for this PR
|
- name: Cancel previous e2e-tests runs for this PR
|
||||||
@@ -236,6 +236,27 @@ jobs:
|
|||||||
submodules: true
|
submodules: true
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Check Postgres submodules revision
|
||||||
|
shell: bash -euo pipefail {0}
|
||||||
|
run: |
|
||||||
|
# This is a temporary solution to ensure that the Postgres submodules revision is correct (i.e. the updated intentionally).
|
||||||
|
# Eventually it will be replaced by a regression test https://github.com/neondatabase/neon/pull/4603
|
||||||
|
|
||||||
|
FAILED=false
|
||||||
|
for postgres in postgres-v14 postgres-v15 postgres-v16; do
|
||||||
|
expected=$(cat vendor/revisions.json | jq --raw-output '."'"${postgres}"'"')
|
||||||
|
actual=$(git rev-parse "HEAD:vendor/${postgres}")
|
||||||
|
if [ "${expected}" != "${actual}" ]; then
|
||||||
|
echo >&2 "Expected ${postgres} rev to be at '${expected}', but it is at '${actual}'"
|
||||||
|
FAILED=true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${FAILED}" = "true" ]; then
|
||||||
|
echo >&2 "Please update vendor/revisions.json if these changes are intentional"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Set pg 14 revision for caching
|
- name: Set pg 14 revision for caching
|
||||||
id: pg_v14_rev
|
id: pg_v14_rev
|
||||||
run: echo pg_rev=$(git rev-parse HEAD:vendor/postgres-v14) >> $GITHUB_OUTPUT
|
run: echo pg_rev=$(git rev-parse HEAD:vendor/postgres-v14) >> $GITHUB_OUTPUT
|
||||||
@@ -299,21 +320,21 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: pg_install/v14
|
path: pg_install/v14
|
||||||
key: v1-${{ runner.os }}-${{ matrix.build_type }}-pg-${{ steps.pg_v14_rev.outputs.pg_rev }}-${{ hashFiles('Makefile', 'Dockerfile.build-tools') }}
|
key: v1-${{ runner.os }}-${{ matrix.build_type }}-pg-${{ steps.pg_v14_rev.outputs.pg_rev }}-${{ hashFiles('Makefile') }}
|
||||||
|
|
||||||
- name: Cache postgres v15 build
|
- name: Cache postgres v15 build
|
||||||
id: cache_pg_15
|
id: cache_pg_15
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: pg_install/v15
|
path: pg_install/v15
|
||||||
key: v1-${{ runner.os }}-${{ matrix.build_type }}-pg-${{ steps.pg_v15_rev.outputs.pg_rev }}-${{ hashFiles('Makefile', 'Dockerfile.build-tools') }}
|
key: v1-${{ runner.os }}-${{ matrix.build_type }}-pg-${{ steps.pg_v15_rev.outputs.pg_rev }}-${{ hashFiles('Makefile') }}
|
||||||
|
|
||||||
- name: Cache postgres v16 build
|
- name: Cache postgres v16 build
|
||||||
id: cache_pg_16
|
id: cache_pg_16
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: pg_install/v16
|
path: pg_install/v16
|
||||||
key: v1-${{ runner.os }}-${{ matrix.build_type }}-pg-${{ steps.pg_v16_rev.outputs.pg_rev }}-${{ hashFiles('Makefile', 'Dockerfile.build-tools') }}
|
key: v1-${{ runner.os }}-${{ matrix.build_type }}-pg-${{ steps.pg_v16_rev.outputs.pg_rev }}-${{ hashFiles('Makefile') }}
|
||||||
|
|
||||||
- name: Build postgres v14
|
- name: Build postgres v14
|
||||||
if: steps.cache_pg_14.outputs.cache-hit != 'true'
|
if: steps.cache_pg_14.outputs.cache-hit != 'true'
|
||||||
@@ -337,8 +358,31 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
${cov_prefix} mold -run cargo build $CARGO_FLAGS $CARGO_FEATURES --bins --tests
|
${cov_prefix} mold -run cargo build $CARGO_FLAGS $CARGO_FEATURES --bins --tests
|
||||||
|
|
||||||
# Do install *before* running rust tests because they might recompile the
|
- name: Run rust tests
|
||||||
# binaries with different features/flags.
|
env:
|
||||||
|
NEXTEST_RETRIES: 3
|
||||||
|
run: |
|
||||||
|
for io_engine in std-fs tokio-epoll-uring ; do
|
||||||
|
NEON_PAGESERVER_UNIT_TEST_VIRTUAL_FILE_IOENGINE=$io_engine ${cov_prefix} cargo nextest run $CARGO_FLAGS $CARGO_FEATURES
|
||||||
|
done
|
||||||
|
|
||||||
|
# Run separate tests for real S3
|
||||||
|
export ENABLE_REAL_S3_REMOTE_STORAGE=nonempty
|
||||||
|
export REMOTE_STORAGE_S3_BUCKET=neon-github-ci-tests
|
||||||
|
export REMOTE_STORAGE_S3_REGION=eu-central-1
|
||||||
|
# Avoid `$CARGO_FEATURES` since there's no `testing` feature in the e2e tests now
|
||||||
|
${cov_prefix} cargo nextest run $CARGO_FLAGS -E 'package(remote_storage)' -E 'test(test_real_s3)'
|
||||||
|
|
||||||
|
# Run separate tests for real Azure Blob Storage
|
||||||
|
# XXX: replace region with `eu-central-1`-like region
|
||||||
|
export ENABLE_REAL_AZURE_REMOTE_STORAGE=y
|
||||||
|
export AZURE_STORAGE_ACCOUNT="${{ secrets.AZURE_STORAGE_ACCOUNT_DEV }}"
|
||||||
|
export AZURE_STORAGE_ACCESS_KEY="${{ secrets.AZURE_STORAGE_ACCESS_KEY_DEV }}"
|
||||||
|
export REMOTE_STORAGE_AZURE_CONTAINER="${{ vars.REMOTE_STORAGE_AZURE_CONTAINER }}"
|
||||||
|
export REMOTE_STORAGE_AZURE_REGION="${{ vars.REMOTE_STORAGE_AZURE_REGION }}"
|
||||||
|
# Avoid `$CARGO_FEATURES` since there's no `testing` feature in the e2e tests now
|
||||||
|
${cov_prefix} cargo nextest run $CARGO_FLAGS -E 'package(remote_storage)' -E 'test(test_real_azure)'
|
||||||
|
|
||||||
- name: Install rust binaries
|
- name: Install rust binaries
|
||||||
run: |
|
run: |
|
||||||
# Install target binaries
|
# Install target binaries
|
||||||
@@ -379,32 +423,6 @@ jobs:
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Run rust tests
|
|
||||||
env:
|
|
||||||
NEXTEST_RETRIES: 3
|
|
||||||
run: |
|
|
||||||
#nextest does not yet support running doctests
|
|
||||||
cargo test --doc $CARGO_FLAGS $CARGO_FEATURES
|
|
||||||
|
|
||||||
for io_engine in std-fs tokio-epoll-uring ; do
|
|
||||||
NEON_PAGESERVER_UNIT_TEST_VIRTUAL_FILE_IOENGINE=$io_engine ${cov_prefix} cargo nextest run $CARGO_FLAGS $CARGO_FEATURES
|
|
||||||
done
|
|
||||||
|
|
||||||
# Run separate tests for real S3
|
|
||||||
export ENABLE_REAL_S3_REMOTE_STORAGE=nonempty
|
|
||||||
export REMOTE_STORAGE_S3_BUCKET=neon-github-ci-tests
|
|
||||||
export REMOTE_STORAGE_S3_REGION=eu-central-1
|
|
||||||
${cov_prefix} cargo nextest run $CARGO_FLAGS $CARGO_FEATURES -E 'package(remote_storage)' -E 'test(test_real_s3)'
|
|
||||||
|
|
||||||
# Run separate tests for real Azure Blob Storage
|
|
||||||
# XXX: replace region with `eu-central-1`-like region
|
|
||||||
export ENABLE_REAL_AZURE_REMOTE_STORAGE=y
|
|
||||||
export AZURE_STORAGE_ACCOUNT="${{ secrets.AZURE_STORAGE_ACCOUNT_DEV }}"
|
|
||||||
export AZURE_STORAGE_ACCESS_KEY="${{ secrets.AZURE_STORAGE_ACCESS_KEY_DEV }}"
|
|
||||||
export REMOTE_STORAGE_AZURE_CONTAINER="${{ vars.REMOTE_STORAGE_AZURE_CONTAINER }}"
|
|
||||||
export REMOTE_STORAGE_AZURE_REGION="${{ vars.REMOTE_STORAGE_AZURE_REGION }}"
|
|
||||||
${cov_prefix} cargo nextest run $CARGO_FLAGS $CARGO_FEATURES -E 'package(remote_storage)' -E 'test(test_real_azure)'
|
|
||||||
|
|
||||||
- name: Install postgres binaries
|
- name: Install postgres binaries
|
||||||
run: cp -a pg_install /tmp/neon/pg_install
|
run: cp -a pg_install /tmp/neon/pg_install
|
||||||
|
|
||||||
@@ -459,8 +477,6 @@ jobs:
|
|||||||
BUILD_TAG: ${{ needs.tag.outputs.build-tag }}
|
BUILD_TAG: ${{ needs.tag.outputs.build-tag }}
|
||||||
PAGESERVER_VIRTUAL_FILE_IO_ENGINE: tokio-epoll-uring
|
PAGESERVER_VIRTUAL_FILE_IO_ENGINE: tokio-epoll-uring
|
||||||
PAGESERVER_GET_VECTORED_IMPL: vectored
|
PAGESERVER_GET_VECTORED_IMPL: vectored
|
||||||
PAGESERVER_GET_IMPL: vectored
|
|
||||||
PAGESERVER_VALIDATE_VEC_GET: true
|
|
||||||
|
|
||||||
# Temporary disable this step until we figure out why it's so flaky
|
# Temporary disable this step until we figure out why it's so flaky
|
||||||
# Ref https://github.com/neondatabase/neon/issues/4540
|
# Ref https://github.com/neondatabase/neon/issues/4540
|
||||||
@@ -540,33 +556,12 @@ jobs:
|
|||||||
PERF_TEST_RESULT_CONNSTR: "${{ secrets.PERF_TEST_RESULT_CONNSTR }}"
|
PERF_TEST_RESULT_CONNSTR: "${{ secrets.PERF_TEST_RESULT_CONNSTR }}"
|
||||||
TEST_RESULT_CONNSTR: "${{ secrets.REGRESS_TEST_RESULT_CONNSTR_NEW }}"
|
TEST_RESULT_CONNSTR: "${{ secrets.REGRESS_TEST_RESULT_CONNSTR_NEW }}"
|
||||||
PAGESERVER_VIRTUAL_FILE_IO_ENGINE: tokio-epoll-uring
|
PAGESERVER_VIRTUAL_FILE_IO_ENGINE: tokio-epoll-uring
|
||||||
PAGESERVER_GET_VECTORED_IMPL: vectored
|
|
||||||
PAGESERVER_GET_IMPL: vectored
|
|
||||||
PAGESERVER_VALIDATE_VEC_GET: false
|
|
||||||
# XXX: no coverage data handling here, since benchmarks are run on release builds,
|
# XXX: no coverage data handling here, since benchmarks are run on release builds,
|
||||||
# while coverage is currently collected for the debug ones
|
# while coverage is currently collected for the debug ones
|
||||||
|
|
||||||
report-benchmarks-failures:
|
|
||||||
needs: [ benchmarks, create-test-report ]
|
|
||||||
if: github.ref_name == 'main' && failure() && needs.benchmarks.result == 'failure'
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: slackapi/slack-github-action@v1
|
|
||||||
with:
|
|
||||||
channel-id: C060CNA47S9 # on-call-staging-storage-stream
|
|
||||||
slack-message: |
|
|
||||||
Benchmarks failed on main: ${{ github.event.head_commit.url }}
|
|
||||||
|
|
||||||
Allure report: ${{ needs.create-test-report.outputs.report-url }}
|
|
||||||
env:
|
|
||||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
|
||||||
|
|
||||||
create-test-report:
|
create-test-report:
|
||||||
needs: [ check-permissions, regress-tests, coverage-report, benchmarks, build-build-tools-image ]
|
needs: [ check-permissions, regress-tests, coverage-report, benchmarks, build-build-tools-image ]
|
||||||
if: ${{ !cancelled() && contains(fromJSON('["skipped", "success"]'), needs.check-permissions.result) }}
|
if: ${{ !cancelled() && contains(fromJSON('["skipped", "success"]'), needs.check-permissions.result) }}
|
||||||
outputs:
|
|
||||||
report-url: ${{ steps.create-allure-report.outputs.report-url }}
|
|
||||||
|
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: [ self-hosted, gen3, small ]
|
||||||
container:
|
container:
|
||||||
@@ -723,13 +718,9 @@ jobs:
|
|||||||
uses: ./.github/workflows/trigger-e2e-tests.yml
|
uses: ./.github/workflows/trigger-e2e-tests.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
neon-image-arch:
|
neon-image:
|
||||||
needs: [ check-permissions, build-build-tools-image, tag ]
|
needs: [ check-permissions, build-build-tools-image, tag ]
|
||||||
strategy:
|
runs-on: [ self-hosted, gen3, large ]
|
||||||
matrix:
|
|
||||||
arch: [ x64, arm64 ]
|
|
||||||
|
|
||||||
runs-on: ${{ fromJson(format('["self-hosted", "gen3", "{0}"]', matrix.arch == 'arm64' && 'large-arm64' || 'large')) }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -744,13 +735,19 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p .docker-custom
|
mkdir -p .docker-custom
|
||||||
echo DOCKER_CONFIG=$(pwd)/.docker-custom >> $GITHUB_ENV
|
echo DOCKER_CONFIG=$(pwd)/.docker-custom >> $GITHUB_ENV
|
||||||
- uses: docker/setup-buildx-action@v2
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- uses: docker/login-action@v3
|
- uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.NEON_DOCKERHUB_USERNAME }}
|
username: ${{ secrets.NEON_DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: 369495373322.dkr.ecr.eu-central-1.amazonaws.com
|
||||||
|
username: ${{ secrets.AWS_ACCESS_KEY_DEV }}
|
||||||
|
password: ${{ secrets.AWS_SECRET_KEY_DEV }}
|
||||||
|
|
||||||
- uses: docker/build-push-action@v5
|
- uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -762,52 +759,25 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
pull: true
|
pull: true
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
cache-from: type=registry,ref=neondatabase/neon:cache-${{ matrix.arch }}
|
cache-from: type=registry,ref=neondatabase/neon:cache
|
||||||
cache-to: type=registry,ref=neondatabase/neon:cache-${{ matrix.arch }},mode=max
|
cache-to: type=registry,ref=neondatabase/neon:cache,mode=max
|
||||||
tags: |
|
tags: |
|
||||||
neondatabase/neon:${{ needs.tag.outputs.build-tag }}-${{ matrix.arch }}
|
369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}}
|
||||||
|
neondatabase/neon:${{needs.tag.outputs.build-tag}}
|
||||||
|
|
||||||
- name: Remove custom docker config directory
|
- name: Remove custom docker config directory
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
rm -rf .docker-custom
|
rm -rf .docker-custom
|
||||||
|
|
||||||
neon-image:
|
compute-node-image:
|
||||||
needs: [ neon-image-arch, tag ]
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.NEON_DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Create multi-arch image
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools create -t neondatabase/neon:${{ needs.tag.outputs.build-tag }} \
|
|
||||||
neondatabase/neon:${{ needs.tag.outputs.build-tag }}-x64 \
|
|
||||||
neondatabase/neon:${{ needs.tag.outputs.build-tag }}-arm64
|
|
||||||
|
|
||||||
- uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: 369495373322.dkr.ecr.eu-central-1.amazonaws.com
|
|
||||||
username: ${{ secrets.AWS_ACCESS_KEY_DEV }}
|
|
||||||
password: ${{ secrets.AWS_SECRET_KEY_DEV }}
|
|
||||||
|
|
||||||
- name: Push multi-arch image to ECR
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools create -t 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{ needs.tag.outputs.build-tag }} \
|
|
||||||
neondatabase/neon:${{ needs.tag.outputs.build-tag }}
|
|
||||||
|
|
||||||
compute-node-image-arch:
|
|
||||||
needs: [ check-permissions, build-build-tools-image, tag ]
|
needs: [ check-permissions, build-build-tools-image, tag ]
|
||||||
|
runs-on: [ self-hosted, gen3, large ]
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
version: [ v14, v15, v16 ]
|
version: [ v14, v15, v16 ]
|
||||||
arch: [ x64, arm64 ]
|
|
||||||
|
|
||||||
runs-on: ${{ fromJson(format('["self-hosted", "gen3", "{0}"]', matrix.arch == 'arm64' && 'large-arm64' || 'large')) }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -822,7 +792,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p .docker-custom
|
mkdir -p .docker-custom
|
||||||
echo DOCKER_CONFIG=$(pwd)/.docker-custom >> $GITHUB_ENV
|
echo DOCKER_CONFIG=$(pwd)/.docker-custom >> $GITHUB_ENV
|
||||||
- uses: docker/setup-buildx-action@v2
|
- uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
# Disable parallelism for docker buildkit.
|
# Disable parallelism for docker buildkit.
|
||||||
# As we already build everything with `make -j$(nproc)`, running it in additional level of parallelisam blows up the Runner.
|
# As we already build everything with `make -j$(nproc)`, running it in additional level of parallelisam blows up the Runner.
|
||||||
@@ -854,34 +824,15 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
pull: true
|
pull: true
|
||||||
file: Dockerfile.compute-node
|
file: Dockerfile.compute-node
|
||||||
cache-from: type=registry,ref=neondatabase/compute-node-${{ matrix.version }}:cache-${{ matrix.arch }}
|
cache-from: type=registry,ref=neondatabase/compute-node-${{ matrix.version }}:cache
|
||||||
cache-to: type=registry,ref=neondatabase/compute-node-${{ matrix.version }}:cache-${{ matrix.arch }},mode=max
|
cache-to: type=registry,ref=neondatabase/compute-node-${{ matrix.version }}:cache,mode=max
|
||||||
tags: |
|
tags: |
|
||||||
neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}-${{ matrix.arch }}
|
369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
||||||
|
neondatabase/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
||||||
- name: Build neon extensions test image
|
|
||||||
if: matrix.version == 'v16'
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
build-args: |
|
|
||||||
GIT_VERSION=${{ github.event.pull_request.head.sha || github.sha }}
|
|
||||||
PG_VERSION=${{ matrix.version }}
|
|
||||||
BUILD_TAG=${{ needs.tag.outputs.build-tag }}
|
|
||||||
TAG=${{ needs.build-build-tools-image.outputs.image-tag }}
|
|
||||||
provenance: false
|
|
||||||
push: true
|
|
||||||
pull: true
|
|
||||||
file: Dockerfile.compute-node
|
|
||||||
target: neon-pg-ext-test
|
|
||||||
cache-from: type=registry,ref=neondatabase/neon-test-extensions-${{ matrix.version }}:cache-${{ matrix.arch }}
|
|
||||||
cache-to: type=registry,ref=neondatabase/neon-test-extensions-${{ matrix.version }}:cache-${{ matrix.arch }},mode=max
|
|
||||||
tags: |
|
|
||||||
neondatabase/neon-test-extensions-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}-${{ matrix.arch }}
|
|
||||||
|
|
||||||
- name: Build compute-tools image
|
- name: Build compute-tools image
|
||||||
# compute-tools are Postgres independent, so build it only once
|
# compute-tools are Postgres independent, so build it only once
|
||||||
if: matrix.version == 'v16'
|
if: ${{ matrix.version == 'v16' }}
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
target: compute-tools-image
|
target: compute-tools-image
|
||||||
@@ -895,64 +846,14 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
file: Dockerfile.compute-node
|
file: Dockerfile.compute-node
|
||||||
tags: |
|
tags: |
|
||||||
neondatabase/compute-tools:${{ needs.tag.outputs.build-tag }}-${{ matrix.arch }}
|
369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{ needs.tag.outputs.build-tag }}
|
||||||
|
neondatabase/compute-tools:${{ needs.tag.outputs.build-tag }}
|
||||||
|
|
||||||
- name: Remove custom docker config directory
|
- name: Remove custom docker config directory
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
rm -rf .docker-custom
|
rm -rf .docker-custom
|
||||||
|
|
||||||
compute-node-image:
|
|
||||||
needs: [ compute-node-image-arch, tag ]
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
version: [ v14, v15, v16 ]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.NEON_DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Create multi-arch compute-node image
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools create -t neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }} \
|
|
||||||
neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}-x64 \
|
|
||||||
neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}-arm64
|
|
||||||
|
|
||||||
- name: Create multi-arch neon-test-extensions image
|
|
||||||
if: matrix.version == 'v16'
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools create -t neondatabase/neon-test-extensions-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }} \
|
|
||||||
neondatabase/neon-test-extensions-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}-x64 \
|
|
||||||
neondatabase/neon-test-extensions-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}-arm64
|
|
||||||
|
|
||||||
- name: Create multi-arch compute-tools image
|
|
||||||
if: matrix.version == 'v16'
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools create -t neondatabase/compute-tools:${{ needs.tag.outputs.build-tag }} \
|
|
||||||
neondatabase/compute-tools:${{ needs.tag.outputs.build-tag }}-x64 \
|
|
||||||
neondatabase/compute-tools:${{ needs.tag.outputs.build-tag }}-arm64
|
|
||||||
|
|
||||||
- uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: 369495373322.dkr.ecr.eu-central-1.amazonaws.com
|
|
||||||
username: ${{ secrets.AWS_ACCESS_KEY_DEV }}
|
|
||||||
password: ${{ secrets.AWS_SECRET_KEY_DEV }}
|
|
||||||
|
|
||||||
- name: Push multi-arch compute-node-${{ matrix.version }} image to ECR
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools create -t 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }} \
|
|
||||||
neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}
|
|
||||||
|
|
||||||
- name: Push multi-arch compute-tools image to ECR
|
|
||||||
if: matrix.version == 'v16'
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools create -t 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{ needs.tag.outputs.build-tag }} \
|
|
||||||
neondatabase/compute-tools:${{ needs.tag.outputs.build-tag }}
|
|
||||||
|
|
||||||
vm-compute-node-image:
|
vm-compute-node-image:
|
||||||
needs: [ check-permissions, tag, compute-node-image ]
|
needs: [ check-permissions, tag, compute-node-image ]
|
||||||
runs-on: [ self-hosted, gen3, large ]
|
runs-on: [ self-hosted, gen3, large ]
|
||||||
@@ -960,12 +861,15 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
version: [ v14, v15, v16 ]
|
version: [ v14, v15, v16 ]
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: sh -eu {0}
|
||||||
env:
|
env:
|
||||||
VM_BUILDER_VERSION: v0.29.3
|
VM_BUILDER_VERSION: v0.23.2
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -974,48 +878,26 @@ jobs:
|
|||||||
curl -fL https://github.com/neondatabase/autoscaling/releases/download/$VM_BUILDER_VERSION/vm-builder -o vm-builder
|
curl -fL https://github.com/neondatabase/autoscaling/releases/download/$VM_BUILDER_VERSION/vm-builder -o vm-builder
|
||||||
chmod +x vm-builder
|
chmod +x vm-builder
|
||||||
|
|
||||||
# Use custom DOCKER_CONFIG directory to avoid conflicts with default settings
|
|
||||||
# The default value is ~/.docker
|
|
||||||
- name: Set custom docker config directory
|
|
||||||
run: |
|
|
||||||
mkdir -p .docker-custom
|
|
||||||
echo DOCKER_CONFIG=$(pwd)/.docker-custom >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.NEON_DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
|
||||||
|
|
||||||
# Note: we need a separate pull step here because otherwise vm-builder will try to pull, and
|
# Note: we need a separate pull step here because otherwise vm-builder will try to pull, and
|
||||||
# it won't have the proper authentication (written at v0.6.0)
|
# it won't have the proper authentication (written at v0.6.0)
|
||||||
- name: Pulling compute-node image
|
- name: Pulling compute-node image
|
||||||
run: |
|
run: |
|
||||||
docker pull neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}
|
docker pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
||||||
|
|
||||||
- name: Build vm image
|
- name: Build vm image
|
||||||
run: |
|
run: |
|
||||||
./vm-builder \
|
./vm-builder \
|
||||||
-spec=vm-image-spec.yaml \
|
-spec=vm-image-spec.yaml \
|
||||||
-src=neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }} \
|
-src=369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}} \
|
||||||
-dst=neondatabase/vm-compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}
|
-dst=369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
||||||
|
|
||||||
- name: Pushing vm-compute-node image
|
- name: Pushing vm-compute-node image
|
||||||
run: |
|
run: |
|
||||||
docker push neondatabase/vm-compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}
|
docker push 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
||||||
|
|
||||||
- name: Remove custom docker config directory
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
rm -rf .docker-custom
|
|
||||||
|
|
||||||
test-images:
|
test-images:
|
||||||
needs: [ check-permissions, tag, neon-image, compute-node-image ]
|
needs: [ check-permissions, tag, neon-image, compute-node-image ]
|
||||||
strategy:
|
runs-on: [ self-hosted, gen3, small ]
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
arch: [ x64, arm64 ]
|
|
||||||
|
|
||||||
runs-on: ${{ fromJson(format('["self-hosted", "gen3", "{0}"]', matrix.arch == 'arm64' && 'small-arm64' || 'small')) }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -1023,18 +905,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# Use custom DOCKER_CONFIG directory to avoid conflicts with default settings
|
|
||||||
# The default value is ~/.docker
|
|
||||||
- name: Set custom docker config directory
|
|
||||||
run: |
|
|
||||||
mkdir -p .docker-custom
|
|
||||||
echo DOCKER_CONFIG=$(pwd)/.docker-custom >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.NEON_DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
|
||||||
|
|
||||||
# `neondatabase/neon` contains multiple binaries, all of them use the same input for the version into the same version formatting library.
|
# `neondatabase/neon` contains multiple binaries, all of them use the same input for the version into the same version formatting library.
|
||||||
# Pick pageserver as currently the only binary with extra "version" features printed in the string to verify.
|
# Pick pageserver as currently the only binary with extra "version" features printed in the string to verify.
|
||||||
# Regular pageserver version string looks like
|
# Regular pageserver version string looks like
|
||||||
@@ -1045,7 +915,7 @@ jobs:
|
|||||||
- name: Verify image versions
|
- name: Verify image versions
|
||||||
shell: bash # ensure no set -e for better error messages
|
shell: bash # ensure no set -e for better error messages
|
||||||
run: |
|
run: |
|
||||||
pageserver_version=$(docker run --rm neondatabase/neon:${{ needs.tag.outputs.build-tag }} "/bin/sh" "-c" "/usr/local/bin/pageserver --version")
|
pageserver_version=$(docker run --rm 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}} "/bin/sh" "-c" "/usr/local/bin/pageserver --version")
|
||||||
|
|
||||||
echo "Pageserver version string: $pageserver_version"
|
echo "Pageserver version string: $pageserver_version"
|
||||||
|
|
||||||
@@ -1059,7 +929,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Verify docker-compose example and test extensions
|
- name: Verify docker-compose example
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
run: env TAG=${{needs.tag.outputs.build-tag}} ./docker-compose/docker_compose_test.sh
|
run: env TAG=${{needs.tag.outputs.build-tag}} ./docker-compose/docker_compose_test.sh
|
||||||
|
|
||||||
@@ -1069,78 +939,84 @@ jobs:
|
|||||||
docker compose -f ./docker-compose/docker-compose.yml logs || 0
|
docker compose -f ./docker-compose/docker-compose.yml logs || 0
|
||||||
docker compose -f ./docker-compose/docker-compose.yml down
|
docker compose -f ./docker-compose/docker-compose.yml down
|
||||||
|
|
||||||
- name: Remove custom docker config directory
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
rm -rf .docker-custom
|
|
||||||
|
|
||||||
promote-images:
|
promote-images:
|
||||||
needs: [ check-permissions, tag, test-images, vm-compute-node-image ]
|
needs: [ check-permissions, tag, test-images, vm-compute-node-image ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: [ self-hosted, gen3, small ]
|
||||||
|
container: golang:1.19-bullseye
|
||||||
env:
|
# Don't add if-condition here.
|
||||||
VERSIONS: v14 v15 v16
|
# The job should always be run because we have dependant other jobs that shouldn't be skipped
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: docker/login-action@v3
|
- name: Install Crane & ECR helper
|
||||||
with:
|
|
||||||
username: ${{ secrets.NEON_DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Login to dev ECR
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: 369495373322.dkr.ecr.eu-central-1.amazonaws.com
|
|
||||||
username: ${{ secrets.AWS_ACCESS_KEY_DEV }}
|
|
||||||
password: ${{ secrets.AWS_SECRET_KEY_DEV }}
|
|
||||||
|
|
||||||
- name: Copy vm-compute-node images to ECR
|
|
||||||
run: |
|
run: |
|
||||||
for version in ${VERSIONS}; do
|
go install github.com/google/go-containerregistry/cmd/crane@31786c6cbb82d6ec4fb8eb79cd9387905130534e # v0.11.0
|
||||||
docker buildx imagetools create -t 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-${version}:${{ needs.tag.outputs.build-tag }} \
|
go install github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login@69c85dc22db6511932bbf119e1a0cc5c90c69a7f # v0.6.0
|
||||||
neondatabase/vm-compute-node-${version}:${{ needs.tag.outputs.build-tag }}
|
|
||||||
done
|
- name: Configure ECR login
|
||||||
|
run: |
|
||||||
|
mkdir /github/home/.docker/
|
||||||
|
echo "{\"credsStore\":\"ecr-login\"}" > /github/home/.docker/config.json
|
||||||
|
|
||||||
|
- name: Copy vm-compute-node images to Docker Hub
|
||||||
|
run: |
|
||||||
|
crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} vm-compute-node-v14
|
||||||
|
crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} vm-compute-node-v15
|
||||||
|
crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v16:${{needs.tag.outputs.build-tag}} vm-compute-node-v16
|
||||||
|
|
||||||
- name: Add latest tag to images
|
- name: Add latest tag to images
|
||||||
if: github.ref_name == 'main'
|
if: github.ref_name == 'main' || github.ref_name == 'release' || github.ref_name == 'release-proxy'
|
||||||
run: |
|
run: |
|
||||||
for repo in neondatabase 369495373322.dkr.ecr.eu-central-1.amazonaws.com; do
|
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}} latest
|
||||||
docker buildx imagetools create -t $repo/neon:latest \
|
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} latest
|
||||||
$repo/neon:${{ needs.tag.outputs.build-tag }}
|
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/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/compute-node-v16:${{needs.tag.outputs.build-tag}} latest
|
||||||
|
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v16:${{needs.tag.outputs.build-tag}} latest
|
||||||
|
|
||||||
docker buildx imagetools create -t $repo/compute-tools:latest \
|
- name: Push images to production ECR
|
||||||
$repo/compute-tools:${{ needs.tag.outputs.build-tag }}
|
if: github.ref_name == 'main' || github.ref_name == 'release'|| github.ref_name == 'release-proxy'
|
||||||
|
|
||||||
for version in ${VERSIONS}; do
|
|
||||||
docker buildx imagetools create -t $repo/compute-node-${version}:latest \
|
|
||||||
$repo/compute-node-${version}:${{ needs.tag.outputs.build-tag }}
|
|
||||||
|
|
||||||
docker buildx imagetools create -t $repo/vm-compute-node-${version}:latest \
|
|
||||||
$repo/vm-compute-node-${version}:${{ needs.tag.outputs.build-tag }}
|
|
||||||
done
|
|
||||||
done
|
|
||||||
docker buildx imagetools create -t neondatabase/neon-test-extensions-v16:latest \
|
|
||||||
neondatabase/neon-test-extensions-v16:${{ needs.tag.outputs.build-tag }}
|
|
||||||
|
|
||||||
- name: Login to prod ECR
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
if: github.ref_name == 'release'|| github.ref_name == 'release-proxy'
|
|
||||||
with:
|
|
||||||
registry: 093970136003.dkr.ecr.eu-central-1.amazonaws.com
|
|
||||||
username: ${{ secrets.PROD_GHA_RUNNER_LIMITED_AWS_ACCESS_KEY_ID }}
|
|
||||||
password: ${{ secrets.PROD_GHA_RUNNER_LIMITED_AWS_SECRET_ACCESS_KEY }}
|
|
||||||
|
|
||||||
- name: Copy all images to prod ECR
|
|
||||||
if: github.ref_name == 'release'|| github.ref_name == 'release-proxy'
|
|
||||||
run: |
|
run: |
|
||||||
for image in neon compute-tools {vm-,}compute-node-{v14,v15,v16}; do
|
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/neon:latest
|
||||||
docker buildx imagetools create -t 093970136003.dkr.ecr.eu-central-1.amazonaws.com/${image}:${{ needs.tag.outputs.build-tag }} \
|
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
|
||||||
369495373322.dkr.ecr.eu-central-1.amazonaws.com/${image}:${{ needs.tag.outputs.build-tag }}
|
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
|
||||||
done
|
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/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/compute-node-v16:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v16:latest
|
||||||
|
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v16:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v16:latest
|
||||||
|
|
||||||
|
- name: Configure Docker Hub login
|
||||||
|
run: |
|
||||||
|
# ECR Credential Helper & Docker Hub don't work together in config, hence reset
|
||||||
|
echo "" > /github/home/.docker/config.json
|
||||||
|
crane auth login -u ${{ secrets.NEON_DOCKERHUB_USERNAME }} -p ${{ secrets.NEON_DOCKERHUB_PASSWORD }} index.docker.io
|
||||||
|
|
||||||
|
- name: Push vm-compute-node to Docker Hub
|
||||||
|
run: |
|
||||||
|
crane push vm-compute-node-v14 neondatabase/vm-compute-node-v14:${{needs.tag.outputs.build-tag}}
|
||||||
|
crane push vm-compute-node-v15 neondatabase/vm-compute-node-v15:${{needs.tag.outputs.build-tag}}
|
||||||
|
crane push vm-compute-node-v16 neondatabase/vm-compute-node-v16:${{needs.tag.outputs.build-tag}}
|
||||||
|
|
||||||
|
- name: Push latest tags to Docker Hub
|
||||||
|
if: github.ref_name == 'main' || github.ref_name == 'release'|| github.ref_name == 'release-proxy'
|
||||||
|
run: |
|
||||||
|
crane tag neondatabase/neon:${{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/vm-compute-node-v14:${{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/compute-node-v16:${{needs.tag.outputs.build-tag}} latest
|
||||||
|
crane tag neondatabase/vm-compute-node-v16:${{needs.tag.outputs.build-tag}} latest
|
||||||
|
|
||||||
|
- name: Cleanup ECR folder
|
||||||
|
run: rm -rf ~/.ecr
|
||||||
|
|
||||||
trigger-custom-extensions-build-and-wait:
|
trigger-custom-extensions-build-and-wait:
|
||||||
needs: [ check-permissions, tag ]
|
needs: [ check-permissions, tag ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Set PR's status to pending and request a remote CI test
|
- name: Set PR's status to pending and request a remote CI test
|
||||||
run: |
|
run: |
|
||||||
@@ -1251,15 +1127,15 @@ jobs:
|
|||||||
-f deployProxy=false \
|
-f deployProxy=false \
|
||||||
-f deployStorage=true \
|
-f deployStorage=true \
|
||||||
-f deployStorageBroker=true \
|
-f deployStorageBroker=true \
|
||||||
-f deployStorageController=true \
|
|
||||||
-f branch=main \
|
-f branch=main \
|
||||||
-f dockerTag=${{needs.tag.outputs.build-tag}} \
|
-f dockerTag=${{needs.tag.outputs.build-tag}} \
|
||||||
-f deployPreprodRegion=true
|
-f deployPreprodRegion=true
|
||||||
|
|
||||||
gh workflow --repo neondatabase/aws run deploy-prod.yml --ref main \
|
gh workflow --repo neondatabase/aws run deploy-prod.yml --ref main \
|
||||||
|
-f deployPgSniRouter=false \
|
||||||
|
-f deployProxy=false \
|
||||||
-f deployStorage=true \
|
-f deployStorage=true \
|
||||||
-f deployStorageBroker=true \
|
-f deployStorageBroker=true \
|
||||||
-f deployStorageController=true \
|
|
||||||
-f branch=main \
|
-f branch=main \
|
||||||
-f dockerTag=${{needs.tag.outputs.build-tag}}
|
-f dockerTag=${{needs.tag.outputs.build-tag}}
|
||||||
elif [[ "$GITHUB_REF_NAME" == "release-proxy" ]]; then
|
elif [[ "$GITHUB_REF_NAME" == "release-proxy" ]]; then
|
||||||
@@ -1268,7 +1144,6 @@ jobs:
|
|||||||
-f deployProxy=true \
|
-f deployProxy=true \
|
||||||
-f deployStorage=false \
|
-f deployStorage=false \
|
||||||
-f deployStorageBroker=false \
|
-f deployStorageBroker=false \
|
||||||
-f deployStorageController=false \
|
|
||||||
-f branch=main \
|
-f branch=main \
|
||||||
-f dockerTag=${{needs.tag.outputs.build-tag}} \
|
-f dockerTag=${{needs.tag.outputs.build-tag}} \
|
||||||
-f deployPreprodRegion=true
|
-f deployPreprodRegion=true
|
||||||
|
|||||||
@@ -19,23 +19,30 @@ permissions: {}
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-image:
|
check-image:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
tag: ${{ steps.get-build-tools-tag.outputs.image-tag }}
|
tag: ${{ steps.get-build-tools-tag.outputs.image-tag }}
|
||||||
found: ${{ steps.check-image.outputs.found }}
|
found: ${{ steps.check-image.outputs.found }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Get build-tools image tag for the current commit
|
- name: Get build-tools image tag for the current commit
|
||||||
id: get-build-tools-tag
|
id: get-build-tools-tag
|
||||||
env:
|
env:
|
||||||
IMAGE_TAG: |
|
COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
${{ hashFiles('Dockerfile.build-tools',
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
'.github/workflows/check-build-tools-image.yml',
|
|
||||||
'.github/workflows/build-build-tools-image.yml') }}
|
|
||||||
run: |
|
run: |
|
||||||
echo "image-tag=${IMAGE_TAG}" | tee -a $GITHUB_OUTPUT
|
LAST_BUILD_TOOLS_SHA=$(
|
||||||
|
gh api \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
--method GET \
|
||||||
|
--field path=Dockerfile.build-tools \
|
||||||
|
--field sha=${COMMIT_SHA} \
|
||||||
|
--field per_page=1 \
|
||||||
|
--jq ".[0].sha" \
|
||||||
|
"/repos/${GITHUB_REPOSITORY}/commits"
|
||||||
|
)
|
||||||
|
echo "image-tag=${LAST_BUILD_TOOLS_SHA}" | tee -a $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Check if such tag found in the registry
|
- name: Check if such tag found in the registry
|
||||||
id: check-image
|
id: check-image
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ permissions: {}
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-permissions:
|
check-permissions:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Disallow CI runs on PRs from forks
|
- name: Disallow CI runs on PRs from forks
|
||||||
if: |
|
if: |
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cleanup:
|
cleanup:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Cleanup
|
- name: Cleanup
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ jobs:
|
|||||||
check-linux-arm-build:
|
check-linux-arm-build:
|
||||||
needs: [ check-permissions, build-build-tools-image ]
|
needs: [ check-permissions, build-build-tools-image ]
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
runs-on: [ self-hosted, small-arm64 ]
|
runs-on: [ self-hosted, dev, arm64 ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Use release build only, to have less debug info around
|
# Use release build only, to have less debug info around
|
||||||
@@ -232,20 +232,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Run cargo build
|
- name: Run cargo build
|
||||||
run: |
|
run: |
|
||||||
mold -run cargo build --locked $CARGO_FLAGS $CARGO_FEATURES --bins --tests -j$(nproc)
|
mold -run cargo build --locked $CARGO_FLAGS $CARGO_FEATURES --bins --tests
|
||||||
|
|
||||||
- name: Run cargo test
|
- name: Run cargo test
|
||||||
env:
|
env:
|
||||||
NEXTEST_RETRIES: 3
|
NEXTEST_RETRIES: 3
|
||||||
run: |
|
run: |
|
||||||
cargo nextest run $CARGO_FEATURES -j$(nproc)
|
cargo nextest run $CARGO_FEATURES
|
||||||
|
|
||||||
# Run separate tests for real S3
|
# Run separate tests for real S3
|
||||||
export ENABLE_REAL_S3_REMOTE_STORAGE=nonempty
|
export ENABLE_REAL_S3_REMOTE_STORAGE=nonempty
|
||||||
export REMOTE_STORAGE_S3_BUCKET=neon-github-ci-tests
|
export REMOTE_STORAGE_S3_BUCKET=neon-github-ci-tests
|
||||||
export REMOTE_STORAGE_S3_REGION=eu-central-1
|
export REMOTE_STORAGE_S3_REGION=eu-central-1
|
||||||
# Avoid `$CARGO_FEATURES` since there's no `testing` feature in the e2e tests now
|
# Avoid `$CARGO_FEATURES` since there's no `testing` feature in the e2e tests now
|
||||||
cargo nextest run --package remote_storage --test test_real_s3 -j$(nproc)
|
cargo nextest run --package remote_storage --test test_real_s3
|
||||||
|
|
||||||
# Run separate tests for real Azure Blob Storage
|
# Run separate tests for real Azure Blob Storage
|
||||||
# XXX: replace region with `eu-central-1`-like region
|
# XXX: replace region with `eu-central-1`-like region
|
||||||
@@ -255,12 +255,12 @@ jobs:
|
|||||||
export REMOTE_STORAGE_AZURE_CONTAINER="${{ vars.REMOTE_STORAGE_AZURE_CONTAINER }}"
|
export REMOTE_STORAGE_AZURE_CONTAINER="${{ vars.REMOTE_STORAGE_AZURE_CONTAINER }}"
|
||||||
export REMOTE_STORAGE_AZURE_REGION="${{ vars.REMOTE_STORAGE_AZURE_REGION }}"
|
export REMOTE_STORAGE_AZURE_REGION="${{ vars.REMOTE_STORAGE_AZURE_REGION }}"
|
||||||
# Avoid `$CARGO_FEATURES` since there's no `testing` feature in the e2e tests now
|
# Avoid `$CARGO_FEATURES` since there's no `testing` feature in the e2e tests now
|
||||||
cargo nextest run --package remote_storage --test test_real_azure -j$(nproc)
|
cargo nextest run --package remote_storage --test test_real_azure
|
||||||
|
|
||||||
check-codestyle-rust-arm:
|
check-codestyle-rust-arm:
|
||||||
needs: [ check-permissions, build-build-tools-image ]
|
needs: [ check-permissions, build-build-tools-image ]
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
runs-on: [ self-hosted, small-arm64 ]
|
runs-on: [ self-hosted, dev, arm64 ]
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: ${{ needs.build-build-tools-image.outputs.image }}
|
image: ${{ needs.build-build-tools-image.outputs.image }}
|
||||||
@@ -269,11 +269,6 @@ jobs:
|
|||||||
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
||||||
options: --init
|
options: --init
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
build_type: [ debug, release ]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Fix git ownership
|
- name: Fix git ownership
|
||||||
run: |
|
run: |
|
||||||
@@ -310,35 +305,31 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "CLIPPY_COMMON_ARGS=${CLIPPY_COMMON_ARGS}" >> $GITHUB_ENV
|
echo "CLIPPY_COMMON_ARGS=${CLIPPY_COMMON_ARGS}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Run cargo clippy (debug)
|
- name: Run cargo clippy (debug)
|
||||||
if: matrix.build_type == 'debug'
|
|
||||||
run: cargo hack --feature-powerset clippy $CLIPPY_COMMON_ARGS
|
run: cargo hack --feature-powerset clippy $CLIPPY_COMMON_ARGS
|
||||||
- name: Run cargo clippy (release)
|
- name: Run cargo clippy (release)
|
||||||
if: matrix.build_type == 'release'
|
|
||||||
run: cargo hack --feature-powerset clippy --release $CLIPPY_COMMON_ARGS
|
run: cargo hack --feature-powerset clippy --release $CLIPPY_COMMON_ARGS
|
||||||
|
|
||||||
- name: Check documentation generation
|
- name: Check documentation generation
|
||||||
if: matrix.build_type == 'release'
|
run: cargo doc --workspace --no-deps --document-private-items
|
||||||
run: cargo doc --workspace --no-deps --document-private-items -j$(nproc)
|
|
||||||
env:
|
env:
|
||||||
RUSTDOCFLAGS: "-Dwarnings -Arustdoc::private_intra_doc_links"
|
RUSTDOCFLAGS: "-Dwarnings -Arustdoc::private_intra_doc_links"
|
||||||
|
|
||||||
# Use `${{ !cancelled() }}` to run quck tests after the longer clippy run
|
# Use `${{ !cancelled() }}` to run quck tests after the longer clippy run
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
if: ${{ !cancelled() && matrix.build_type == 'release' }}
|
if: ${{ !cancelled() }}
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
# https://github.com/facebookincubator/cargo-guppy/tree/bec4e0eb29dcd1faac70b1b5360267fc02bf830e/tools/cargo-hakari#2-keep-the-workspace-hack-up-to-date-in-ci
|
# https://github.com/facebookincubator/cargo-guppy/tree/bec4e0eb29dcd1faac70b1b5360267fc02bf830e/tools/cargo-hakari#2-keep-the-workspace-hack-up-to-date-in-ci
|
||||||
- name: Check rust dependencies
|
- name: Check rust dependencies
|
||||||
if: ${{ !cancelled() && matrix.build_type == 'release' }}
|
if: ${{ !cancelled() }}
|
||||||
run: |
|
run: |
|
||||||
cargo hakari generate --diff # workspace-hack Cargo.toml is up-to-date
|
cargo hakari generate --diff # workspace-hack Cargo.toml is up-to-date
|
||||||
cargo hakari manage-deps --dry-run # all workspace crates depend on workspace-hack
|
cargo hakari manage-deps --dry-run # all workspace crates depend on workspace-hack
|
||||||
|
|
||||||
# https://github.com/EmbarkStudios/cargo-deny
|
# https://github.com/EmbarkStudios/cargo-deny
|
||||||
- name: Check rust licenses/bans/advisories/sources
|
- name: Check rust licenses/bans/advisories/sources
|
||||||
if: ${{ !cancelled() && matrix.build_type == 'release' }}
|
if: ${{ !cancelled() }}
|
||||||
run: cargo deny check
|
run: cargo deny check
|
||||||
|
|
||||||
gather-rust-build-stats:
|
gather-rust-build-stats:
|
||||||
@@ -347,7 +338,7 @@ jobs:
|
|||||||
contains(github.event.pull_request.labels.*.name, 'run-extra-build-stats') ||
|
contains(github.event.pull_request.labels.*.name, 'run-extra-build-stats') ||
|
||||||
contains(github.event.pull_request.labels.*.name, 'run-extra-build-*') ||
|
contains(github.event.pull_request.labels.*.name, 'run-extra-build-*') ||
|
||||||
github.ref_name == 'main'
|
github.ref_name == 'main'
|
||||||
runs-on: [ self-hosted, large ]
|
runs-on: [ self-hosted, gen3, large ]
|
||||||
container:
|
container:
|
||||||
image: ${{ needs.build-build-tools-image.outputs.image }}
|
image: ${{ needs.build-build-tools-image.outputs.image }}
|
||||||
credentials:
|
credentials:
|
||||||
@@ -378,7 +369,7 @@ jobs:
|
|||||||
run: make walproposer-lib -j$(nproc)
|
run: make walproposer-lib -j$(nproc)
|
||||||
|
|
||||||
- name: Produce the build stats
|
- name: Produce the build stats
|
||||||
run: cargo build --all --release --timings -j$(nproc)
|
run: cargo build --all --release --timings
|
||||||
|
|
||||||
- name: Upload the build stats
|
- name: Upload the build stats
|
||||||
id: upload-stats
|
id: upload-stats
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
test-postgres-client-libs:
|
test-postgres-client-libs:
|
||||||
# TODO: switch to gen2 runner, requires docker
|
# TODO: switch to gen2 runner, requires docker
|
||||||
runs-on: ubuntu-22.04
|
runs-on: [ ubuntu-latest ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEFAULT_PG_VERSION: 14
|
DEFAULT_PG_VERSION: 14
|
||||||
|
|||||||
@@ -20,13 +20,12 @@ defaults:
|
|||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: pin-build-tools-image-${{ inputs.from-tag }}
|
group: pin-build-tools-image-${{ inputs.from-tag }}
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tag-image:
|
tag-image:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FROM_TAG: ${{ inputs.from-tag }}
|
FROM_TAG: ${{ inputs.from-tag }}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
notify:
|
notify:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: [ ubuntu-latest ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: neondatabase/dev-actions/release-pr-notify@main
|
- uses: neondatabase/dev-actions/release-pr-notify@main
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ defaults:
|
|||||||
jobs:
|
jobs:
|
||||||
create-storage-release-branch:
|
create-storage-release-branch:
|
||||||
if: ${{ github.event.schedule == '0 6 * * MON' || format('{0}', inputs.create-storage-release-branch) == 'true' }}
|
if: ${{ github.event.schedule == '0 6 * * MON' || format('{0}', inputs.create-storage-release-branch) == 'true' }}
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # for `git push`
|
contents: write # for `git push`
|
||||||
@@ -52,22 +52,20 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
TITLE="Storage & Compute release ${RELEASE_DATE}"
|
|
||||||
|
|
||||||
cat << EOF > body.md
|
cat << EOF > body.md
|
||||||
## ${TITLE}
|
## Release ${RELEASE_DATE}
|
||||||
|
|
||||||
**Please merge this Pull Request using 'Create a merge commit' button**
|
**Please merge this Pull Request using 'Create a merge commit' button**
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
gh pr create --title "${TITLE}" \
|
gh pr create --title "Release ${RELEASE_DATE}" \
|
||||||
--body-file "body.md" \
|
--body-file "body.md" \
|
||||||
--head "${RELEASE_BRANCH}" \
|
--head "${RELEASE_BRANCH}" \
|
||||||
--base "release"
|
--base "release"
|
||||||
|
|
||||||
create-proxy-release-branch:
|
create-proxy-release-branch:
|
||||||
if: ${{ github.event.schedule == '0 6 * * THU' || format('{0}', inputs.create-proxy-release-branch) == 'true' }}
|
if: ${{ github.event.schedule == '0 6 * * THU' || format('{0}', inputs.create-proxy-release-branch) == 'true' }}
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # for `git push`
|
contents: write # for `git push`
|
||||||
@@ -93,15 +91,13 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
TITLE="Proxy release ${RELEASE_DATE}"
|
|
||||||
|
|
||||||
cat << EOF > body.md
|
cat << EOF > body.md
|
||||||
## ${TITLE}
|
## Proxy release ${RELEASE_DATE}
|
||||||
|
|
||||||
**Please merge this Pull Request using 'Create a merge commit' button**
|
**Please merge this Pull Request using 'Create a merge commit' button**
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
gh pr create --title "${TITLE}" \
|
gh pr create --title "Proxy release ${RELEASE_DATE}" \
|
||||||
--body-file "body.md" \
|
--body-file "body.md" \
|
||||||
--head "${RELEASE_BRANCH}" \
|
--head "${RELEASE_BRANCH}" \
|
||||||
--base "release-proxy"
|
--base "release-proxy"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
cancel-previous-e2e-tests:
|
cancel-previous-e2e-tests:
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Cancel previous e2e-tests runs for this PR
|
- name: Cancel previous e2e-tests runs for this PR
|
||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
--field concurrency_group="${{ env.E2E_CONCURRENCY_GROUP }}"
|
--field concurrency_group="${{ env.E2E_CONCURRENCY_GROUP }}"
|
||||||
|
|
||||||
tag:
|
tag:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: [ ubuntu-latest ]
|
||||||
outputs:
|
outputs:
|
||||||
build-tag: ${{ steps.build-tag.outputs.tag }}
|
build-tag: ${{ steps.build-tag.outputs.tag }}
|
||||||
|
|
||||||
@@ -62,14 +62,14 @@ jobs:
|
|||||||
|
|
||||||
trigger-e2e-tests:
|
trigger-e2e-tests:
|
||||||
needs: [ tag ]
|
needs: [ tag ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: [ self-hosted, gen3, small ]
|
||||||
env:
|
env:
|
||||||
TAG: ${{ needs.tag.outputs.build-tag }}
|
TAG: ${{ needs.tag.outputs.build-tag }}
|
||||||
|
container:
|
||||||
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/base:pinned
|
||||||
|
options: --init
|
||||||
steps:
|
steps:
|
||||||
- name: check if ecr image are present
|
- name: check if ecr image are present
|
||||||
env:
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_DEV }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY_DEV }}
|
|
||||||
run: |
|
run: |
|
||||||
for REPO in neon compute-tools compute-node-v14 vm-compute-node-v14 compute-node-v15 vm-compute-node-v15 compute-node-v16 vm-compute-node-v16; do
|
for REPO in neon compute-tools compute-node-v14 vm-compute-node-v14 compute-node-v15 vm-compute-node-v15 compute-node-v16 vm-compute-node-v16; do
|
||||||
OUTPUT=$(aws ecr describe-images --repository-name ${REPO} --region eu-central-1 --query "imageDetails[?imageTags[?contains(@, '${TAG}')]]" --output text)
|
OUTPUT=$(aws ecr describe-images --repository-name ${REPO} --region eu-central-1 --query "imageDetails[?imageTags[?contains(@, '${TAG}')]]" --output text)
|
||||||
@@ -79,55 +79,41 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Set e2e-platforms
|
|
||||||
id: e2e-platforms
|
|
||||||
env:
|
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
# Default set of platforms to run e2e tests on
|
|
||||||
platforms='["docker", "k8s"]'
|
|
||||||
|
|
||||||
# If the PR changes vendor/, pgxn/ or libs/vm_monitor/ directories, or Dockerfile.compute-node, add k8s-neonvm to the list of platforms.
|
|
||||||
# If the workflow run is not a pull request, add k8s-neonvm to the list.
|
|
||||||
if [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then
|
|
||||||
for f in $(gh api "/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename'); do
|
|
||||||
case "$f" in
|
|
||||||
vendor/*|pgxn/*|libs/vm_monitor/*|Dockerfile.compute-node)
|
|
||||||
platforms=$(echo "${platforms}" | jq --compact-output '. += ["k8s-neonvm"] | unique')
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
# no-op
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
else
|
|
||||||
platforms=$(echo "${platforms}" | jq --compact-output '. += ["k8s-neonvm"] | unique')
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "e2e-platforms=${platforms}" | tee -a $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Set PR's status to pending and request a remote CI test
|
- name: Set PR's status to pending and request a remote CI test
|
||||||
env:
|
|
||||||
E2E_PLATFORMS: ${{ steps.e2e-platforms.outputs.e2e-platforms }}
|
|
||||||
COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
||||||
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
|
||||||
run: |
|
run: |
|
||||||
REMOTE_REPO="${GITHUB_REPOSITORY_OWNER}/cloud"
|
# For pull requests, GH Actions set "github.sha" variable to point at a fake merge commit
|
||||||
|
# but we need to use a real sha of a latest commit in the PR's branch for the e2e job,
|
||||||
|
# to place a job run status update later.
|
||||||
|
COMMIT_SHA=${{ github.event.pull_request.head.sha }}
|
||||||
|
# For non-PR kinds of runs, the above will produce an empty variable, pick the original sha value for those
|
||||||
|
COMMIT_SHA=${COMMIT_SHA:-${{ github.sha }}}
|
||||||
|
|
||||||
gh api "/repos/${GITHUB_REPOSITORY}/statuses/${COMMIT_SHA}" \
|
REMOTE_REPO="${{ github.repository_owner }}/cloud"
|
||||||
--method POST \
|
|
||||||
--raw-field "state=pending" \
|
|
||||||
--raw-field "description=[$REMOTE_REPO] Remote CI job is about to start" \
|
|
||||||
--raw-field "context=neon-cloud-e2e"
|
|
||||||
|
|
||||||
gh workflow --repo ${REMOTE_REPO} \
|
curl -f -X POST \
|
||||||
run testing.yml \
|
https://api.github.com/repos/${{ github.repository }}/statuses/$COMMIT_SHA \
|
||||||
--ref "main" \
|
-H "Accept: application/vnd.github.v3+json" \
|
||||||
--raw-field "ci_job_name=neon-cloud-e2e" \
|
--user "${{ secrets.CI_ACCESS_TOKEN }}" \
|
||||||
--raw-field "commit_hash=$COMMIT_SHA" \
|
--data \
|
||||||
--raw-field "remote_repo=${GITHUB_REPOSITORY}" \
|
"{
|
||||||
--raw-field "storage_image_tag=${TAG}" \
|
\"state\": \"pending\",
|
||||||
--raw-field "compute_image_tag=${TAG}" \
|
\"context\": \"neon-cloud-e2e\",
|
||||||
--raw-field "concurrency_group=${E2E_CONCURRENCY_GROUP}" \
|
\"description\": \"[$REMOTE_REPO] Remote CI job is about to start\"
|
||||||
--raw-field "e2e-platforms=${E2E_PLATFORMS}"
|
}"
|
||||||
|
|
||||||
|
curl -f -X POST \
|
||||||
|
https://api.github.com/repos/$REMOTE_REPO/actions/workflows/testing.yml/dispatches \
|
||||||
|
-H "Accept: application/vnd.github.v3+json" \
|
||||||
|
--user "${{ secrets.CI_ACCESS_TOKEN }}" \
|
||||||
|
--data \
|
||||||
|
"{
|
||||||
|
\"ref\": \"main\",
|
||||||
|
\"inputs\": {
|
||||||
|
\"ci_job_name\": \"neon-cloud-e2e\",
|
||||||
|
\"commit_hash\": \"$COMMIT_SHA\",
|
||||||
|
\"remote_repo\": \"${{ github.repository }}\",
|
||||||
|
\"storage_image_tag\": \"${TAG}\",
|
||||||
|
\"compute_image_tag\": \"${TAG}\",
|
||||||
|
\"concurrency_group\": \"${{ env.E2E_CONCURRENCY_GROUP }}\"
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/compute_tools/ @neondatabase/control-plane @neondatabase/compute
|
/compute_tools/ @neondatabase/control-plane @neondatabase/compute
|
||||||
/storage_controller @neondatabase/storage
|
/control_plane/attachment_service @neondatabase/storage
|
||||||
/libs/pageserver_api/ @neondatabase/storage
|
/libs/pageserver_api/ @neondatabase/storage
|
||||||
/libs/postgres_ffi/ @neondatabase/compute @neondatabase/safekeepers
|
/libs/postgres_ffi/ @neondatabase/compute @neondatabase/safekeepers
|
||||||
/libs/remote_storage/ @neondatabase/storage
|
/libs/remote_storage/ @neondatabase/storage
|
||||||
|
|||||||
Generated
+490
-801
File diff suppressed because it is too large
Load Diff
+37
-43
@@ -3,7 +3,7 @@ resolver = "2"
|
|||||||
members = [
|
members = [
|
||||||
"compute_tools",
|
"compute_tools",
|
||||||
"control_plane",
|
"control_plane",
|
||||||
"control_plane/storcon_cli",
|
"control_plane/attachment_service",
|
||||||
"pageserver",
|
"pageserver",
|
||||||
"pageserver/compaction",
|
"pageserver/compaction",
|
||||||
"pageserver/ctl",
|
"pageserver/ctl",
|
||||||
@@ -12,8 +12,7 @@ members = [
|
|||||||
"proxy",
|
"proxy",
|
||||||
"safekeeper",
|
"safekeeper",
|
||||||
"storage_broker",
|
"storage_broker",
|
||||||
"storage_controller",
|
"s3_scrubber",
|
||||||
"storage_scrubber",
|
|
||||||
"workspace_hack",
|
"workspace_hack",
|
||||||
"trace",
|
"trace",
|
||||||
"libs/compute_api",
|
"libs/compute_api",
|
||||||
@@ -41,26 +40,24 @@ license = "Apache-2.0"
|
|||||||
|
|
||||||
## All dependency versions, used in the project
|
## All dependency versions, used in the project
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
ahash = "0.8"
|
|
||||||
anyhow = { version = "1.0", features = ["backtrace"] }
|
anyhow = { version = "1.0", features = ["backtrace"] }
|
||||||
arc-swap = "1.6"
|
arc-swap = "1.6"
|
||||||
async-compression = { version = "0.4.0", features = ["tokio", "gzip", "zstd"] }
|
async-compression = { version = "0.4.0", features = ["tokio", "gzip", "zstd"] }
|
||||||
atomic-take = "1.1.0"
|
azure_core = "0.18"
|
||||||
azure_core = { version = "0.19", default-features = false, features = ["enable_reqwest_rustls", "hmac_rust"] }
|
azure_identity = "0.18"
|
||||||
azure_identity = { version = "0.19", default-features = false, features = ["enable_reqwest_rustls"] }
|
azure_storage = "0.18"
|
||||||
azure_storage = { version = "0.19", default-features = false, features = ["enable_reqwest_rustls"] }
|
azure_storage_blobs = "0.18"
|
||||||
azure_storage_blobs = { version = "0.19", default-features = false, features = ["enable_reqwest_rustls"] }
|
|
||||||
flate2 = "1.0.26"
|
flate2 = "1.0.26"
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
aws-config = { version = "1.3", default-features = false, features=["rustls"] }
|
aws-config = { version = "1.1.4", default-features = false, features=["rustls"] }
|
||||||
aws-sdk-s3 = "1.26"
|
aws-sdk-s3 = "1.14"
|
||||||
aws-sdk-iam = "1.15.0"
|
aws-sdk-iam = "1.15.0"
|
||||||
aws-smithy-async = { version = "1.2.1", default-features = false, features=["rt-tokio"] }
|
aws-smithy-async = { version = "1.1.4", default-features = false, features=["rt-tokio"] }
|
||||||
aws-smithy-types = "1.1.9"
|
aws-smithy-types = "1.1.4"
|
||||||
aws-credential-types = "1.2.0"
|
aws-credential-types = "1.1.4"
|
||||||
aws-sigv4 = { version = "1.2.1", features = ["sign-http"] }
|
aws-sigv4 = { version = "1.2.0", features = ["sign-http"] }
|
||||||
aws-types = "1.2.0"
|
aws-types = "1.1.7"
|
||||||
axum = { version = "0.6.20", features = ["ws"] }
|
axum = { version = "0.6.20", features = ["ws"] }
|
||||||
base64 = "0.13.0"
|
base64 = "0.13.0"
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
@@ -75,7 +72,6 @@ clap = { version = "4.0", features = ["derive"] }
|
|||||||
comfy-table = "6.1"
|
comfy-table = "6.1"
|
||||||
const_format = "0.2"
|
const_format = "0.2"
|
||||||
crc32c = "0.6"
|
crc32c = "0.6"
|
||||||
crossbeam-deque = "0.8.5"
|
|
||||||
crossbeam-utils = "0.8.5"
|
crossbeam-utils = "0.8.5"
|
||||||
dashmap = { version = "5.5.0", features = ["raw-api"] }
|
dashmap = { version = "5.5.0", features = ["raw-api"] }
|
||||||
either = "1.8"
|
either = "1.8"
|
||||||
@@ -83,14 +79,13 @@ enum-map = "2.4.2"
|
|||||||
enumset = "1.0.12"
|
enumset = "1.0.12"
|
||||||
fail = "0.5.0"
|
fail = "0.5.0"
|
||||||
fallible-iterator = "0.2"
|
fallible-iterator = "0.2"
|
||||||
framed-websockets = { version = "0.1.0", git = "https://github.com/neondatabase/framed-websockets" }
|
|
||||||
fs2 = "0.4.3"
|
fs2 = "0.4.3"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
futures-core = "0.3"
|
futures-core = "0.3"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
git-version = "0.3"
|
git-version = "0.3"
|
||||||
hashbrown = "0.14"
|
hashbrown = "0.13"
|
||||||
hashlink = "0.9.1"
|
hashlink = "0.8.4"
|
||||||
hdrhistogram = "7.5.2"
|
hdrhistogram = "7.5.2"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
hex-literal = "0.4"
|
hex-literal = "0.4"
|
||||||
@@ -101,8 +96,7 @@ http-types = { version = "2", default-features = false }
|
|||||||
humantime = "2.1"
|
humantime = "2.1"
|
||||||
humantime-serde = "1.1.1"
|
humantime-serde = "1.1.1"
|
||||||
hyper = "0.14"
|
hyper = "0.14"
|
||||||
tokio-tungstenite = "0.20.0"
|
hyper-tungstenite = "0.11"
|
||||||
indexmap = "2"
|
|
||||||
inotify = "0.10.2"
|
inotify = "0.10.2"
|
||||||
ipnet = "2.9.0"
|
ipnet = "2.9.0"
|
||||||
itertools = "0.10"
|
itertools = "0.10"
|
||||||
@@ -111,32 +105,32 @@ lasso = "0.7"
|
|||||||
leaky-bucket = "1.0.1"
|
leaky-bucket = "1.0.1"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
md5 = "0.7.0"
|
md5 = "0.7.0"
|
||||||
measured = { version = "0.0.21", features=["lasso"] }
|
measured = { version = "0.0.13", features=["default", "lasso"] }
|
||||||
measured-process = { version = "0.0.21" }
|
|
||||||
memoffset = "0.8"
|
memoffset = "0.8"
|
||||||
|
native-tls = "0.2"
|
||||||
nix = { version = "0.27", features = ["fs", "process", "socket", "signal", "poll"] }
|
nix = { version = "0.27", features = ["fs", "process", "socket", "signal", "poll"] }
|
||||||
notify = "6.0.0"
|
notify = "6.0.0"
|
||||||
num_cpus = "1.15"
|
num_cpus = "1.15"
|
||||||
num-traits = "0.2.15"
|
num-traits = "0.2.15"
|
||||||
once_cell = "1.13"
|
once_cell = "1.13"
|
||||||
opentelemetry = "0.20.0"
|
opentelemetry = "0.20.0"
|
||||||
opentelemetry-otlp = { version = "0.13.0", default-features=false, features = ["http-proto", "trace", "http", "reqwest-client"] }
|
opentelemetry-otlp = { version = "0.13.0", default_features=false, features = ["http-proto", "trace", "http", "reqwest-client"] }
|
||||||
opentelemetry-semantic-conventions = "0.12.0"
|
opentelemetry-semantic-conventions = "0.12.0"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
parquet = { version = "51.0.0", default-features = false, features = ["zstd"] }
|
parquet = { version = "49.0.0", default-features = false, features = ["zstd"] }
|
||||||
parquet_derive = "51.0.0"
|
parquet_derive = "49.0.0"
|
||||||
pbkdf2 = { version = "0.12.1", features = ["simple", "std"] }
|
pbkdf2 = { version = "0.12.1", features = ["simple", "std"] }
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
procfs = "0.14"
|
procfs = "0.14"
|
||||||
prometheus = {version = "0.13", default-features=false, features = ["process"]} # removes protobuf dependency
|
prometheus = {version = "0.13", default_features=false, features = ["process"]} # removes protobuf dependency
|
||||||
prost = "0.11"
|
prost = "0.11"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
redis = { version = "0.25.2", features = ["tokio-rustls-comp", "keep-alive"] }
|
redis = { version = "0.25.2", features = ["tokio-rustls-comp", "keep-alive"] }
|
||||||
regex = "1.10.2"
|
regex = "1.10.2"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
|
||||||
reqwest-tracing = { version = "0.5", features = ["opentelemetry_0_20"] }
|
reqwest-tracing = { version = "0.4.7", features = ["opentelemetry_0_20"] }
|
||||||
reqwest-middleware = "0.3.0"
|
reqwest-middleware = "0.2.0"
|
||||||
reqwest-retry = "0.5"
|
reqwest-retry = "0.2.2"
|
||||||
routerify = "3"
|
routerify = "3"
|
||||||
rpds = "0.13"
|
rpds = "0.13"
|
||||||
rustc-hash = "1.1.0"
|
rustc-hash = "1.1.0"
|
||||||
@@ -146,7 +140,7 @@ rustls-split = "0.3"
|
|||||||
scopeguard = "1.1"
|
scopeguard = "1.1"
|
||||||
sysinfo = "0.29.2"
|
sysinfo = "0.29.2"
|
||||||
sd-notify = "0.4.1"
|
sd-notify = "0.4.1"
|
||||||
sentry = { version = "0.32", default-features = false, features = ["backtrace", "contexts", "panic", "rustls", "reqwest" ] }
|
sentry = { version = "0.31", default-features = false, features = ["backtrace", "contexts", "panic", "rustls", "reqwest" ] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_path_to_error = "0.1"
|
serde_path_to_error = "0.1"
|
||||||
@@ -160,12 +154,11 @@ socket2 = "0.5"
|
|||||||
strum = "0.24"
|
strum = "0.24"
|
||||||
strum_macros = "0.24"
|
strum_macros = "0.24"
|
||||||
"subtle" = "2.5.0"
|
"subtle" = "2.5.0"
|
||||||
# Our PR https://github.com/nical/rust_debug/pull/4 has been merged but no new version released yet
|
svg_fmt = "0.4.1"
|
||||||
svg_fmt = { git = "https://github.com/nical/rust_debug", rev = "28a7d96eecff2f28e75b1ea09f2d499a60d0e3b4" }
|
|
||||||
sync_wrapper = "0.1.2"
|
sync_wrapper = "0.1.2"
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
task-local-extensions = "0.1.4"
|
task-local-extensions = "0.1.4"
|
||||||
test-context = "0.3"
|
test-context = "0.1"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tikv-jemallocator = "0.5"
|
tikv-jemallocator = "0.5"
|
||||||
tikv-jemalloc-ctl = "0.5"
|
tikv-jemalloc-ctl = "0.5"
|
||||||
@@ -180,17 +173,16 @@ tokio-util = { version = "0.7.10", features = ["io", "rt"] }
|
|||||||
toml = "0.7"
|
toml = "0.7"
|
||||||
toml_edit = "0.19"
|
toml_edit = "0.19"
|
||||||
tonic = {version = "0.9", features = ["tls", "tls-roots"]}
|
tonic = {version = "0.9", features = ["tls", "tls-roots"]}
|
||||||
tower-service = "0.3.2"
|
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
tracing-opentelemetry = "0.21.0"
|
tracing-opentelemetry = "0.20.0"
|
||||||
tracing-subscriber = { version = "0.3", default-features = false, features = ["smallvec", "fmt", "tracing-log", "std", "env-filter", "json", "ansi"] }
|
tracing-subscriber = { version = "0.3", default_features = false, features = ["smallvec", "fmt", "tracing-log", "std", "env-filter", "json"] }
|
||||||
twox-hash = { version = "1.6.3", default-features = false }
|
twox-hash = { version = "1.6.3", default-features = false }
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
urlencoding = "2.1"
|
urlencoding = "2.1"
|
||||||
uuid = { version = "1.6.1", features = ["v4", "v7", "serde"] }
|
uuid = { version = "1.6.1", features = ["v4", "v7", "serde"] }
|
||||||
walkdir = "2.3.2"
|
walkdir = "2.3.2"
|
||||||
rustls-native-certs = "0.7"
|
webpki-roots = "0.25"
|
||||||
x509-parser = "0.15"
|
x509-parser = "0.15"
|
||||||
|
|
||||||
## TODO replace this with tracing
|
## TODO replace this with tracing
|
||||||
@@ -199,6 +191,7 @@ log = "0.4"
|
|||||||
|
|
||||||
## Libraries from neondatabase/ git forks, ideally with changes to be upstreamed
|
## Libraries from neondatabase/ git forks, ideally with changes to be upstreamed
|
||||||
postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
||||||
|
postgres-native-tls = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
||||||
postgres-protocol = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
postgres-protocol = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
||||||
postgres-types = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
postgres-types = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
||||||
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
||||||
@@ -239,12 +232,13 @@ tonic-build = "0.9"
|
|||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
|
||||||
# Needed to get `tokio-postgres-rustls` to depend on our fork.
|
# This is only needed for proxy's tests.
|
||||||
|
# TODO: we should probably fork `tokio-postgres-rustls` instead.
|
||||||
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
||||||
|
|
||||||
# bug fixes for UUID
|
# bug fixes for UUID
|
||||||
parquet = { git = "https://github.com/apache/arrow-rs", branch = "master" }
|
parquet = { git = "https://github.com/neondatabase/arrow-rs", branch = "neon-fix-bugs" }
|
||||||
parquet_derive = { git = "https://github.com/apache/arrow-rs", branch = "master" }
|
parquet_derive = { git = "https://github.com/neondatabase/arrow-rs", branch = "neon-fix-bugs" }
|
||||||
|
|
||||||
################# Binary contents sections
|
################# Binary contents sections
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ RUN set -e \
|
|||||||
&& apt install -y \
|
&& apt install -y \
|
||||||
libreadline-dev \
|
libreadline-dev \
|
||||||
libseccomp-dev \
|
libseccomp-dev \
|
||||||
|
libicu67 \
|
||||||
|
openssl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
|
||||||
&& useradd -d /data neon \
|
&& useradd -d /data neon \
|
||||||
|
|||||||
+3
-51
@@ -58,14 +58,8 @@ RUN curl -fsSL "https://github.com/protocolbuffers/protobuf/releases/download/v$
|
|||||||
&& mv protoc/include/google /usr/local/include/google \
|
&& mv protoc/include/google /usr/local/include/google \
|
||||||
&& rm -rf protoc.zip protoc
|
&& rm -rf protoc.zip protoc
|
||||||
|
|
||||||
# s5cmd
|
|
||||||
ENV S5CMD_VERSION=2.2.2
|
|
||||||
RUN curl -sL "https://github.com/peak/s5cmd/releases/download/v${S5CMD_VERSION}/s5cmd_${S5CMD_VERSION}_Linux-$(uname -m | sed 's/x86_64/64bit/g' | sed 's/aarch64/arm64/g').tar.gz" | tar zxvf - s5cmd \
|
|
||||||
&& chmod +x s5cmd \
|
|
||||||
&& mv s5cmd /usr/local/bin/s5cmd
|
|
||||||
|
|
||||||
# LLVM
|
# LLVM
|
||||||
ENV LLVM_VERSION=18
|
ENV LLVM_VERSION=17
|
||||||
RUN curl -fsSL 'https://apt.llvm.org/llvm-snapshot.gpg.key' | apt-key add - \
|
RUN curl -fsSL 'https://apt.llvm.org/llvm-snapshot.gpg.key' | apt-key add - \
|
||||||
&& echo "deb http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-${LLVM_VERSION} main" > /etc/apt/sources.list.d/llvm.stable.list \
|
&& echo "deb http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-${LLVM_VERSION} main" > /etc/apt/sources.list.d/llvm.stable.list \
|
||||||
&& apt update \
|
&& apt update \
|
||||||
@@ -87,7 +81,7 @@ RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o "aws
|
|||||||
&& rm awscliv2.zip
|
&& rm awscliv2.zip
|
||||||
|
|
||||||
# Mold: A Modern Linker
|
# Mold: A Modern Linker
|
||||||
ENV MOLD_VERSION v2.31.0
|
ENV MOLD_VERSION v2.4.0
|
||||||
RUN set -e \
|
RUN set -e \
|
||||||
&& git clone https://github.com/rui314/mold.git \
|
&& git clone https://github.com/rui314/mold.git \
|
||||||
&& mkdir mold/build \
|
&& mkdir mold/build \
|
||||||
@@ -112,45 +106,6 @@ RUN for package in Capture::Tiny DateTime Devel::Cover Digest::MD5 File::Spec JS
|
|||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf ../lcov.tar.gz
|
&& rm -rf ../lcov.tar.gz
|
||||||
|
|
||||||
# Compile and install the static OpenSSL library
|
|
||||||
ENV OPENSSL_VERSION=3.2.2
|
|
||||||
ENV OPENSSL_PREFIX=/usr/local/openssl
|
|
||||||
RUN wget -O /tmp/openssl-${OPENSSL_VERSION}.tar.gz https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz && \
|
|
||||||
echo "197149c18d9e9f292c43f0400acaba12e5f52cacfe050f3d199277ea738ec2e7 /tmp/openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum --check && \
|
|
||||||
cd /tmp && \
|
|
||||||
tar xzvf /tmp/openssl-${OPENSSL_VERSION}.tar.gz && \
|
|
||||||
rm /tmp/openssl-${OPENSSL_VERSION}.tar.gz && \
|
|
||||||
cd /tmp/openssl-${OPENSSL_VERSION} && \
|
|
||||||
./config --prefix=${OPENSSL_PREFIX} -static --static no-shared -fPIC && \
|
|
||||||
make -j "$(nproc)" && \
|
|
||||||
make install && \
|
|
||||||
cd /tmp && \
|
|
||||||
rm -rf /tmp/openssl-${OPENSSL_VERSION}
|
|
||||||
|
|
||||||
# Use the same version of libicu as the compute nodes so that
|
|
||||||
# clusters created using inidb on pageserver can be used by computes.
|
|
||||||
#
|
|
||||||
# TODO: at this time, Dockerfile.compute-node uses the debian bullseye libicu
|
|
||||||
# package, which is 67.1. We're duplicating that knowledge here, and also, technically,
|
|
||||||
# Debian has a few patches on top of 67.1 that we're not adding here.
|
|
||||||
ENV ICU_VERSION=67.1
|
|
||||||
ENV ICU_PREFIX=/usr/local/icu
|
|
||||||
|
|
||||||
# Download and build static ICU
|
|
||||||
RUN wget -O /tmp/libicu-${ICU_VERSION}.tgz https://github.com/unicode-org/icu/releases/download/release-${ICU_VERSION//./-}/icu4c-${ICU_VERSION//./_}-src.tgz && \
|
|
||||||
echo "94a80cd6f251a53bd2a997f6f1b5ac6653fe791dfab66e1eb0227740fb86d5dc /tmp/libicu-${ICU_VERSION}.tgz" | sha256sum --check && \
|
|
||||||
mkdir /tmp/icu && \
|
|
||||||
pushd /tmp/icu && \
|
|
||||||
tar -xzf /tmp/libicu-${ICU_VERSION}.tgz && \
|
|
||||||
pushd icu/source && \
|
|
||||||
./configure --prefix=${ICU_PREFIX} --enable-static --enable-shared=no CXXFLAGS="-fPIC" CFLAGS="-fPIC" && \
|
|
||||||
make -j "$(nproc)" && \
|
|
||||||
make install && \
|
|
||||||
popd && \
|
|
||||||
rm -rf icu && \
|
|
||||||
rm -f /tmp/libicu-${ICU_VERSION}.tgz && \
|
|
||||||
popd
|
|
||||||
|
|
||||||
# Switch to nonroot user
|
# Switch to nonroot user
|
||||||
USER nonroot:nonroot
|
USER nonroot:nonroot
|
||||||
WORKDIR /home/nonroot
|
WORKDIR /home/nonroot
|
||||||
@@ -180,7 +135,7 @@ WORKDIR /home/nonroot
|
|||||||
|
|
||||||
# Rust
|
# Rust
|
||||||
# Please keep the version of llvm (installed above) in sync with rust llvm (`rustc --version --verbose | grep LLVM`)
|
# Please keep the version of llvm (installed above) in sync with rust llvm (`rustc --version --verbose | grep LLVM`)
|
||||||
ENV RUSTC_VERSION=1.79.0
|
ENV RUSTC_VERSION=1.77.0
|
||||||
ENV RUSTUP_HOME="/home/nonroot/.rustup"
|
ENV RUSTUP_HOME="/home/nonroot/.rustup"
|
||||||
ENV PATH="/home/nonroot/.cargo/bin:${PATH}"
|
ENV PATH="/home/nonroot/.cargo/bin:${PATH}"
|
||||||
RUN curl -sSO https://static.rust-lang.org/rustup/dist/$(uname -m)-unknown-linux-gnu/rustup-init && whoami && \
|
RUN curl -sSO https://static.rust-lang.org/rustup/dist/$(uname -m)-unknown-linux-gnu/rustup-init && whoami && \
|
||||||
@@ -209,6 +164,3 @@ RUN whoami \
|
|||||||
&& rustup --version --verbose \
|
&& rustup --version --verbose \
|
||||||
&& rustc --version --verbose \
|
&& rustc --version --verbose \
|
||||||
&& clang --version
|
&& clang --version
|
||||||
|
|
||||||
# Set following flag to check in Makefile if its running in Docker
|
|
||||||
RUN touch /home/nonroot/.docker_build
|
|
||||||
|
|||||||
+38
-110
@@ -89,7 +89,7 @@ RUN apt update && \
|
|||||||
# SFCGAL > 1.3 requires CGAL > 5.2, Bullseye's libcgal-dev is 5.2
|
# SFCGAL > 1.3 requires CGAL > 5.2, Bullseye's libcgal-dev is 5.2
|
||||||
RUN wget https://gitlab.com/Oslandia/SFCGAL/-/archive/v1.3.10/SFCGAL-v1.3.10.tar.gz -O SFCGAL.tar.gz && \
|
RUN wget https://gitlab.com/Oslandia/SFCGAL/-/archive/v1.3.10/SFCGAL-v1.3.10.tar.gz -O SFCGAL.tar.gz && \
|
||||||
echo "4e39b3b2adada6254a7bdba6d297bb28e1a9835a9f879b74f37e2dab70203232 SFCGAL.tar.gz" | sha256sum --check && \
|
echo "4e39b3b2adada6254a7bdba6d297bb28e1a9835a9f879b74f37e2dab70203232 SFCGAL.tar.gz" | sha256sum --check && \
|
||||||
mkdir sfcgal-src && cd sfcgal-src && tar xzf ../SFCGAL.tar.gz --strip-components=1 -C . && \
|
mkdir sfcgal-src && cd sfcgal-src && tar xvzf ../SFCGAL.tar.gz --strip-components=1 -C . && \
|
||||||
cmake -DCMAKE_BUILD_TYPE=Release . && make -j $(getconf _NPROCESSORS_ONLN) && \
|
cmake -DCMAKE_BUILD_TYPE=Release . && make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
DESTDIR=/sfcgal make install -j $(getconf _NPROCESSORS_ONLN) && \
|
DESTDIR=/sfcgal make install -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make clean && cp -R /sfcgal/* /
|
make clean && cp -R /sfcgal/* /
|
||||||
@@ -98,7 +98,7 @@ ENV PATH "/usr/local/pgsql/bin:$PATH"
|
|||||||
|
|
||||||
RUN wget https://download.osgeo.org/postgis/source/postgis-3.3.3.tar.gz -O postgis.tar.gz && \
|
RUN wget https://download.osgeo.org/postgis/source/postgis-3.3.3.tar.gz -O postgis.tar.gz && \
|
||||||
echo "74eb356e3f85f14233791013360881b6748f78081cc688ff9d6f0f673a762d13 postgis.tar.gz" | sha256sum --check && \
|
echo "74eb356e3f85f14233791013360881b6748f78081cc688ff9d6f0f673a762d13 postgis.tar.gz" | sha256sum --check && \
|
||||||
mkdir postgis-src && cd postgis-src && tar xzf ../postgis.tar.gz --strip-components=1 -C . && \
|
mkdir postgis-src && cd postgis-src && tar xvzf ../postgis.tar.gz --strip-components=1 -C . && \
|
||||||
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /before.txt &&\
|
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /before.txt &&\
|
||||||
./autogen.sh && \
|
./autogen.sh && \
|
||||||
./configure --with-sfcgal=/usr/local/bin/sfcgal-config && \
|
./configure --with-sfcgal=/usr/local/bin/sfcgal-config && \
|
||||||
@@ -124,7 +124,7 @@ RUN wget https://download.osgeo.org/postgis/source/postgis-3.3.3.tar.gz -O postg
|
|||||||
|
|
||||||
RUN wget https://github.com/pgRouting/pgrouting/archive/v3.4.2.tar.gz -O pgrouting.tar.gz && \
|
RUN wget https://github.com/pgRouting/pgrouting/archive/v3.4.2.tar.gz -O pgrouting.tar.gz && \
|
||||||
echo "cac297c07d34460887c4f3b522b35c470138760fe358e351ad1db4edb6ee306e pgrouting.tar.gz" | sha256sum --check && \
|
echo "cac297c07d34460887c4f3b522b35c470138760fe358e351ad1db4edb6ee306e pgrouting.tar.gz" | sha256sum --check && \
|
||||||
mkdir pgrouting-src && cd pgrouting-src && tar xzf ../pgrouting.tar.gz --strip-components=1 -C . && \
|
mkdir pgrouting-src && cd pgrouting-src && tar xvzf ../pgrouting.tar.gz --strip-components=1 -C . && \
|
||||||
mkdir build && cd build && \
|
mkdir build && cd build && \
|
||||||
cmake -DCMAKE_BUILD_TYPE=Release .. && \
|
cmake -DCMAKE_BUILD_TYPE=Release .. && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
@@ -149,7 +149,7 @@ RUN apt update && \
|
|||||||
|
|
||||||
RUN wget https://github.com/plv8/plv8/archive/refs/tags/v3.1.10.tar.gz -O plv8.tar.gz && \
|
RUN wget https://github.com/plv8/plv8/archive/refs/tags/v3.1.10.tar.gz -O plv8.tar.gz && \
|
||||||
echo "7096c3290928561f0d4901b7a52794295dc47f6303102fae3f8e42dd575ad97d plv8.tar.gz" | sha256sum --check && \
|
echo "7096c3290928561f0d4901b7a52794295dc47f6303102fae3f8e42dd575ad97d plv8.tar.gz" | sha256sum --check && \
|
||||||
mkdir plv8-src && cd plv8-src && tar xzf ../plv8.tar.gz --strip-components=1 -C . && \
|
mkdir plv8-src && cd plv8-src && tar xvzf ../plv8.tar.gz --strip-components=1 -C . && \
|
||||||
# generate and copy upgrade scripts
|
# generate and copy upgrade scripts
|
||||||
mkdir -p upgrade && ./generate_upgrade.sh 3.1.10 && \
|
mkdir -p upgrade && ./generate_upgrade.sh 3.1.10 && \
|
||||||
cp upgrade/* /usr/local/pgsql/share/extension/ && \
|
cp upgrade/* /usr/local/pgsql/share/extension/ && \
|
||||||
@@ -194,7 +194,7 @@ RUN case "$(uname -m)" in \
|
|||||||
|
|
||||||
RUN wget https://github.com/uber/h3/archive/refs/tags/v4.1.0.tar.gz -O h3.tar.gz && \
|
RUN wget https://github.com/uber/h3/archive/refs/tags/v4.1.0.tar.gz -O h3.tar.gz && \
|
||||||
echo "ec99f1f5974846bde64f4513cf8d2ea1b8d172d2218ab41803bf6a63532272bc h3.tar.gz" | sha256sum --check && \
|
echo "ec99f1f5974846bde64f4513cf8d2ea1b8d172d2218ab41803bf6a63532272bc h3.tar.gz" | sha256sum --check && \
|
||||||
mkdir h3-src && cd h3-src && tar xzf ../h3.tar.gz --strip-components=1 -C . && \
|
mkdir h3-src && cd h3-src && tar xvzf ../h3.tar.gz --strip-components=1 -C . && \
|
||||||
mkdir build && cd build && \
|
mkdir build && cd build && \
|
||||||
cmake .. -DCMAKE_BUILD_TYPE=Release && \
|
cmake .. -DCMAKE_BUILD_TYPE=Release && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
@@ -204,7 +204,7 @@ RUN wget https://github.com/uber/h3/archive/refs/tags/v4.1.0.tar.gz -O h3.tar.gz
|
|||||||
|
|
||||||
RUN wget https://github.com/zachasme/h3-pg/archive/refs/tags/v4.1.3.tar.gz -O h3-pg.tar.gz && \
|
RUN wget https://github.com/zachasme/h3-pg/archive/refs/tags/v4.1.3.tar.gz -O h3-pg.tar.gz && \
|
||||||
echo "5c17f09a820859ffe949f847bebf1be98511fb8f1bd86f94932512c00479e324 h3-pg.tar.gz" | sha256sum --check && \
|
echo "5c17f09a820859ffe949f847bebf1be98511fb8f1bd86f94932512c00479e324 h3-pg.tar.gz" | sha256sum --check && \
|
||||||
mkdir h3-pg-src && cd h3-pg-src && tar xzf ../h3-pg.tar.gz --strip-components=1 -C . && \
|
mkdir h3-pg-src && cd h3-pg-src && tar xvzf ../h3-pg.tar.gz --strip-components=1 -C . && \
|
||||||
export PATH="/usr/local/pgsql/bin:$PATH" && \
|
export PATH="/usr/local/pgsql/bin:$PATH" && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
@@ -222,7 +222,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/df7cb/postgresql-unit/archive/refs/tags/7.7.tar.gz -O postgresql-unit.tar.gz && \
|
RUN wget https://github.com/df7cb/postgresql-unit/archive/refs/tags/7.7.tar.gz -O postgresql-unit.tar.gz && \
|
||||||
echo "411d05beeb97e5a4abf17572bfcfbb5a68d98d1018918feff995f6ee3bb03e79 postgresql-unit.tar.gz" | sha256sum --check && \
|
echo "411d05beeb97e5a4abf17572bfcfbb5a68d98d1018918feff995f6ee3bb03e79 postgresql-unit.tar.gz" | sha256sum --check && \
|
||||||
mkdir postgresql-unit-src && cd postgresql-unit-src && tar xzf ../postgresql-unit.tar.gz --strip-components=1 -C . && \
|
mkdir postgresql-unit-src && cd postgresql-unit-src && tar xvzf ../postgresql-unit.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
# unit extension's "create extension" script relies on absolute install path to fill some reference tables.
|
# unit extension's "create extension" script relies on absolute install path to fill some reference tables.
|
||||||
@@ -241,17 +241,11 @@ RUN wget https://github.com/df7cb/postgresql-unit/archive/refs/tags/7.7.tar.gz -
|
|||||||
FROM build-deps AS vector-pg-build
|
FROM build-deps AS vector-pg-build
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
|
||||||
COPY patches/pgvector.patch /pgvector.patch
|
RUN wget https://github.com/pgvector/pgvector/archive/refs/tags/v0.5.1.tar.gz -O pgvector.tar.gz && \
|
||||||
|
echo "cc7a8e034a96e30a819911ac79d32f6bc47bdd1aa2de4d7d4904e26b83209dc8 pgvector.tar.gz" | sha256sum --check && \
|
||||||
# By default, pgvector Makefile uses `-march=native`. We don't want that,
|
mkdir pgvector-src && cd pgvector-src && tar xvzf ../pgvector.tar.gz --strip-components=1 -C . && \
|
||||||
# because we build the images on different machines than where we run them.
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
# Pass OPTFLAGS="" to remove it.
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
RUN wget https://github.com/pgvector/pgvector/archive/refs/tags/v0.7.2.tar.gz -O pgvector.tar.gz && \
|
|
||||||
echo "617fba855c9bcb41a2a9bc78a78567fd2e147c72afd5bf9d37b31b9591632b30 pgvector.tar.gz" | sha256sum --check && \
|
|
||||||
mkdir pgvector-src && cd pgvector-src && tar xzf ../pgvector.tar.gz --strip-components=1 -C . && \
|
|
||||||
patch -p1 < /pgvector.patch && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) OPTFLAGS="" PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) OPTFLAGS="" install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/vector.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/vector.control
|
||||||
|
|
||||||
#########################################################################################
|
#########################################################################################
|
||||||
@@ -266,7 +260,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
# 9742dab1b2f297ad3811120db7b21451bca2d3c9 made on 13/11/2021
|
# 9742dab1b2f297ad3811120db7b21451bca2d3c9 made on 13/11/2021
|
||||||
RUN wget https://github.com/michelp/pgjwt/archive/9742dab1b2f297ad3811120db7b21451bca2d3c9.tar.gz -O pgjwt.tar.gz && \
|
RUN wget https://github.com/michelp/pgjwt/archive/9742dab1b2f297ad3811120db7b21451bca2d3c9.tar.gz -O pgjwt.tar.gz && \
|
||||||
echo "cfdefb15007286f67d3d45510f04a6a7a495004be5b3aecb12cda667e774203f pgjwt.tar.gz" | sha256sum --check && \
|
echo "cfdefb15007286f67d3d45510f04a6a7a495004be5b3aecb12cda667e774203f pgjwt.tar.gz" | sha256sum --check && \
|
||||||
mkdir pgjwt-src && cd pgjwt-src && tar xzf ../pgjwt.tar.gz --strip-components=1 -C . && \
|
mkdir pgjwt-src && cd pgjwt-src && tar xvzf ../pgjwt.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgjwt.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgjwt.control
|
||||||
|
|
||||||
@@ -281,7 +275,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/HypoPG/hypopg/archive/refs/tags/1.4.0.tar.gz -O hypopg.tar.gz && \
|
RUN wget https://github.com/HypoPG/hypopg/archive/refs/tags/1.4.0.tar.gz -O hypopg.tar.gz && \
|
||||||
echo "0821011743083226fc9b813c1f2ef5897a91901b57b6bea85a78e466187c6819 hypopg.tar.gz" | sha256sum --check && \
|
echo "0821011743083226fc9b813c1f2ef5897a91901b57b6bea85a78e466187c6819 hypopg.tar.gz" | sha256sum --check && \
|
||||||
mkdir hypopg-src && cd hypopg-src && tar xzf ../hypopg.tar.gz --strip-components=1 -C . && \
|
mkdir hypopg-src && cd hypopg-src && tar xvzf ../hypopg.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/hypopg.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/hypopg.control
|
||||||
@@ -297,7 +291,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/iCyberon/pg_hashids/archive/refs/tags/v1.2.1.tar.gz -O pg_hashids.tar.gz && \
|
RUN wget https://github.com/iCyberon/pg_hashids/archive/refs/tags/v1.2.1.tar.gz -O pg_hashids.tar.gz && \
|
||||||
echo "74576b992d9277c92196dd8d816baa2cc2d8046fe102f3dcd7f3c3febed6822a pg_hashids.tar.gz" | sha256sum --check && \
|
echo "74576b992d9277c92196dd8d816baa2cc2d8046fe102f3dcd7f3c3febed6822a pg_hashids.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_hashids-src && cd pg_hashids-src && tar xzf ../pg_hashids.tar.gz --strip-components=1 -C . && \
|
mkdir pg_hashids-src && cd pg_hashids-src && tar xvzf ../pg_hashids.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_hashids.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_hashids.control
|
||||||
@@ -313,7 +307,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/postgrespro/rum/archive/refs/tags/1.3.13.tar.gz -O rum.tar.gz && \
|
RUN wget https://github.com/postgrespro/rum/archive/refs/tags/1.3.13.tar.gz -O rum.tar.gz && \
|
||||||
echo "6ab370532c965568df6210bd844ac6ba649f53055e48243525b0b7e5c4d69a7d rum.tar.gz" | sha256sum --check && \
|
echo "6ab370532c965568df6210bd844ac6ba649f53055e48243525b0b7e5c4d69a7d rum.tar.gz" | sha256sum --check && \
|
||||||
mkdir rum-src && cd rum-src && tar xzf ../rum.tar.gz --strip-components=1 -C . && \
|
mkdir rum-src && cd rum-src && tar xvzf ../rum.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/rum.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/rum.control
|
||||||
@@ -329,7 +323,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/theory/pgtap/archive/refs/tags/v1.2.0.tar.gz -O pgtap.tar.gz && \
|
RUN wget https://github.com/theory/pgtap/archive/refs/tags/v1.2.0.tar.gz -O pgtap.tar.gz && \
|
||||||
echo "9c7c3de67ea41638e14f06da5da57bac6f5bd03fea05c165a0ec862205a5c052 pgtap.tar.gz" | sha256sum --check && \
|
echo "9c7c3de67ea41638e14f06da5da57bac6f5bd03fea05c165a0ec862205a5c052 pgtap.tar.gz" | sha256sum --check && \
|
||||||
mkdir pgtap-src && cd pgtap-src && tar xzf ../pgtap.tar.gz --strip-components=1 -C . && \
|
mkdir pgtap-src && cd pgtap-src && tar xvzf ../pgtap.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgtap.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgtap.control
|
||||||
@@ -345,7 +339,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/RhodiumToad/ip4r/archive/refs/tags/2.4.2.tar.gz -O ip4r.tar.gz && \
|
RUN wget https://github.com/RhodiumToad/ip4r/archive/refs/tags/2.4.2.tar.gz -O ip4r.tar.gz && \
|
||||||
echo "0f7b1f159974f49a47842a8ab6751aecca1ed1142b6d5e38d81b064b2ead1b4b ip4r.tar.gz" | sha256sum --check && \
|
echo "0f7b1f159974f49a47842a8ab6751aecca1ed1142b6d5e38d81b064b2ead1b4b ip4r.tar.gz" | sha256sum --check && \
|
||||||
mkdir ip4r-src && cd ip4r-src && tar xzf ../ip4r.tar.gz --strip-components=1 -C . && \
|
mkdir ip4r-src && cd ip4r-src && tar xvzf ../ip4r.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/ip4r.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/ip4r.control
|
||||||
@@ -361,7 +355,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/dimitri/prefix/archive/refs/tags/v1.2.10.tar.gz -O prefix.tar.gz && \
|
RUN wget https://github.com/dimitri/prefix/archive/refs/tags/v1.2.10.tar.gz -O prefix.tar.gz && \
|
||||||
echo "4342f251432a5f6fb05b8597139d3ccde8dcf87e8ca1498e7ee931ca057a8575 prefix.tar.gz" | sha256sum --check && \
|
echo "4342f251432a5f6fb05b8597139d3ccde8dcf87e8ca1498e7ee931ca057a8575 prefix.tar.gz" | sha256sum --check && \
|
||||||
mkdir prefix-src && cd prefix-src && tar xzf ../prefix.tar.gz --strip-components=1 -C . && \
|
mkdir prefix-src && cd prefix-src && tar xvzf ../prefix.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/prefix.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/prefix.control
|
||||||
@@ -377,7 +371,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/citusdata/postgresql-hll/archive/refs/tags/v2.18.tar.gz -O hll.tar.gz && \
|
RUN wget https://github.com/citusdata/postgresql-hll/archive/refs/tags/v2.18.tar.gz -O hll.tar.gz && \
|
||||||
echo "e2f55a6f4c4ab95ee4f1b4a2b73280258c5136b161fe9d059559556079694f0e hll.tar.gz" | sha256sum --check && \
|
echo "e2f55a6f4c4ab95ee4f1b4a2b73280258c5136b161fe9d059559556079694f0e hll.tar.gz" | sha256sum --check && \
|
||||||
mkdir hll-src && cd hll-src && tar xzf ../hll.tar.gz --strip-components=1 -C . && \
|
mkdir hll-src && cd hll-src && tar xvzf ../hll.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/hll.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/hll.control
|
||||||
@@ -393,7 +387,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/okbob/plpgsql_check/archive/refs/tags/v2.5.3.tar.gz -O plpgsql_check.tar.gz && \
|
RUN wget https://github.com/okbob/plpgsql_check/archive/refs/tags/v2.5.3.tar.gz -O plpgsql_check.tar.gz && \
|
||||||
echo "6631ec3e7fb3769eaaf56e3dfedb829aa761abf163d13dba354b4c218508e1c0 plpgsql_check.tar.gz" | sha256sum --check && \
|
echo "6631ec3e7fb3769eaaf56e3dfedb829aa761abf163d13dba354b4c218508e1c0 plpgsql_check.tar.gz" | sha256sum --check && \
|
||||||
mkdir plpgsql_check-src && cd plpgsql_check-src && tar xzf ../plpgsql_check.tar.gz --strip-components=1 -C . && \
|
mkdir plpgsql_check-src && cd plpgsql_check-src && tar xvzf ../plpgsql_check.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/plpgsql_check.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/plpgsql_check.control
|
||||||
@@ -424,7 +418,7 @@ RUN case "${PG_VERSION}" in \
|
|||||||
apt-get install -y cmake && \
|
apt-get install -y cmake && \
|
||||||
wget https://github.com/timescale/timescaledb/archive/refs/tags/${TIMESCALEDB_VERSION}.tar.gz -O timescaledb.tar.gz && \
|
wget https://github.com/timescale/timescaledb/archive/refs/tags/${TIMESCALEDB_VERSION}.tar.gz -O timescaledb.tar.gz && \
|
||||||
echo "${TIMESCALEDB_CHECKSUM} timescaledb.tar.gz" | sha256sum --check && \
|
echo "${TIMESCALEDB_CHECKSUM} timescaledb.tar.gz" | sha256sum --check && \
|
||||||
mkdir timescaledb-src && cd timescaledb-src && tar xzf ../timescaledb.tar.gz --strip-components=1 -C . && \
|
mkdir timescaledb-src && cd timescaledb-src && tar xvzf ../timescaledb.tar.gz --strip-components=1 -C . && \
|
||||||
./bootstrap -DSEND_TELEMETRY_DEFAULT:BOOL=OFF -DUSE_TELEMETRY:BOOL=OFF -DAPACHE_ONLY:BOOL=ON -DCMAKE_BUILD_TYPE=Release && \
|
./bootstrap -DSEND_TELEMETRY_DEFAULT:BOOL=OFF -DUSE_TELEMETRY:BOOL=OFF -DAPACHE_ONLY:BOOL=ON -DCMAKE_BUILD_TYPE=Release && \
|
||||||
cd build && \
|
cd build && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
@@ -462,7 +456,7 @@ RUN case "${PG_VERSION}" in \
|
|||||||
esac && \
|
esac && \
|
||||||
wget https://github.com/ossc-db/pg_hint_plan/archive/refs/tags/REL${PG_HINT_PLAN_VERSION}.tar.gz -O pg_hint_plan.tar.gz && \
|
wget https://github.com/ossc-db/pg_hint_plan/archive/refs/tags/REL${PG_HINT_PLAN_VERSION}.tar.gz -O pg_hint_plan.tar.gz && \
|
||||||
echo "${PG_HINT_PLAN_CHECKSUM} pg_hint_plan.tar.gz" | sha256sum --check && \
|
echo "${PG_HINT_PLAN_CHECKSUM} pg_hint_plan.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_hint_plan-src && cd pg_hint_plan-src && tar xzf ../pg_hint_plan.tar.gz --strip-components=1 -C . && \
|
mkdir pg_hint_plan-src && cd pg_hint_plan-src && tar xvzf ../pg_hint_plan.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make install -j $(getconf _NPROCESSORS_ONLN) && \
|
make install -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_hint_plan.control
|
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_hint_plan.control
|
||||||
@@ -481,7 +475,7 @@ RUN apt-get update && \
|
|||||||
apt-get install -y git libgtk2.0-dev libpq-dev libpam-dev libxslt-dev libkrb5-dev cmake && \
|
apt-get install -y git libgtk2.0-dev libpq-dev libpam-dev libxslt-dev libkrb5-dev cmake && \
|
||||||
wget https://github.com/ketteq-neon/postgres-exts/archive/e0bd1a9d9313d7120c1b9c7bb15c48c0dede4c4e.tar.gz -O kq_imcx.tar.gz && \
|
wget https://github.com/ketteq-neon/postgres-exts/archive/e0bd1a9d9313d7120c1b9c7bb15c48c0dede4c4e.tar.gz -O kq_imcx.tar.gz && \
|
||||||
echo "dc93a97ff32d152d32737ba7e196d9687041cda15e58ab31344c2f2de8855336 kq_imcx.tar.gz" | sha256sum --check && \
|
echo "dc93a97ff32d152d32737ba7e196d9687041cda15e58ab31344c2f2de8855336 kq_imcx.tar.gz" | sha256sum --check && \
|
||||||
mkdir kq_imcx-src && cd kq_imcx-src && tar xzf ../kq_imcx.tar.gz --strip-components=1 -C . && \
|
mkdir kq_imcx-src && cd kq_imcx-src && tar xvzf ../kq_imcx.tar.gz --strip-components=1 -C . && \
|
||||||
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /before.txt &&\
|
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /before.txt &&\
|
||||||
mkdir build && cd build && \
|
mkdir build && cd build && \
|
||||||
cmake -DCMAKE_BUILD_TYPE=Release .. && \
|
cmake -DCMAKE_BUILD_TYPE=Release .. && \
|
||||||
@@ -505,7 +499,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
RUN wget https://github.com/citusdata/pg_cron/archive/refs/tags/v1.6.0.tar.gz -O pg_cron.tar.gz && \
|
RUN wget https://github.com/citusdata/pg_cron/archive/refs/tags/v1.6.0.tar.gz -O pg_cron.tar.gz && \
|
||||||
echo "383a627867d730222c272bfd25cd5e151c578d73f696d32910c7db8c665cc7db pg_cron.tar.gz" | sha256sum --check && \
|
echo "383a627867d730222c272bfd25cd5e151c578d73f696d32910c7db8c665cc7db pg_cron.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_cron-src && cd pg_cron-src && tar xzf ../pg_cron.tar.gz --strip-components=1 -C . && \
|
mkdir pg_cron-src && cd pg_cron-src && tar xvzf ../pg_cron.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_cron.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_cron.control
|
||||||
@@ -531,7 +525,7 @@ RUN apt-get update && \
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:/usr/local/pgsql/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:/usr/local/pgsql/:$PATH"
|
||||||
RUN wget https://github.com/rdkit/rdkit/archive/refs/tags/Release_2023_03_3.tar.gz -O rdkit.tar.gz && \
|
RUN wget https://github.com/rdkit/rdkit/archive/refs/tags/Release_2023_03_3.tar.gz -O rdkit.tar.gz && \
|
||||||
echo "bdbf9a2e6988526bfeb8c56ce3cdfe2998d60ac289078e2215374288185e8c8d rdkit.tar.gz" | sha256sum --check && \
|
echo "bdbf9a2e6988526bfeb8c56ce3cdfe2998d60ac289078e2215374288185e8c8d rdkit.tar.gz" | sha256sum --check && \
|
||||||
mkdir rdkit-src && cd rdkit-src && tar xzf ../rdkit.tar.gz --strip-components=1 -C . && \
|
mkdir rdkit-src && cd rdkit-src && tar xvzf ../rdkit.tar.gz --strip-components=1 -C . && \
|
||||||
cmake \
|
cmake \
|
||||||
-D RDK_BUILD_CAIRO_SUPPORT=OFF \
|
-D RDK_BUILD_CAIRO_SUPPORT=OFF \
|
||||||
-D RDK_BUILD_INCHI_SUPPORT=ON \
|
-D RDK_BUILD_INCHI_SUPPORT=ON \
|
||||||
@@ -571,7 +565,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
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 && \
|
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 && \
|
echo "0d0759ab01b7fb23851ecffb0bce27822e1868a4a5819bfd276101c716637a7a pg_uuidv7.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_uuidv7-src && cd pg_uuidv7-src && tar xzf ../pg_uuidv7.tar.gz --strip-components=1 -C . && \
|
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) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_uuidv7.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_uuidv7.control
|
||||||
@@ -588,7 +582,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
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 && \
|
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 && \
|
echo "b75201efcb1c2d1b014ec4ae6a22769cc7a224e6e406a587f5784a37b6b5a2aa pg_roaringbitmap.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_roaringbitmap-src && cd pg_roaringbitmap-src && tar xzf ../pg_roaringbitmap.tar.gz --strip-components=1 -C . && \
|
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) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/roaringbitmap.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/roaringbitmap.control
|
||||||
@@ -605,7 +599,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
RUN wget https://github.com/theory/pg-semver/archive/refs/tags/v0.32.1.tar.gz -O pg_semver.tar.gz && \
|
RUN wget https://github.com/theory/pg-semver/archive/refs/tags/v0.32.1.tar.gz -O pg_semver.tar.gz && \
|
||||||
echo "fbdaf7512026d62eec03fad8687c15ed509b6ba395bff140acd63d2e4fbe25d7 pg_semver.tar.gz" | sha256sum --check && \
|
echo "fbdaf7512026d62eec03fad8687c15ed509b6ba395bff140acd63d2e4fbe25d7 pg_semver.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_semver-src && cd pg_semver-src && tar xzf ../pg_semver.tar.gz --strip-components=1 -C . && \
|
mkdir pg_semver-src && cd pg_semver-src && tar xvzf ../pg_semver.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/semver.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/semver.control
|
||||||
@@ -631,7 +625,7 @@ RUN case "${PG_VERSION}" in \
|
|||||||
esac && \
|
esac && \
|
||||||
wget https://github.com/neondatabase/pg_embedding/archive/refs/tags/${PG_EMBEDDING_VERSION}.tar.gz -O pg_embedding.tar.gz && \
|
wget https://github.com/neondatabase/pg_embedding/archive/refs/tags/${PG_EMBEDDING_VERSION}.tar.gz -O pg_embedding.tar.gz && \
|
||||||
echo "${PG_EMBEDDING_CHECKSUM} pg_embedding.tar.gz" | sha256sum --check && \
|
echo "${PG_EMBEDDING_CHECKSUM} pg_embedding.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_embedding-src && cd pg_embedding-src && tar xzf ../pg_embedding.tar.gz --strip-components=1 -C . && \
|
mkdir pg_embedding-src && cd pg_embedding-src && tar xvzf ../pg_embedding.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install
|
make -j $(getconf _NPROCESSORS_ONLN) install
|
||||||
|
|
||||||
@@ -647,7 +641,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
RUN wget https://github.com/neondatabase/postgresql_anonymizer/archive/refs/tags/neon_1.1.1.tar.gz -O pg_anon.tar.gz && \
|
RUN wget https://github.com/neondatabase/postgresql_anonymizer/archive/refs/tags/neon_1.1.1.tar.gz -O pg_anon.tar.gz && \
|
||||||
echo "321ea8d5c1648880aafde850a2c576e4a9e7b9933a34ce272efc839328999fa9 pg_anon.tar.gz" | sha256sum --check && \
|
echo "321ea8d5c1648880aafde850a2c576e4a9e7b9933a34ce272efc839328999fa9 pg_anon.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_anon-src && cd pg_anon-src && tar xzf ../pg_anon.tar.gz --strip-components=1 -C . && \
|
mkdir pg_anon-src && cd pg_anon-src && tar xvzf ../pg_anon.tar.gz --strip-components=1 -C . && \
|
||||||
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /before.txt &&\
|
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /before.txt &&\
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/anon.control && \
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/anon.control && \
|
||||||
@@ -696,7 +690,7 @@ ARG PG_VERSION
|
|||||||
|
|
||||||
RUN wget https://github.com/supabase/pg_jsonschema/archive/refs/tags/v0.2.0.tar.gz -O pg_jsonschema.tar.gz && \
|
RUN wget https://github.com/supabase/pg_jsonschema/archive/refs/tags/v0.2.0.tar.gz -O pg_jsonschema.tar.gz && \
|
||||||
echo "9118fc508a6e231e7a39acaa6f066fcd79af17a5db757b47d2eefbe14f7794f0 pg_jsonschema.tar.gz" | sha256sum --check && \
|
echo "9118fc508a6e231e7a39acaa6f066fcd79af17a5db757b47d2eefbe14f7794f0 pg_jsonschema.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_jsonschema-src && cd pg_jsonschema-src && tar xzf ../pg_jsonschema.tar.gz --strip-components=1 -C . && \
|
mkdir pg_jsonschema-src && cd pg_jsonschema-src && tar xvzf ../pg_jsonschema.tar.gz --strip-components=1 -C . && \
|
||||||
sed -i 's/pgrx = "0.10.2"/pgrx = { version = "0.10.2", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
|
sed -i 's/pgrx = "0.10.2"/pgrx = { version = "0.10.2", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
|
||||||
cargo pgrx install --release && \
|
cargo pgrx install --release && \
|
||||||
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_jsonschema.control
|
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_jsonschema.control
|
||||||
@@ -713,7 +707,7 @@ ARG PG_VERSION
|
|||||||
|
|
||||||
RUN wget https://github.com/supabase/pg_graphql/archive/refs/tags/v1.4.0.tar.gz -O pg_graphql.tar.gz && \
|
RUN wget https://github.com/supabase/pg_graphql/archive/refs/tags/v1.4.0.tar.gz -O pg_graphql.tar.gz && \
|
||||||
echo "bd8dc7230282b3efa9ae5baf053a54151ed0e66881c7c53750e2d0c765776edc pg_graphql.tar.gz" | sha256sum --check && \
|
echo "bd8dc7230282b3efa9ae5baf053a54151ed0e66881c7c53750e2d0c765776edc pg_graphql.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_graphql-src && cd pg_graphql-src && tar xzf ../pg_graphql.tar.gz --strip-components=1 -C . && \
|
mkdir pg_graphql-src && cd pg_graphql-src && tar xvzf ../pg_graphql.tar.gz --strip-components=1 -C . && \
|
||||||
sed -i 's/pgrx = "=0.10.2"/pgrx = { version = "0.10.2", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
|
sed -i 's/pgrx = "=0.10.2"/pgrx = { version = "0.10.2", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
|
||||||
cargo pgrx install --release && \
|
cargo pgrx install --release && \
|
||||||
# it's needed to enable extension because it uses untrusted C language
|
# it's needed to enable extension because it uses untrusted C language
|
||||||
@@ -733,7 +727,7 @@ ARG PG_VERSION
|
|||||||
# 26806147b17b60763039c6a6878884c41a262318 made on 26/09/2023
|
# 26806147b17b60763039c6a6878884c41a262318 made on 26/09/2023
|
||||||
RUN wget https://github.com/kelvich/pg_tiktoken/archive/26806147b17b60763039c6a6878884c41a262318.tar.gz -O pg_tiktoken.tar.gz && \
|
RUN wget https://github.com/kelvich/pg_tiktoken/archive/26806147b17b60763039c6a6878884c41a262318.tar.gz -O pg_tiktoken.tar.gz && \
|
||||||
echo "e64e55aaa38c259512d3e27c572da22c4637418cf124caba904cd50944e5004e pg_tiktoken.tar.gz" | sha256sum --check && \
|
echo "e64e55aaa38c259512d3e27c572da22c4637418cf124caba904cd50944e5004e pg_tiktoken.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_tiktoken-src && cd pg_tiktoken-src && tar xzf ../pg_tiktoken.tar.gz --strip-components=1 -C . && \
|
mkdir pg_tiktoken-src && cd pg_tiktoken-src && tar xvzf ../pg_tiktoken.tar.gz --strip-components=1 -C . && \
|
||||||
cargo pgrx install --release && \
|
cargo pgrx install --release && \
|
||||||
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_tiktoken.control
|
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_tiktoken.control
|
||||||
|
|
||||||
@@ -749,7 +743,7 @@ ARG PG_VERSION
|
|||||||
|
|
||||||
RUN wget https://github.com/pksunkara/pgx_ulid/archive/refs/tags/v0.1.3.tar.gz -O pgx_ulid.tar.gz && \
|
RUN wget https://github.com/pksunkara/pgx_ulid/archive/refs/tags/v0.1.3.tar.gz -O pgx_ulid.tar.gz && \
|
||||||
echo "ee5db82945d2d9f2d15597a80cf32de9dca67b897f605beb830561705f12683c pgx_ulid.tar.gz" | sha256sum --check && \
|
echo "ee5db82945d2d9f2d15597a80cf32de9dca67b897f605beb830561705f12683c pgx_ulid.tar.gz" | sha256sum --check && \
|
||||||
mkdir pgx_ulid-src && cd pgx_ulid-src && tar xzf ../pgx_ulid.tar.gz --strip-components=1 -C . && \
|
mkdir pgx_ulid-src && cd pgx_ulid-src && tar xvzf ../pgx_ulid.tar.gz --strip-components=1 -C . && \
|
||||||
echo "******************* Apply a patch for Postgres 16 support; delete in the next release ******************" && \
|
echo "******************* Apply a patch for Postgres 16 support; delete in the next release ******************" && \
|
||||||
wget https://github.com/pksunkara/pgx_ulid/commit/f84954cf63fc8c80d964ac970d9eceed3c791196.patch && \
|
wget https://github.com/pksunkara/pgx_ulid/commit/f84954cf63fc8c80d964ac970d9eceed3c791196.patch && \
|
||||||
patch -p1 < f84954cf63fc8c80d964ac970d9eceed3c791196.patch && \
|
patch -p1 < f84954cf63fc8c80d964ac970d9eceed3c791196.patch && \
|
||||||
@@ -771,7 +765,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
RUN wget https://github.com/eulerto/wal2json/archive/refs/tags/wal2json_2_5.tar.gz && \
|
RUN wget https://github.com/eulerto/wal2json/archive/refs/tags/wal2json_2_5.tar.gz && \
|
||||||
echo "b516653575541cf221b99cf3f8be9b6821f6dbcfc125675c85f35090f824f00e wal2json_2_5.tar.gz" | sha256sum --check && \
|
echo "b516653575541cf221b99cf3f8be9b6821f6dbcfc125675c85f35090f824f00e wal2json_2_5.tar.gz" | sha256sum --check && \
|
||||||
mkdir wal2json-src && cd wal2json-src && tar xzf ../wal2json_2_5.tar.gz --strip-components=1 -C . && \
|
mkdir wal2json-src && cd wal2json-src && tar xvzf ../wal2json_2_5.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install
|
make -j $(getconf _NPROCESSORS_ONLN) install
|
||||||
|
|
||||||
@@ -787,7 +781,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
RUN wget https://github.com/sraoss/pg_ivm/archive/refs/tags/v1.7.tar.gz -O pg_ivm.tar.gz && \
|
RUN wget https://github.com/sraoss/pg_ivm/archive/refs/tags/v1.7.tar.gz -O pg_ivm.tar.gz && \
|
||||||
echo "ebfde04f99203c7be4b0e873f91104090e2e83e5429c32ac242d00f334224d5e pg_ivm.tar.gz" | sha256sum --check && \
|
echo "ebfde04f99203c7be4b0e873f91104090e2e83e5429c32ac242d00f334224d5e pg_ivm.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_ivm-src && cd pg_ivm-src && tar xzf ../pg_ivm.tar.gz --strip-components=1 -C . && \
|
mkdir pg_ivm-src && cd pg_ivm-src && tar xvzf ../pg_ivm.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_ivm.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_ivm.control
|
||||||
@@ -804,7 +798,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
RUN wget https://github.com/pgpartman/pg_partman/archive/refs/tags/v5.0.1.tar.gz -O pg_partman.tar.gz && \
|
RUN wget https://github.com/pgpartman/pg_partman/archive/refs/tags/v5.0.1.tar.gz -O pg_partman.tar.gz && \
|
||||||
echo "75b541733a9659a6c90dbd40fccb904a630a32880a6e3044d0c4c5f4c8a65525 pg_partman.tar.gz" | sha256sum --check && \
|
echo "75b541733a9659a6c90dbd40fccb904a630a32880a6e3044d0c4c5f4c8a65525 pg_partman.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_partman-src && cd pg_partman-src && tar xzf ../pg_partman.tar.gz --strip-components=1 -C . && \
|
mkdir pg_partman-src && cd pg_partman-src && tar xvzf ../pg_partman.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_partman.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_partman.control
|
||||||
@@ -928,69 +922,6 @@ RUN rm -r /usr/local/pgsql/include
|
|||||||
# if they were to be used by other libraries.
|
# if they were to be used by other libraries.
|
||||||
RUN rm /usr/local/pgsql/lib/lib*.a
|
RUN rm /usr/local/pgsql/lib/lib*.a
|
||||||
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer neon-pg-ext-test
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
|
|
||||||
FROM neon-pg-ext-build AS neon-pg-ext-test
|
|
||||||
ARG PG_VERSION
|
|
||||||
RUN mkdir /ext-src
|
|
||||||
|
|
||||||
#COPY --from=postgis-build /postgis.tar.gz /ext-src/
|
|
||||||
#COPY --from=postgis-build /sfcgal/* /usr
|
|
||||||
COPY --from=plv8-build /plv8.tar.gz /ext-src/
|
|
||||||
COPY --from=h3-pg-build /h3-pg.tar.gz /ext-src/
|
|
||||||
COPY --from=unit-pg-build /postgresql-unit.tar.gz /ext-src/
|
|
||||||
COPY --from=vector-pg-build /pgvector.tar.gz /ext-src/
|
|
||||||
COPY --from=vector-pg-build /pgvector.patch /ext-src/
|
|
||||||
COPY --from=pgjwt-pg-build /pgjwt.tar.gz /ext-src
|
|
||||||
#COPY --from=pg-jsonschema-pg-build /home/nonroot/pg_jsonschema.tar.gz /ext-src
|
|
||||||
#COPY --from=pg-graphql-pg-build /home/nonroot/pg_graphql.tar.gz /ext-src
|
|
||||||
#COPY --from=pg-tiktoken-pg-build /home/nonroot/pg_tiktoken.tar.gz /ext-src
|
|
||||||
COPY --from=hypopg-pg-build /hypopg.tar.gz /ext-src
|
|
||||||
COPY --from=pg-hashids-pg-build /pg_hashids.tar.gz /ext-src
|
|
||||||
#COPY --from=rum-pg-build /rum.tar.gz /ext-src
|
|
||||||
#COPY --from=pgtap-pg-build /pgtap.tar.gz /ext-src
|
|
||||||
COPY --from=ip4r-pg-build /ip4r.tar.gz /ext-src
|
|
||||||
COPY --from=prefix-pg-build /prefix.tar.gz /ext-src
|
|
||||||
COPY --from=hll-pg-build /hll.tar.gz /ext-src
|
|
||||||
COPY --from=plpgsql-check-pg-build /plpgsql_check.tar.gz /ext-src
|
|
||||||
#COPY --from=timescaledb-pg-build /timescaledb.tar.gz /ext-src
|
|
||||||
COPY --from=pg-hint-plan-pg-build /pg_hint_plan.tar.gz /ext-src
|
|
||||||
COPY patches/pg_hintplan.patch /ext-src
|
|
||||||
#COPY --from=kq-imcx-pg-build /kq_imcx.tar.gz /ext-src
|
|
||||||
COPY --from=pg-cron-pg-build /pg_cron.tar.gz /ext-src
|
|
||||||
COPY patches/pg_cron.patch /ext-src
|
|
||||||
#COPY --from=pg-pgx-ulid-build /home/nonroot/pgx_ulid.tar.gz /ext-src
|
|
||||||
COPY --from=rdkit-pg-build /rdkit.tar.gz /ext-src
|
|
||||||
COPY --from=pg-uuidv7-pg-build /pg_uuidv7.tar.gz /ext-src
|
|
||||||
COPY --from=pg-roaringbitmap-pg-build /pg_roaringbitmap.tar.gz /ext-src
|
|
||||||
COPY --from=pg-semver-pg-build /pg_semver.tar.gz /ext-src
|
|
||||||
#COPY --from=pg-embedding-pg-build /home/nonroot/pg_embedding-src/ /ext-src
|
|
||||||
#COPY --from=wal2json-pg-build /wal2json_2_5.tar.gz /ext-src
|
|
||||||
COPY --from=pg-anon-pg-build /pg_anon.tar.gz /ext-src
|
|
||||||
COPY patches/pg_anon.patch /ext-src
|
|
||||||
COPY --from=pg-ivm-build /pg_ivm.tar.gz /ext-src
|
|
||||||
COPY --from=pg-partman-build /pg_partman.tar.gz /ext-src
|
|
||||||
RUN cd /ext-src/ && for f in *.tar.gz; \
|
|
||||||
do echo $f; dname=$(echo $f | sed 's/\.tar.*//')-src; \
|
|
||||||
rm -rf $dname; mkdir $dname; tar xzf $f --strip-components=1 -C $dname \
|
|
||||||
|| exit 1; rm -f $f; done
|
|
||||||
RUN cd /ext-src/pgvector-src && patch -p1 <../pgvector.patch
|
|
||||||
# cmake is required for the h3 test
|
|
||||||
RUN apt-get update && apt-get install -y cmake
|
|
||||||
RUN patch -p1 < /ext-src/pg_hintplan.patch
|
|
||||||
COPY --chmod=755 docker-compose/run-tests.sh /run-tests.sh
|
|
||||||
RUN patch -p1 </ext-src/pg_anon.patch
|
|
||||||
RUN patch -p1 </ext-src/pg_cron.patch
|
|
||||||
ENV PATH=/usr/local/pgsql/bin:$PATH
|
|
||||||
ENV PGHOST=compute
|
|
||||||
ENV PGPORT=55433
|
|
||||||
ENV PGUSER=cloud_admin
|
|
||||||
ENV PGDATABASE=postgres
|
|
||||||
#########################################################################################
|
#########################################################################################
|
||||||
#
|
#
|
||||||
# Final layer
|
# Final layer
|
||||||
@@ -1013,9 +944,6 @@ RUN mkdir /var/db && useradd -m -d /var/db/postgres postgres && \
|
|||||||
COPY --from=postgres-cleanup-layer --chown=postgres /usr/local/pgsql /usr/local
|
COPY --from=postgres-cleanup-layer --chown=postgres /usr/local/pgsql /usr/local
|
||||||
COPY --from=compute-tools --chown=postgres /home/nonroot/target/release-line-debug-size-lto/compute_ctl /usr/local/bin/compute_ctl
|
COPY --from=compute-tools --chown=postgres /home/nonroot/target/release-line-debug-size-lto/compute_ctl /usr/local/bin/compute_ctl
|
||||||
|
|
||||||
# Create remote extension download directory
|
|
||||||
RUN mkdir /usr/local/download_extensions && chown -R postgres:postgres /usr/local/download_extensions
|
|
||||||
|
|
||||||
# Install:
|
# Install:
|
||||||
# libreadline8 for psql
|
# libreadline8 for psql
|
||||||
# libicu67, locales for collations (including ICU and plpgsql_check)
|
# libicu67, locales for collations (including ICU and plpgsql_check)
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ ROOT_PROJECT_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
|||||||
# Where to install Postgres, default is ./pg_install, maybe useful for package managers
|
# Where to install Postgres, default is ./pg_install, maybe useful for package managers
|
||||||
POSTGRES_INSTALL_DIR ?= $(ROOT_PROJECT_DIR)/pg_install/
|
POSTGRES_INSTALL_DIR ?= $(ROOT_PROJECT_DIR)/pg_install/
|
||||||
|
|
||||||
OPENSSL_PREFIX_DIR := /usr/local/openssl
|
|
||||||
ICU_PREFIX_DIR := /usr/local/icu
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# We differentiate between release / debug build types using the BUILD_TYPE
|
# We differentiate between release / debug build types using the BUILD_TYPE
|
||||||
# environment variable.
|
# environment variable.
|
||||||
@@ -23,31 +20,19 @@ else
|
|||||||
$(error Bad build type '$(BUILD_TYPE)', see Makefile for options)
|
$(error Bad build type '$(BUILD_TYPE)', see Makefile for options)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(shell test -e /home/nonroot/.docker_build && echo -n yes),yes)
|
|
||||||
# Exclude static build openssl, icu for local build (MacOS, Linux)
|
|
||||||
# Only keep for build type release and debug
|
|
||||||
PG_CFLAGS += -I$(OPENSSL_PREFIX_DIR)/include
|
|
||||||
PG_CONFIGURE_OPTS += --with-icu
|
|
||||||
PG_CONFIGURE_OPTS += ICU_CFLAGS='-I/$(ICU_PREFIX_DIR)/include -DU_STATIC_IMPLEMENTATION'
|
|
||||||
PG_CONFIGURE_OPTS += ICU_LIBS='-L$(ICU_PREFIX_DIR)/lib -L$(ICU_PREFIX_DIR)/lib64 -licui18n -licuuc -licudata -lstdc++ -Wl,-Bdynamic -lm'
|
|
||||||
PG_CONFIGURE_OPTS += LDFLAGS='-L$(OPENSSL_PREFIX_DIR)/lib -L$(OPENSSL_PREFIX_DIR)/lib64 -L$(ICU_PREFIX_DIR)/lib -L$(ICU_PREFIX_DIR)/lib64 -Wl,-Bstatic -lssl -lcrypto -Wl,-Bdynamic -lrt -lm -ldl -lpthread'
|
|
||||||
endif
|
|
||||||
|
|
||||||
UNAME_S := $(shell uname -s)
|
UNAME_S := $(shell uname -s)
|
||||||
ifeq ($(UNAME_S),Linux)
|
ifeq ($(UNAME_S),Linux)
|
||||||
# Seccomp BPF is only available for Linux
|
# Seccomp BPF is only available for Linux
|
||||||
PG_CONFIGURE_OPTS += --with-libseccomp
|
PG_CONFIGURE_OPTS += --with-libseccomp
|
||||||
else ifeq ($(UNAME_S),Darwin)
|
else ifeq ($(UNAME_S),Darwin)
|
||||||
ifndef DISABLE_HOMEBREW
|
# macOS with brew-installed openssl requires explicit paths
|
||||||
# macOS with brew-installed openssl requires explicit paths
|
# It can be configured with OPENSSL_PREFIX variable
|
||||||
# It can be configured with OPENSSL_PREFIX variable
|
OPENSSL_PREFIX ?= $(shell brew --prefix openssl@3)
|
||||||
OPENSSL_PREFIX := $(shell brew --prefix openssl@3)
|
PG_CONFIGURE_OPTS += --with-includes=$(OPENSSL_PREFIX)/include --with-libraries=$(OPENSSL_PREFIX)/lib
|
||||||
PG_CONFIGURE_OPTS += --with-includes=$(OPENSSL_PREFIX)/include --with-libraries=$(OPENSSL_PREFIX)/lib
|
PG_CONFIGURE_OPTS += PKG_CONFIG_PATH=$(shell brew --prefix icu4c)/lib/pkgconfig
|
||||||
PG_CONFIGURE_OPTS += PKG_CONFIG_PATH=$(shell brew --prefix icu4c)/lib/pkgconfig
|
# macOS already has bison and flex in the system, but they are old and result in postgres-v14 target failure
|
||||||
# macOS already has bison and flex in the system, but they are old and result in postgres-v14 target failure
|
# brew formulae are keg-only and not symlinked into HOMEBREW_PREFIX, force their usage
|
||||||
# brew formulae are keg-only and not symlinked into HOMEBREW_PREFIX, force their usage
|
EXTRA_PATH_OVERRIDES += $(shell brew --prefix bison)/bin/:$(shell brew --prefix flex)/bin/:
|
||||||
EXTRA_PATH_OVERRIDES += $(shell brew --prefix bison)/bin/:$(shell brew --prefix flex)/bin/:
|
|
||||||
endif
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Use -C option so that when PostgreSQL "make install" installs the
|
# Use -C option so that when PostgreSQL "make install" installs the
|
||||||
@@ -94,14 +79,11 @@ $(POSTGRES_INSTALL_DIR)/build/%/config.status:
|
|||||||
echo "'git submodule update --init --recursive --depth 2 --progress .' in project root.\n"; \
|
echo "'git submodule update --init --recursive --depth 2 --progress .' in project root.\n"; \
|
||||||
exit 1; }
|
exit 1; }
|
||||||
mkdir -p $(POSTGRES_INSTALL_DIR)/build/$*
|
mkdir -p $(POSTGRES_INSTALL_DIR)/build/$*
|
||||||
|
(cd $(POSTGRES_INSTALL_DIR)/build/$* && \
|
||||||
VERSION=$*; \
|
env PATH="$(EXTRA_PATH_OVERRIDES):$$PATH" $(ROOT_PROJECT_DIR)/vendor/postgres-$*/configure \
|
||||||
EXTRA_VERSION=$$(cd $(ROOT_PROJECT_DIR)/vendor/postgres-$$VERSION && git rev-parse HEAD); \
|
|
||||||
(cd $(POSTGRES_INSTALL_DIR)/build/$$VERSION && \
|
|
||||||
env PATH="$(EXTRA_PATH_OVERRIDES):$$PATH" $(ROOT_PROJECT_DIR)/vendor/postgres-$$VERSION/configure \
|
|
||||||
CFLAGS='$(PG_CFLAGS)' \
|
CFLAGS='$(PG_CFLAGS)' \
|
||||||
$(PG_CONFIGURE_OPTS) --with-extra-version=" ($$EXTRA_VERSION)" \
|
$(PG_CONFIGURE_OPTS) \
|
||||||
--prefix=$(abspath $(POSTGRES_INSTALL_DIR))/$$VERSION > configure.log)
|
--prefix=$(abspath $(POSTGRES_INSTALL_DIR))/$* > configure.log)
|
||||||
|
|
||||||
# nicer alias to run 'configure'
|
# nicer alias to run 'configure'
|
||||||
# Note: I've been unable to use templates for this part of our configuration.
|
# Note: I've been unable to use templates for this part of our configuration.
|
||||||
@@ -137,8 +119,6 @@ postgres-%: postgres-configure-% \
|
|||||||
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/pageinspect install
|
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/pageinspect install
|
||||||
+@echo "Compiling amcheck $*"
|
+@echo "Compiling amcheck $*"
|
||||||
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/amcheck install
|
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/amcheck install
|
||||||
+@echo "Compiling test_decoding $*"
|
|
||||||
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/test_decoding install
|
|
||||||
|
|
||||||
.PHONY: postgres-clean-%
|
.PHONY: postgres-clean-%
|
||||||
postgres-clean-%:
|
postgres-clean-%:
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
[](https://neon.tech)
|
[](https://neon.tech)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Neon
|
# Neon
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,10 @@ reqwest = { workspace = true, features = ["json"] }
|
|||||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
||||||
tokio-postgres.workspace = true
|
tokio-postgres.workspace = true
|
||||||
tokio-util.workspace = true
|
tokio-util.workspace = true
|
||||||
tokio-stream.workspace = true
|
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-opentelemetry.workspace = true
|
tracing-opentelemetry.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
tracing-utils.workspace = true
|
tracing-utils.workspace = true
|
||||||
thiserror.workspace = true
|
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
|
||||||
compute_api.workspace = true
|
compute_api.workspace = true
|
||||||
|
|||||||
@@ -47,11 +47,10 @@ use chrono::Utc;
|
|||||||
use clap::Arg;
|
use clap::Arg;
|
||||||
use signal_hook::consts::{SIGQUIT, SIGTERM};
|
use signal_hook::consts::{SIGQUIT, SIGTERM};
|
||||||
use signal_hook::{consts::SIGINT, iterator::Signals};
|
use signal_hook::{consts::SIGINT, iterator::Signals};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use compute_api::responses::ComputeStatus;
|
use compute_api::responses::ComputeStatus;
|
||||||
use compute_api::spec::ComputeSpec;
|
|
||||||
|
|
||||||
use compute_tools::compute::{
|
use compute_tools::compute::{
|
||||||
forward_termination_signal, ComputeNode, ComputeState, ParsedSpec, PG_PID,
|
forward_termination_signal, ComputeNode, ComputeState, ParsedSpec, PG_PID,
|
||||||
@@ -63,41 +62,12 @@ use compute_tools::logger::*;
|
|||||||
use compute_tools::monitor::launch_monitor;
|
use compute_tools::monitor::launch_monitor;
|
||||||
use compute_tools::params::*;
|
use compute_tools::params::*;
|
||||||
use compute_tools::spec::*;
|
use compute_tools::spec::*;
|
||||||
use compute_tools::swap::resize_swap;
|
|
||||||
|
|
||||||
// this is an arbitrary build tag. Fine as a default / for testing purposes
|
// this is an arbitrary build tag. Fine as a default / for testing purposes
|
||||||
// in-case of not-set environment var
|
// in-case of not-set environment var
|
||||||
const BUILD_TAG_DEFAULT: &str = "latest";
|
const BUILD_TAG_DEFAULT: &str = "latest";
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let (build_tag, clap_args) = init()?;
|
|
||||||
|
|
||||||
let (pg_handle, start_pg_result) = {
|
|
||||||
// Enter startup tracing context
|
|
||||||
let _startup_context_guard = startup_context_from_env();
|
|
||||||
|
|
||||||
let cli_args = process_cli(&clap_args)?;
|
|
||||||
|
|
||||||
let cli_spec = try_spec_from_cli(&clap_args, &cli_args)?;
|
|
||||||
|
|
||||||
let wait_spec_result = wait_spec(build_tag, cli_args, cli_spec)?;
|
|
||||||
|
|
||||||
start_postgres(&clap_args, wait_spec_result)?
|
|
||||||
|
|
||||||
// Startup is finished, exit the startup tracing span
|
|
||||||
};
|
|
||||||
|
|
||||||
// PostgreSQL is now running, if startup was successful. Wait until it exits.
|
|
||||||
let wait_pg_result = wait_postgres(pg_handle)?;
|
|
||||||
|
|
||||||
let delay_exit = cleanup_after_postgres_exit(start_pg_result)?;
|
|
||||||
|
|
||||||
maybe_delay_exit(delay_exit);
|
|
||||||
|
|
||||||
deinit_and_exit(wait_pg_result);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init() -> Result<(String, clap::ArgMatches)> {
|
|
||||||
init_tracing_and_logging(DEFAULT_LOG_LEVEL)?;
|
init_tracing_and_logging(DEFAULT_LOG_LEVEL)?;
|
||||||
|
|
||||||
let mut signals = Signals::new([SIGINT, SIGTERM, SIGQUIT])?;
|
let mut signals = Signals::new([SIGINT, SIGTERM, SIGQUIT])?;
|
||||||
@@ -112,15 +82,9 @@ fn init() -> Result<(String, clap::ArgMatches)> {
|
|||||||
.to_string();
|
.to_string();
|
||||||
info!("build_tag: {build_tag}");
|
info!("build_tag: {build_tag}");
|
||||||
|
|
||||||
Ok((build_tag, 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);
|
||||||
fn process_cli(matches: &clap::ArgMatches) -> Result<ProcessCliResult> {
|
|
||||||
let pgbin_default = "postgres";
|
|
||||||
let pgbin = matches
|
|
||||||
.get_one::<String>("pgbin")
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(pgbin_default);
|
|
||||||
|
|
||||||
let ext_remote_storage = matches
|
let ext_remote_storage = matches
|
||||||
.get_one::<String>("remote-ext-config")
|
.get_one::<String>("remote-ext-config")
|
||||||
@@ -146,32 +110,7 @@ fn process_cli(matches: &clap::ArgMatches) -> Result<ProcessCliResult> {
|
|||||||
.expect("Postgres connection string is required");
|
.expect("Postgres connection string is required");
|
||||||
let spec_json = matches.get_one::<String>("spec");
|
let spec_json = matches.get_one::<String>("spec");
|
||||||
let spec_path = matches.get_one::<String>("spec-path");
|
let spec_path = matches.get_one::<String>("spec-path");
|
||||||
let resize_swap_on_bind = matches.get_flag("resize-swap-on-bind");
|
|
||||||
|
|
||||||
Ok(ProcessCliResult {
|
|
||||||
connstr,
|
|
||||||
pgdata,
|
|
||||||
pgbin,
|
|
||||||
ext_remote_storage,
|
|
||||||
http_port,
|
|
||||||
spec_json,
|
|
||||||
spec_path,
|
|
||||||
resize_swap_on_bind,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ProcessCliResult<'clap> {
|
|
||||||
connstr: &'clap str,
|
|
||||||
pgdata: &'clap str,
|
|
||||||
pgbin: &'clap str,
|
|
||||||
ext_remote_storage: Option<&'clap str>,
|
|
||||||
http_port: u16,
|
|
||||||
spec_json: Option<&'clap String>,
|
|
||||||
spec_path: Option<&'clap String>,
|
|
||||||
resize_swap_on_bind: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn startup_context_from_env() -> Option<opentelemetry::ContextGuard> {
|
|
||||||
// Extract OpenTelemetry context for the startup actions from the
|
// Extract OpenTelemetry context for the startup actions from the
|
||||||
// TRACEPARENT and TRACESTATE env variables, and attach it to the current
|
// TRACEPARENT and TRACESTATE env variables, and attach it to the current
|
||||||
// tracing context.
|
// tracing context.
|
||||||
@@ -208,7 +147,7 @@ fn startup_context_from_env() -> Option<opentelemetry::ContextGuard> {
|
|||||||
if let Ok(val) = std::env::var("TRACESTATE") {
|
if let Ok(val) = std::env::var("TRACESTATE") {
|
||||||
startup_tracing_carrier.insert("tracestate".to_string(), val);
|
startup_tracing_carrier.insert("tracestate".to_string(), val);
|
||||||
}
|
}
|
||||||
if !startup_tracing_carrier.is_empty() {
|
let startup_context_guard = if !startup_tracing_carrier.is_empty() {
|
||||||
use opentelemetry::propagation::TextMapPropagator;
|
use opentelemetry::propagation::TextMapPropagator;
|
||||||
use opentelemetry::sdk::propagation::TraceContextPropagator;
|
use opentelemetry::sdk::propagation::TraceContextPropagator;
|
||||||
let guard = TraceContextPropagator::new()
|
let guard = TraceContextPropagator::new()
|
||||||
@@ -218,17 +157,8 @@ fn startup_context_from_env() -> Option<opentelemetry::ContextGuard> {
|
|||||||
Some(guard)
|
Some(guard)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
fn try_spec_from_cli(
|
|
||||||
matches: &clap::ArgMatches,
|
|
||||||
ProcessCliResult {
|
|
||||||
spec_json,
|
|
||||||
spec_path,
|
|
||||||
..
|
|
||||||
}: &ProcessCliResult,
|
|
||||||
) -> Result<CliSpecParams> {
|
|
||||||
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");
|
||||||
|
|
||||||
@@ -269,34 +199,6 @@ fn try_spec_from_cli(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(CliSpecParams {
|
|
||||||
spec,
|
|
||||||
live_config_allowed,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CliSpecParams {
|
|
||||||
/// If a spec was provided via CLI or file, the [`ComputeSpec`]
|
|
||||||
spec: Option<ComputeSpec>,
|
|
||||||
live_config_allowed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wait_spec(
|
|
||||||
build_tag: String,
|
|
||||||
ProcessCliResult {
|
|
||||||
connstr,
|
|
||||||
pgdata,
|
|
||||||
pgbin,
|
|
||||||
ext_remote_storage,
|
|
||||||
resize_swap_on_bind,
|
|
||||||
http_port,
|
|
||||||
..
|
|
||||||
}: ProcessCliResult,
|
|
||||||
CliSpecParams {
|
|
||||||
spec,
|
|
||||||
live_config_allowed,
|
|
||||||
}: CliSpecParams,
|
|
||||||
) -> Result<WaitSpecResult> {
|
|
||||||
let mut new_state = ComputeState::new();
|
let mut new_state = ComputeState::new();
|
||||||
let spec_set;
|
let spec_set;
|
||||||
|
|
||||||
@@ -324,17 +226,19 @@ fn wait_spec(
|
|||||||
|
|
||||||
// If this is a pooled VM, prewarm before starting HTTP server and becoming
|
// If this is a pooled VM, prewarm before starting HTTP server and becoming
|
||||||
// available for binding. Prewarming helps Postgres start quicker later,
|
// available for binding. Prewarming helps Postgres start quicker later,
|
||||||
// because QEMU will already have its memory allocated from the host, and
|
// because QEMU will already have it's memory allocated from the host, and
|
||||||
// the necessary binaries will already be cached.
|
// the necessary binaries will already be cached.
|
||||||
if !spec_set {
|
if !spec_set {
|
||||||
compute.prewarm_postgres()?;
|
compute.prewarm_postgres()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch http service first, so that we can serve control-plane requests
|
// Launch http service first, so we were able to serve control-plane
|
||||||
// while configuration is still in progress.
|
// requests, while configuration is still in progress.
|
||||||
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");
|
||||||
@@ -349,45 +253,21 @@ fn wait_spec(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record for how long we slept waiting for the spec.
|
|
||||||
let now = Utc::now();
|
|
||||||
state.metrics.wait_for_spec_ms = now
|
|
||||||
.signed_duration_since(state.start_time)
|
|
||||||
.to_std()
|
|
||||||
.unwrap()
|
|
||||||
.as_millis() as u64;
|
|
||||||
|
|
||||||
// Reset start time, so that the total startup time that is calculated later will
|
|
||||||
// not include the time that we waited for the spec.
|
|
||||||
state.start_time = now;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(WaitSpecResult {
|
|
||||||
compute,
|
|
||||||
http_port,
|
|
||||||
resize_swap_on_bind,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WaitSpecResult {
|
|
||||||
compute: Arc<ComputeNode>,
|
|
||||||
// passed through from ProcessCliResult
|
|
||||||
http_port: u16,
|
|
||||||
resize_swap_on_bind: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_postgres(
|
|
||||||
// need to allow unused because `matches` is only used if target_os = "linux"
|
|
||||||
#[allow(unused_variables)] matches: &clap::ArgMatches,
|
|
||||||
WaitSpecResult {
|
|
||||||
compute,
|
|
||||||
http_port,
|
|
||||||
resize_swap_on_bind,
|
|
||||||
}: WaitSpecResult,
|
|
||||||
) -> Result<(Option<PostgresHandle>, StartPostgresResult)> {
|
|
||||||
// We got all we need, update the state.
|
// We got all we need, update the state.
|
||||||
let mut state = compute.state.lock().unwrap();
|
let mut state = compute.state.lock().unwrap();
|
||||||
|
|
||||||
|
// Record for how long we slept waiting for the spec.
|
||||||
|
state.metrics.wait_for_spec_ms = Utc::now()
|
||||||
|
.signed_duration_since(state.start_time)
|
||||||
|
.to_std()
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as u64;
|
||||||
|
// Reset start time to the actual start of the configuration, so that
|
||||||
|
// total startup time was properly measured at the end.
|
||||||
|
state.start_time = Utc::now();
|
||||||
|
|
||||||
state.status = ComputeStatus::Init;
|
state.status = ComputeStatus::Init;
|
||||||
compute.state_changed.notify_all();
|
compute.state_changed.notify_all();
|
||||||
|
|
||||||
@@ -395,72 +275,33 @@ fn start_postgres(
|
|||||||
"running compute with features: {:?}",
|
"running compute with features: {:?}",
|
||||||
state.pspec.as_ref().unwrap().spec.features
|
state.pspec.as_ref().unwrap().spec.features
|
||||||
);
|
);
|
||||||
// before we release the mutex, fetch the swap size (if any) for later.
|
|
||||||
let swap_size_bytes = state.pspec.as_ref().unwrap().spec.swap_size_bytes;
|
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
// Launch remaining service threads
|
// Launch remaining service threads
|
||||||
let _monitor_handle = launch_monitor(&compute);
|
let _monitor_handle = launch_monitor(&compute);
|
||||||
let _configurator_handle = launch_configurator(&compute);
|
let _configurator_handle = launch_configurator(&compute);
|
||||||
|
|
||||||
let mut prestartup_failed = false;
|
|
||||||
let mut delay_exit = false;
|
|
||||||
|
|
||||||
// Resize swap to the desired size if the compute spec says so
|
|
||||||
if let (Some(size_bytes), true) = (swap_size_bytes, resize_swap_on_bind) {
|
|
||||||
// To avoid 'swapoff' hitting postgres startup, we need to run resize-swap to completion
|
|
||||||
// *before* starting postgres.
|
|
||||||
//
|
|
||||||
// In theory, we could do this asynchronously if SkipSwapon was enabled for VMs, but this
|
|
||||||
// carries a risk of introducing hard-to-debug issues - e.g. if postgres sometimes gets
|
|
||||||
// OOM-killed during startup because swap wasn't available yet.
|
|
||||||
match resize_swap(size_bytes) {
|
|
||||||
Ok(()) => {
|
|
||||||
let size_gib = size_bytes as f32 / (1 << 20) as f32; // just for more coherent display.
|
|
||||||
info!(%size_bytes, %size_gib, "resized swap");
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let err = err.context("failed to resize swap");
|
|
||||||
error!("{err:#}");
|
|
||||||
|
|
||||||
// Mark compute startup as failed; don't try to start postgres, and report this
|
|
||||||
// error to the control plane when it next asks.
|
|
||||||
prestartup_failed = true;
|
|
||||||
let mut state = compute.state.lock().unwrap();
|
|
||||||
state.error = Some(format!("{err:?}"));
|
|
||||||
state.status = ComputeStatus::Failed;
|
|
||||||
compute.state_changed.notify_all();
|
|
||||||
delay_exit = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let extension_server_port: u16 = http_port;
|
|
||||||
|
|
||||||
// Start Postgres
|
// Start Postgres
|
||||||
let mut pg = None;
|
let mut delay_exit = false;
|
||||||
if !prestartup_failed {
|
let mut exit_code = None;
|
||||||
pg = match compute.start_compute(extension_server_port) {
|
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);
|
||||||
let mut state = compute.state.lock().unwrap();
|
let mut state = compute.state.lock().unwrap();
|
||||||
state.error = Some(format!("{:?}", err));
|
state.error = Some(format!("{:?}", err));
|
||||||
state.status = ComputeStatus::Failed;
|
state.status = ComputeStatus::Failed;
|
||||||
// Notify others that Postgres failed to start. In case of configuring the
|
// Notify others that Postgres failed to start. In case of configuring the
|
||||||
// empty compute, it's likely that API handler is still waiting for compute
|
// empty compute, it's likely that API handler is still waiting for compute
|
||||||
// state change. With this we will notify it that compute is in Failed state,
|
// state change. With this we will notify it that compute is in Failed state,
|
||||||
// so control plane will know about it earlier and record proper error instead
|
// so control plane will know about it earlier and record proper error instead
|
||||||
// of timeout.
|
// of timeout.
|
||||||
compute.state_changed.notify_all();
|
compute.state_changed.notify_all();
|
||||||
drop(state); // unlock
|
drop(state); // unlock
|
||||||
delay_exit = true;
|
delay_exit = true;
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
warn!("skipping postgres startup because pre-startup step failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the vm-monitor if directed to. The vm-monitor only runs on linux
|
// Start the vm-monitor if directed to. The vm-monitor only runs on linux
|
||||||
// because it requires cgroups.
|
// because it requires cgroups.
|
||||||
@@ -493,7 +334,7 @@ fn start_postgres(
|
|||||||
// This token is used internally by the monitor to clean up all threads
|
// This token is used internally by the monitor to clean up all threads
|
||||||
let token = CancellationToken::new();
|
let token = CancellationToken::new();
|
||||||
|
|
||||||
let vm_monitor = rt.as_ref().map(|rt| {
|
let vm_monitor = &rt.as_ref().map(|rt| {
|
||||||
rt.spawn(vm_monitor::start(
|
rt.spawn(vm_monitor::start(
|
||||||
Box::leak(Box::new(vm_monitor::Args {
|
Box::leak(Box::new(vm_monitor::Args {
|
||||||
cgroup: cgroup.cloned(),
|
cgroup: cgroup.cloned(),
|
||||||
@@ -506,41 +347,12 @@ fn start_postgres(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((
|
|
||||||
pg,
|
|
||||||
StartPostgresResult {
|
|
||||||
delay_exit,
|
|
||||||
compute,
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
rt,
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
token,
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
vm_monitor,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostgresHandle = (std::process::Child, std::thread::JoinHandle<()>);
|
|
||||||
|
|
||||||
struct StartPostgresResult {
|
|
||||||
delay_exit: bool,
|
|
||||||
// passed through from WaitSpecResult
|
|
||||||
compute: Arc<ComputeNode>,
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
rt: Option<tokio::runtime::Runtime>,
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
token: tokio_util::sync::CancellationToken,
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
vm_monitor: Option<tokio::task::JoinHandle<Result<()>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wait_postgres(pg: Option<PostgresHandle>) -> Result<WaitPostgresResult> {
|
|
||||||
// Wait for the child Postgres process forever. In this state Ctrl+C will
|
// Wait for the child Postgres process forever. In this state Ctrl+C will
|
||||||
// propagate to Postgres and it will be shut down as well.
|
// propagate to Postgres and it will be shut down as well.
|
||||||
let mut exit_code = None;
|
|
||||||
if let Some((mut pg, logs_handle)) = pg {
|
if let Some((mut pg, logs_handle)) = pg {
|
||||||
|
// Startup is finished, exit the startup tracing span
|
||||||
|
drop(startup_context_guard);
|
||||||
|
|
||||||
let ecode = pg
|
let ecode = pg
|
||||||
.wait()
|
.wait()
|
||||||
.expect("failed to start waiting on Postgres process");
|
.expect("failed to start waiting on Postgres process");
|
||||||
@@ -555,25 +367,6 @@ fn wait_postgres(pg: Option<PostgresHandle>) -> Result<WaitPostgresResult> {
|
|||||||
exit_code = ecode.code()
|
exit_code = ecode.code()
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(WaitPostgresResult { exit_code })
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WaitPostgresResult {
|
|
||||||
exit_code: Option<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cleanup_after_postgres_exit(
|
|
||||||
StartPostgresResult {
|
|
||||||
mut delay_exit,
|
|
||||||
compute,
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
vm_monitor,
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
token,
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
rt,
|
|
||||||
}: StartPostgresResult,
|
|
||||||
) -> Result<bool> {
|
|
||||||
// Terminate the vm_monitor so it releases the file watcher on
|
// Terminate the vm_monitor so it releases the file watcher on
|
||||||
// /sys/fs/cgroup/neon-postgres.
|
// /sys/fs/cgroup/neon-postgres.
|
||||||
// Note: the vm-monitor only runs on linux because it requires cgroups.
|
// Note: the vm-monitor only runs on linux because it requires cgroups.
|
||||||
@@ -615,19 +408,13 @@ fn cleanup_after_postgres_exit(
|
|||||||
error!("error while checking for core dumps: {err:?}");
|
error!("error while checking for core dumps: {err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(delay_exit)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn maybe_delay_exit(delay_exit: bool) {
|
|
||||||
// If launch failed, keep serving HTTP requests for a while, so the cloud
|
// If launch failed, keep serving HTTP requests for a while, so the cloud
|
||||||
// control plane can get the actual error.
|
// control plane can get the actual error.
|
||||||
if delay_exit {
|
if delay_exit {
|
||||||
info!("giving control plane 30s to collect the error before shutdown");
|
info!("giving control plane 30s to collect the error before shutdown");
|
||||||
thread::sleep(Duration::from_secs(30));
|
thread::sleep(Duration::from_secs(30));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit_and_exit(WaitPostgresResult { exit_code }: WaitPostgresResult) -> ! {
|
|
||||||
// Shutdown trace pipeline gracefully, so that it has a chance to send any
|
// Shutdown trace pipeline gracefully, so that it has a chance to send any
|
||||||
// pending traces before we exit. Shutting down OTEL tracing provider may
|
// pending traces before we exit. Shutting down OTEL tracing provider may
|
||||||
// hang for quite some time, see, for example:
|
// hang for quite some time, see, for example:
|
||||||
@@ -735,15 +522,10 @@ fn cli() -> clap::Command {
|
|||||||
Arg::new("filecache-connstr")
|
Arg::new("filecache-connstr")
|
||||||
.long("filecache-connstr")
|
.long("filecache-connstr")
|
||||||
.default_value(
|
.default_value(
|
||||||
"host=localhost port=5432 dbname=postgres user=cloud_admin sslmode=disable application_name=vm-monitor",
|
"host=localhost port=5432 dbname=postgres user=cloud_admin sslmode=disable",
|
||||||
)
|
)
|
||||||
.value_name("FILECACHE_CONNSTR"),
|
.value_name("FILECACHE_CONNSTR"),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::new("resize-swap-on-bind")
|
|
||||||
.long("resize-swap-on-bind")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When compute_ctl is killed, send also termination signal to sync-safekeepers
|
/// When compute_ctl is killed, send also termination signal to sync-safekeepers
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
use compute_api::{
|
|
||||||
responses::CatalogObjects,
|
|
||||||
spec::{Database, Role},
|
|
||||||
};
|
|
||||||
use futures::Stream;
|
|
||||||
use postgres::{Client, NoTls};
|
|
||||||
use std::{path::Path, process::Stdio, result::Result, sync::Arc};
|
|
||||||
use tokio::{
|
|
||||||
io::{AsyncBufReadExt, BufReader},
|
|
||||||
process::Command,
|
|
||||||
task,
|
|
||||||
};
|
|
||||||
use tokio_stream::{self as stream, StreamExt};
|
|
||||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
compute::ComputeNode,
|
|
||||||
pg_helpers::{get_existing_dbs, get_existing_roles},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn get_dbs_and_roles(compute: &Arc<ComputeNode>) -> anyhow::Result<CatalogObjects> {
|
|
||||||
let connstr = compute.connstr.clone();
|
|
||||||
task::spawn_blocking(move || {
|
|
||||||
let mut client = Client::connect(connstr.as_str(), NoTls)?;
|
|
||||||
let roles: Vec<Role>;
|
|
||||||
{
|
|
||||||
let mut xact = client.transaction()?;
|
|
||||||
roles = get_existing_roles(&mut xact)?;
|
|
||||||
}
|
|
||||||
let databases: Vec<Database> = get_existing_dbs(&mut client)?.values().cloned().collect();
|
|
||||||
|
|
||||||
Ok(CatalogObjects { roles, databases })
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum SchemaDumpError {
|
|
||||||
#[error("Database does not exist.")]
|
|
||||||
DatabaseDoesNotExist,
|
|
||||||
#[error("Failed to execute pg_dump.")]
|
|
||||||
IO(#[from] std::io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
// It uses the pg_dump utility to dump the schema of the specified database.
|
|
||||||
// The output is streamed back to the caller and supposed to be streamed via HTTP.
|
|
||||||
//
|
|
||||||
// Before return the result with the output, it checks that pg_dump produced any output.
|
|
||||||
// If not, it tries to parse the stderr output to determine if the database does not exist
|
|
||||||
// and special error is returned.
|
|
||||||
//
|
|
||||||
// To make sure that the process is killed when the caller drops the stream, we use tokio kill_on_drop feature.
|
|
||||||
pub async fn get_database_schema(
|
|
||||||
compute: &Arc<ComputeNode>,
|
|
||||||
dbname: &str,
|
|
||||||
) -> Result<impl Stream<Item = Result<bytes::Bytes, std::io::Error>>, SchemaDumpError> {
|
|
||||||
let pgbin = &compute.pgbin;
|
|
||||||
let basepath = Path::new(pgbin).parent().unwrap();
|
|
||||||
let pgdump = basepath.join("pg_dump");
|
|
||||||
let mut connstr = compute.connstr.clone();
|
|
||||||
connstr.set_path(dbname);
|
|
||||||
let mut cmd = Command::new(pgdump)
|
|
||||||
.arg("--schema-only")
|
|
||||||
.arg(connstr.as_str())
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stderr(Stdio::piped())
|
|
||||||
.kill_on_drop(true)
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
let stdout = cmd.stdout.take().ok_or_else(|| {
|
|
||||||
std::io::Error::new(std::io::ErrorKind::Other, "Failed to capture stdout.")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let stderr = cmd.stderr.take().ok_or_else(|| {
|
|
||||||
std::io::Error::new(std::io::ErrorKind::Other, "Failed to capture stderr.")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut stdout_reader = FramedRead::new(stdout, BytesCodec::new());
|
|
||||||
let stderr_reader = BufReader::new(stderr);
|
|
||||||
|
|
||||||
let first_chunk = match stdout_reader.next().await {
|
|
||||||
Some(Ok(bytes)) if !bytes.is_empty() => bytes,
|
|
||||||
Some(Err(e)) => {
|
|
||||||
return Err(SchemaDumpError::IO(e));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let mut lines = stderr_reader.lines();
|
|
||||||
if let Some(line) = lines.next_line().await? {
|
|
||||||
if line.contains(&format!("FATAL: database \"{}\" does not exist", dbname)) {
|
|
||||||
return Err(SchemaDumpError::DatabaseDoesNotExist);
|
|
||||||
}
|
|
||||||
warn!("pg_dump stderr: {}", line)
|
|
||||||
}
|
|
||||||
tokio::spawn(async move {
|
|
||||||
while let Ok(Some(line)) = lines.next_line().await {
|
|
||||||
warn!("pg_dump stderr: {}", line)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Err(SchemaDumpError::IO(std::io::Error::new(
|
|
||||||
std::io::ErrorKind::Other,
|
|
||||||
"failed to start pg_dump",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let initial_stream = stream::once(Ok(first_chunk.freeze()));
|
|
||||||
// Consume stderr and log warnings
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut lines = stderr_reader.lines();
|
|
||||||
while let Ok(Some(line)) = lines.next_line().await {
|
|
||||||
warn!("pg_dump stderr: {}", line)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(initial_stream.chain(stdout_reader.map(|res| res.map(|b| b.freeze()))))
|
|
||||||
}
|
|
||||||
@@ -818,15 +818,9 @@ impl ComputeNode {
|
|||||||
Client::connect(zenith_admin_connstr.as_str(), NoTls)
|
Client::connect(zenith_admin_connstr.as_str(), NoTls)
|
||||||
.context("broken cloud_admin credential: tried connecting with cloud_admin but could not authenticate, and zenith_admin does not work either")?;
|
.context("broken cloud_admin credential: tried connecting with cloud_admin but could not authenticate, and zenith_admin does not work either")?;
|
||||||
// Disable forwarding so that users don't get a cloud_admin role
|
// Disable forwarding so that users don't get a cloud_admin role
|
||||||
|
client.simple_query("SET neon.forward_ddl = false")?;
|
||||||
let mut func = || {
|
client.simple_query("CREATE USER cloud_admin WITH SUPERUSER")?;
|
||||||
client.simple_query("SET neon.forward_ddl = false")?;
|
client.simple_query("GRANT zenith_admin TO cloud_admin")?;
|
||||||
client.simple_query("CREATE USER cloud_admin WITH SUPERUSER")?;
|
|
||||||
client.simple_query("GRANT zenith_admin TO cloud_admin")?;
|
|
||||||
Ok::<_, anyhow::Error>(())
|
|
||||||
};
|
|
||||||
func().context("apply_config setup cloud_admin")?;
|
|
||||||
|
|
||||||
drop(client);
|
drop(client);
|
||||||
|
|
||||||
// reconnect with connstring with expected name
|
// reconnect with connstring with expected name
|
||||||
@@ -838,29 +832,24 @@ impl ComputeNode {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
client.simple_query("SET neon.forward_ddl = false")?;
|
||||||
.simple_query("SET neon.forward_ddl = false")
|
|
||||||
.context("apply_config SET neon.forward_ddl = false")?;
|
|
||||||
|
|
||||||
// Proceed with post-startup configuration. Note, that order of operations is important.
|
// 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).context("apply_config create_neon_superuser")?;
|
create_neon_superuser(spec, &mut client)?;
|
||||||
cleanup_instance(&mut client).context("apply_config cleanup_instance")?;
|
cleanup_instance(&mut client)?;
|
||||||
handle_roles(spec, &mut client).context("apply_config handle_roles")?;
|
handle_roles(spec, &mut client)?;
|
||||||
handle_databases(spec, &mut client).context("apply_config handle_databases")?;
|
handle_databases(spec, &mut client)?;
|
||||||
handle_role_deletions(spec, connstr.as_str(), &mut client)
|
handle_role_deletions(spec, connstr.as_str(), &mut client)?;
|
||||||
.context("apply_config handle_role_deletions")?;
|
|
||||||
handle_grants(
|
handle_grants(
|
||||||
spec,
|
spec,
|
||||||
&mut client,
|
&mut client,
|
||||||
connstr.as_str(),
|
connstr.as_str(),
|
||||||
self.has_feature(ComputeFeature::AnonExtension),
|
self.has_feature(ComputeFeature::AnonExtension),
|
||||||
)
|
)?;
|
||||||
.context("apply_config handle_grants")?;
|
handle_extensions(spec, &mut client)?;
|
||||||
handle_extensions(spec, &mut client).context("apply_config handle_extensions")?;
|
handle_extension_neon(&mut client)?;
|
||||||
handle_extension_neon(&mut client).context("apply_config handle_extension_neon")?;
|
create_availability_check_data(&mut client)?;
|
||||||
create_availability_check_data(&mut client)
|
|
||||||
.context("apply_config create_availability_check_data")?;
|
|
||||||
|
|
||||||
// 'Close' connection
|
// 'Close' connection
|
||||||
drop(client);
|
drop(client);
|
||||||
@@ -868,7 +857,7 @@ impl ComputeNode {
|
|||||||
// Run migrations separately to not hold up cold starts
|
// Run migrations separately to not hold up cold starts
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut client = Client::connect(connstr.as_str(), NoTls)?;
|
let mut client = Client::connect(connstr.as_str(), NoTls)?;
|
||||||
handle_migrations(&mut client).context("apply_config handle_migrations")
|
handle_migrations(&mut client)
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -918,39 +907,38 @@ impl ComputeNode {
|
|||||||
// temporarily reset max_cluster_size in config
|
// temporarily reset max_cluster_size in config
|
||||||
// to avoid the possibility of hitting the limit, while we are reconfiguring:
|
// to avoid the possibility of hitting the limit, while we are reconfiguring:
|
||||||
// creating new extensions, roles, etc...
|
// creating new extensions, roles, etc...
|
||||||
config::with_compute_ctl_tmp_override(pgdata_path, "neon.max_cluster_size=-1", || {
|
config::compute_ctl_temp_override_create(pgdata_path, "neon.max_cluster_size=-1")?;
|
||||||
self.pg_reload_conf()?;
|
self.pg_reload_conf()?;
|
||||||
|
|
||||||
let mut client = Client::connect(self.connstr.as_str(), NoTls)?;
|
let mut client = Client::connect(self.connstr.as_str(), NoTls)?;
|
||||||
|
|
||||||
// Proceed with post-startup configuration. Note, that order of operations is important.
|
// Proceed with post-startup configuration. Note, that order of operations is important.
|
||||||
// Disable DDL forwarding because control plane already knows about these roles/databases.
|
// Disable DDL forwarding because control plane already knows about these roles/databases.
|
||||||
if spec.mode == ComputeMode::Primary {
|
if spec.mode == ComputeMode::Primary {
|
||||||
client.simple_query("SET neon.forward_ddl = false")?;
|
client.simple_query("SET neon.forward_ddl = false")?;
|
||||||
cleanup_instance(&mut client)?;
|
cleanup_instance(&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(
|
handle_grants(
|
||||||
&spec,
|
&spec,
|
||||||
&mut client,
|
&mut client,
|
||||||
self.connstr.as_str(),
|
self.connstr.as_str(),
|
||||||
self.has_feature(ComputeFeature::AnonExtension),
|
self.has_feature(ComputeFeature::AnonExtension),
|
||||||
)?;
|
)?;
|
||||||
handle_extensions(&spec, &mut client)?;
|
handle_extensions(&spec, &mut client)?;
|
||||||
handle_extension_neon(&mut client)?;
|
handle_extension_neon(&mut client)?;
|
||||||
// We can skip handle_migrations here because a new migration can only appear
|
// We can skip handle_migrations here because a new migration can only appear
|
||||||
// if we have a new version of the compute_ctl binary, which can only happen
|
// if we have a new version of the compute_ctl binary, which can only happen
|
||||||
// if compute got restarted, in which case we'll end up inside of apply_config
|
// if compute got restarted, in which case we'll end up inside of apply_config
|
||||||
// instead of reconfigure.
|
// instead of reconfigure.
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'Close' connection
|
// 'Close' connection
|
||||||
drop(client);
|
drop(client);
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
|
// reset max_cluster_size in config back to original value and reload config
|
||||||
|
config::compute_ctl_temp_override_remove(pgdata_path)?;
|
||||||
self.pg_reload_conf()?;
|
self.pg_reload_conf()?;
|
||||||
|
|
||||||
let unknown_op = "unknown".to_string();
|
let unknown_op = "unknown".to_string();
|
||||||
@@ -1041,17 +1029,12 @@ impl ComputeNode {
|
|||||||
// temporarily reset max_cluster_size in config
|
// temporarily reset max_cluster_size in config
|
||||||
// to avoid the possibility of hitting the limit, while we are applying config:
|
// to avoid the possibility of hitting the limit, while we are applying config:
|
||||||
// creating new extensions, roles, etc...
|
// creating new extensions, roles, etc...
|
||||||
config::with_compute_ctl_tmp_override(
|
config::compute_ctl_temp_override_create(pgdata_path, "neon.max_cluster_size=-1")?;
|
||||||
pgdata_path,
|
self.pg_reload_conf()?;
|
||||||
"neon.max_cluster_size=-1",
|
|
||||||
|| {
|
|
||||||
self.pg_reload_conf()?;
|
|
||||||
|
|
||||||
self.apply_config(&compute_state)?;
|
self.apply_config(&compute_state)?;
|
||||||
|
|
||||||
Ok(())
|
config::compute_ctl_temp_override_remove(pgdata_path)?;
|
||||||
},
|
|
||||||
)?;
|
|
||||||
self.pg_reload_conf()?;
|
self.pg_reload_conf()?;
|
||||||
}
|
}
|
||||||
self.post_apply_config()?;
|
self.post_apply_config()?;
|
||||||
@@ -1279,12 +1262,10 @@ LIMIT 100",
|
|||||||
.await
|
.await
|
||||||
.map_err(DownloadError::Other);
|
.map_err(DownloadError::Other);
|
||||||
|
|
||||||
if download_size.is_ok() {
|
self.ext_download_progress
|
||||||
self.ext_download_progress
|
.write()
|
||||||
.write()
|
.expect("bad lock")
|
||||||
.expect("bad lock")
|
.insert(ext_archive_name.to_string(), (download_start, true));
|
||||||
.insert(ext_archive_name.to_string(), (download_start, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
download_size
|
download_size
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-33
@@ -6,8 +6,8 @@ use std::path::Path;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::pg_helpers::escape_conf_value;
|
use crate::pg_helpers::escape_conf_value;
|
||||||
use crate::pg_helpers::{GenericOptionExt, PgOptionsSerialize};
|
use crate::pg_helpers::PgOptionsSerialize;
|
||||||
use compute_api::spec::{ComputeMode, ComputeSpec, GenericOption};
|
use compute_api::spec::{ComputeMode, ComputeSpec};
|
||||||
|
|
||||||
/// Check that `line` is inside a text file and put it there if it is not.
|
/// Check that `line` is inside a text file and put it there if it is not.
|
||||||
/// Create file if it doesn't exist.
|
/// Create file if it doesn't exist.
|
||||||
@@ -92,27 +92,6 @@ pub fn write_postgres_conf(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg!(target_os = "linux") {
|
|
||||||
// Check /proc/sys/vm/overcommit_memory -- if it equals 2 (i.e. linux memory overcommit is
|
|
||||||
// disabled), then the control plane has enabled swap and we should set
|
|
||||||
// dynamic_shared_memory_type = 'mmap'.
|
|
||||||
//
|
|
||||||
// This is (maybe?) temporary - for more, see https://github.com/neondatabase/cloud/issues/12047.
|
|
||||||
let overcommit_memory_contents = std::fs::read_to_string("/proc/sys/vm/overcommit_memory")
|
|
||||||
// ignore any errors - they may be expected to occur under certain situations (e.g. when
|
|
||||||
// not running in Linux).
|
|
||||||
.unwrap_or_else(|_| String::new());
|
|
||||||
if overcommit_memory_contents.trim() == "2" {
|
|
||||||
let opt = GenericOption {
|
|
||||||
name: "dynamic_shared_memory_type".to_owned(),
|
|
||||||
value: Some("mmap".to_owned()),
|
|
||||||
vartype: "enum".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
write!(file, "{}", opt.to_pg_setting())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are any extra options in the 'settings' field, append those
|
// If there are any extra options in the 'settings' field, append those
|
||||||
if spec.cluster.settings.is_some() {
|
if spec.cluster.settings.is_some() {
|
||||||
writeln!(file, "# Managed by compute_ctl: begin")?;
|
writeln!(file, "# Managed by compute_ctl: begin")?;
|
||||||
@@ -131,17 +110,18 @@ pub fn write_postgres_conf(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_compute_ctl_tmp_override<F>(pgdata_path: &Path, options: &str, exec: F) -> Result<()>
|
/// create file compute_ctl_temp_override.conf in pgdata_dir
|
||||||
where
|
/// add provided options to this file
|
||||||
F: FnOnce() -> Result<()>,
|
pub fn compute_ctl_temp_override_create(pgdata_path: &Path, options: &str) -> Result<()> {
|
||||||
{
|
|
||||||
let path = pgdata_path.join("compute_ctl_temp_override.conf");
|
let path = pgdata_path.join("compute_ctl_temp_override.conf");
|
||||||
let mut file = File::create(path)?;
|
let mut file = File::create(path)?;
|
||||||
write!(file, "{}", options)?;
|
write!(file, "{}", options)?;
|
||||||
|
Ok(())
|
||||||
let res = exec();
|
}
|
||||||
|
|
||||||
file.set_len(0)?;
|
/// remove file compute_ctl_temp_override.conf in pgdata_dir
|
||||||
|
pub fn compute_ctl_temp_override_remove(pgdata_path: &Path) -> Result<()> {
|
||||||
res
|
let path = pgdata_path.join("compute_ctl_temp_override.conf");
|
||||||
|
std::fs::remove_file(path)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,17 @@ use std::net::SocketAddr;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use crate::catalog::SchemaDumpError;
|
|
||||||
use crate::catalog::{get_database_schema, get_dbs_and_roles};
|
|
||||||
use crate::compute::forward_termination_signal;
|
use crate::compute::forward_termination_signal;
|
||||||
use crate::compute::{ComputeNode, ComputeState, ParsedSpec};
|
use crate::compute::{ComputeNode, ComputeState, ParsedSpec};
|
||||||
use compute_api::requests::ConfigurationRequest;
|
use compute_api::requests::ConfigurationRequest;
|
||||||
use compute_api::responses::{ComputeStatus, ComputeStatusResponse, GenericAPIError};
|
use compute_api::responses::{ComputeStatus, ComputeStatusResponse, GenericAPIError};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use hyper::header::CONTENT_TYPE;
|
|
||||||
use hyper::service::{make_service_fn, service_fn};
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
use hyper::{Body, Method, Request, Response, Server, StatusCode};
|
use hyper::{Body, Method, Request, Response, Server, StatusCode};
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
use tracing_utils::http::OtelName;
|
use tracing_utils::http::OtelName;
|
||||||
use utils::http::request::must_get_query_param;
|
|
||||||
|
|
||||||
fn status_response_from_state(state: &ComputeState) -> ComputeStatusResponse {
|
fn status_response_from_state(state: &ComputeState) -> ComputeStatusResponse {
|
||||||
ComputeStatusResponse {
|
ComputeStatusResponse {
|
||||||
@@ -48,7 +44,7 @@ async fn routes(req: Request<Body>, compute: &Arc<ComputeNode>) -> Response<Body
|
|||||||
match (req.method(), req.uri().path()) {
|
match (req.method(), req.uri().path()) {
|
||||||
// Serialized compute state.
|
// Serialized compute state.
|
||||||
(&Method::GET, "/status") => {
|
(&Method::GET, "/status") => {
|
||||||
debug!("serving /status GET request");
|
info!("serving /status GET request");
|
||||||
let state = compute.state.lock().unwrap();
|
let state = compute.state.lock().unwrap();
|
||||||
let status_response = status_response_from_state(&state);
|
let status_response = status_response_from_state(&state);
|
||||||
Response::new(Body::from(serde_json::to_string(&status_response).unwrap()))
|
Response::new(Body::from(serde_json::to_string(&status_response).unwrap()))
|
||||||
@@ -137,34 +133,6 @@ async fn routes(req: Request<Body>, compute: &Arc<ComputeNode>) -> Response<Body
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(&Method::GET, "/dbs_and_roles") => {
|
|
||||||
info!("serving /dbs_and_roles GET request",);
|
|
||||||
match get_dbs_and_roles(compute).await {
|
|
||||||
Ok(res) => render_json(Body::from(serde_json::to_string(&res).unwrap())),
|
|
||||||
Err(_) => {
|
|
||||||
render_json_error("can't get dbs and roles", StatusCode::INTERNAL_SERVER_ERROR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(&Method::GET, "/database_schema") => {
|
|
||||||
let database = match must_get_query_param(&req, "database") {
|
|
||||||
Err(e) => return e.into_response(),
|
|
||||||
Ok(database) => database,
|
|
||||||
};
|
|
||||||
info!("serving /database_schema GET request with database: {database}",);
|
|
||||||
match get_database_schema(compute, &database).await {
|
|
||||||
Ok(res) => render_plain(Body::wrap_stream(res)),
|
|
||||||
Err(SchemaDumpError::DatabaseDoesNotExist) => {
|
|
||||||
render_json_error("database does not exist", StatusCode::NOT_FOUND)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("can't get schema dump: {}", e);
|
|
||||||
render_json_error("can't get schema dump", StatusCode::INTERNAL_SERVER_ERROR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// download extension files from remote extension storage on demand
|
// download extension files from remote extension storage on demand
|
||||||
(&Method::POST, route) if route.starts_with("/extension_server/") => {
|
(&Method::POST, route) if route.starts_with("/extension_server/") => {
|
||||||
info!("serving {:?} POST request", route);
|
info!("serving {:?} POST request", route);
|
||||||
@@ -335,25 +303,10 @@ fn render_json_error(e: &str, status: StatusCode) -> Response<Body> {
|
|||||||
};
|
};
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(status)
|
.status(status)
|
||||||
.header(CONTENT_TYPE, "application/json")
|
|
||||||
.body(Body::from(serde_json::to_string(&error).unwrap()))
|
.body(Body::from(serde_json::to_string(&error).unwrap()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_json(body: Body) -> Response<Body> {
|
|
||||||
Response::builder()
|
|
||||||
.header(CONTENT_TYPE, "application/json")
|
|
||||||
.body(body)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_plain(body: Body) -> Response<Body> {
|
|
||||||
Response::builder()
|
|
||||||
.header(CONTENT_TYPE, "text/plain")
|
|
||||||
.body(body)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_terminate_request(compute: &Arc<ComputeNode>) -> Result<(), (String, StatusCode)> {
|
async fn handle_terminate_request(compute: &Arc<ComputeNode>) -> Result<(), (String, StatusCode)> {
|
||||||
{
|
{
|
||||||
let mut state = compute.state.lock().unwrap();
|
let mut state = compute.state.lock().unwrap();
|
||||||
|
|||||||
@@ -68,51 +68,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Info"
|
$ref: "#/components/schemas/Info"
|
||||||
|
|
||||||
/dbs_and_roles:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- Info
|
|
||||||
summary: Get databases and roles in the catalog.
|
|
||||||
description: ""
|
|
||||||
operationId: getDbsAndRoles
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: Compute schema objects
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: "#/components/schemas/DbsAndRoles"
|
|
||||||
|
|
||||||
/database_schema:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- Info
|
|
||||||
summary: Get schema dump
|
|
||||||
parameters:
|
|
||||||
- name: database
|
|
||||||
in: query
|
|
||||||
description: Database name to dump.
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
example: "postgres"
|
|
||||||
description: Get schema dump in SQL format.
|
|
||||||
operationId: getDatabaseSchema
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: Schema dump
|
|
||||||
content:
|
|
||||||
text/plain:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: Schema dump in SQL format.
|
|
||||||
404:
|
|
||||||
description: Non existing database.
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: "#/components/schemas/GenericError"
|
|
||||||
|
|
||||||
/check_writability:
|
/check_writability:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@@ -274,73 +229,6 @@ components:
|
|||||||
num_cpus:
|
num_cpus:
|
||||||
type: integer
|
type: integer
|
||||||
|
|
||||||
DbsAndRoles:
|
|
||||||
type: object
|
|
||||||
description: Databases and Roles
|
|
||||||
required:
|
|
||||||
- roles
|
|
||||||
- databases
|
|
||||||
properties:
|
|
||||||
roles:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/Role"
|
|
||||||
databases:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/Database"
|
|
||||||
|
|
||||||
Database:
|
|
||||||
type: object
|
|
||||||
description: Database
|
|
||||||
required:
|
|
||||||
- name
|
|
||||||
- owner
|
|
||||||
- restrict_conn
|
|
||||||
- invalid
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
owner:
|
|
||||||
type: string
|
|
||||||
options:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/GenericOption"
|
|
||||||
restrict_conn:
|
|
||||||
type: boolean
|
|
||||||
invalid:
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
Role:
|
|
||||||
type: object
|
|
||||||
description: Role
|
|
||||||
required:
|
|
||||||
- name
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
encrypted_password:
|
|
||||||
type: string
|
|
||||||
options:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/GenericOption"
|
|
||||||
|
|
||||||
GenericOption:
|
|
||||||
type: object
|
|
||||||
description: Schema Generic option
|
|
||||||
required:
|
|
||||||
- name
|
|
||||||
- vartype
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
value:
|
|
||||||
type: string
|
|
||||||
vartype:
|
|
||||||
type: string
|
|
||||||
|
|
||||||
ComputeState:
|
ComputeState:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -8,12 +8,10 @@ pub mod configurator;
|
|||||||
pub mod http;
|
pub mod http;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
pub mod catalog;
|
|
||||||
pub mod compute;
|
pub mod compute;
|
||||||
pub mod extension_server;
|
pub mod extension_server;
|
||||||
pub mod monitor;
|
pub mod monitor;
|
||||||
pub mod params;
|
pub mod params;
|
||||||
pub mod pg_helpers;
|
pub mod pg_helpers;
|
||||||
pub mod spec;
|
pub mod spec;
|
||||||
pub mod swap;
|
|
||||||
pub mod sync_sk;
|
pub mod sync_sk;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
ALTER ROLE neon_superuser BYPASSRLS;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
role_name text;
|
|
||||||
BEGIN
|
|
||||||
FOR role_name IN SELECT rolname FROM pg_roles WHERE pg_has_role(rolname, 'neon_superuser', 'member')
|
|
||||||
LOOP
|
|
||||||
RAISE NOTICE 'EXECUTING ALTER ROLE % INHERIT', quote_ident(role_name);
|
|
||||||
EXECUTE 'ALTER ROLE ' || quote_ident(role_name) || ' INHERIT';
|
|
||||||
END LOOP;
|
|
||||||
|
|
||||||
FOR role_name IN SELECT rolname FROM pg_roles
|
|
||||||
WHERE
|
|
||||||
NOT pg_has_role(rolname, 'neon_superuser', 'member') AND NOT starts_with(rolname, 'pg_')
|
|
||||||
LOOP
|
|
||||||
RAISE NOTICE 'EXECUTING ALTER ROLE % NOBYPASSRLS', quote_ident(role_name);
|
|
||||||
EXECUTE 'ALTER ROLE ' || quote_ident(role_name) || ' NOBYPASSRLS';
|
|
||||||
END LOOP;
|
|
||||||
END $$;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF (SELECT setting::numeric >= 160000 FROM pg_settings WHERE name = 'server_version_num') THEN
|
|
||||||
EXECUTE 'GRANT pg_create_subscription TO neon_superuser';
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
GRANT pg_monitor TO neon_superuser WITH ADMIN OPTION;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- SKIP: Deemed insufficient for allowing relations created by extensions to be
|
|
||||||
-- interacted with by neon_superuser without permission issues.
|
|
||||||
|
|
||||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO neon_superuser;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- SKIP: Deemed insufficient for allowing relations created by extensions to be
|
|
||||||
-- interacted with by neon_superuser without permission issues.
|
|
||||||
|
|
||||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO neon_superuser;
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
-- SKIP: Moved inline to the handle_grants() functions.
|
|
||||||
|
|
||||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO neon_superuser WITH GRANT OPTION;
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
-- SKIP: Moved inline to the handle_grants() functions.
|
|
||||||
|
|
||||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO neon_superuser WITH GRANT OPTION;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
-- SKIP: The original goal of this migration was to prevent creating
|
|
||||||
-- subscriptions, but this migration was insufficient.
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
role_name TEXT;
|
|
||||||
BEGIN
|
|
||||||
FOR role_name IN SELECT rolname FROM pg_roles WHERE rolreplication IS TRUE
|
|
||||||
LOOP
|
|
||||||
RAISE NOTICE 'EXECUTING ALTER ROLE % NOREPLICATION', quote_ident(role_name);
|
|
||||||
EXECUTE 'ALTER ROLE ' || quote_ident(role_name) || ' NOREPLICATION';
|
|
||||||
END LOOP;
|
|
||||||
END $$;
|
|
||||||
@@ -44,7 +44,7 @@ pub fn escape_conf_value(s: &str) -> String {
|
|||||||
format!("'{}'", res)
|
format!("'{}'", res)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait GenericOptionExt {
|
trait GenericOptionExt {
|
||||||
fn to_pg_option(&self) -> String;
|
fn to_pg_option(&self) -> String;
|
||||||
fn to_pg_setting(&self) -> String;
|
fn to_pg_setting(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|||||||
+84
-71
@@ -2,7 +2,7 @@ use std::fs::File;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use postgres::config::Config;
|
use postgres::config::Config;
|
||||||
use postgres::{Client, NoTls};
|
use postgres::{Client, NoTls};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
@@ -302,9 +302,9 @@ pub fn handle_roles(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
|
|||||||
RoleAction::Create => {
|
RoleAction::Create => {
|
||||||
// This branch only runs when roles are created through the console, so it is
|
// This branch only runs when roles are created through the console, so it is
|
||||||
// safe to add more permissions here. BYPASSRLS and REPLICATION are inherited
|
// safe to add more permissions here. BYPASSRLS and REPLICATION are inherited
|
||||||
// from neon_superuser.
|
// from neon_superuser. (NOTE: REPLICATION has been removed from here for now).
|
||||||
let mut query: String = format!(
|
let mut query: String = format!(
|
||||||
"CREATE ROLE {} INHERIT CREATEROLE CREATEDB BYPASSRLS REPLICATION IN ROLE neon_superuser",
|
"CREATE ROLE {} INHERIT CREATEROLE CREATEDB BYPASSRLS IN ROLE neon_superuser",
|
||||||
name.pg_quote()
|
name.pg_quote()
|
||||||
);
|
);
|
||||||
info!("running role create query: '{}'", &query);
|
info!("running role create query: '{}'", &query);
|
||||||
@@ -490,7 +490,7 @@ pub fn handle_databases(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
|
|||||||
"rename_db" => {
|
"rename_db" => {
|
||||||
let new_name = op.new_name.as_ref().unwrap();
|
let new_name = op.new_name.as_ref().unwrap();
|
||||||
|
|
||||||
if existing_dbs.contains_key(&op.name) {
|
if existing_dbs.get(&op.name).is_some() {
|
||||||
let query: String = format!(
|
let query: String = format!(
|
||||||
"ALTER DATABASE {} RENAME TO {}",
|
"ALTER DATABASE {} RENAME TO {}",
|
||||||
op.name.pg_quote(),
|
op.name.pg_quote(),
|
||||||
@@ -698,8 +698,7 @@ pub fn handle_grants(
|
|||||||
|
|
||||||
// it is important to run this after all grants
|
// it is important to run this after all grants
|
||||||
if enable_anon_extension {
|
if enable_anon_extension {
|
||||||
handle_extension_anon(spec, &db.owner, &mut db_client, false)
|
handle_extension_anon(spec, &db.owner, &mut db_client, false)?;
|
||||||
.context("handle_grants handle_extension_anon")?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,24 +743,21 @@ pub fn handle_extension_neon(client: &mut Client) -> Result<()> {
|
|||||||
// which may happen in two cases:
|
// which may happen in two cases:
|
||||||
// - extension was just installed
|
// - extension was just installed
|
||||||
// - extension was already installed and is up to date
|
// - extension was already installed and is up to date
|
||||||
let query = "ALTER EXTENSION neon UPDATE";
|
// DISABLED due to compute node unpinning epic
|
||||||
info!("update neon extension version with query: {}", query);
|
// let query = "ALTER EXTENSION neon UPDATE";
|
||||||
if let Err(e) = client.simple_query(query) {
|
// info!("update neon extension version with query: {}", query);
|
||||||
error!(
|
// client.simple_query(query)?;
|
||||||
"failed to upgrade neon extension during `handle_extension_neon`: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub fn handle_neon_extension_upgrade(client: &mut Client) -> Result<()> {
|
pub fn handle_neon_extension_upgrade(_client: &mut Client) -> Result<()> {
|
||||||
info!("handle neon extension upgrade");
|
info!("handle neon extension upgrade (not really)");
|
||||||
let query = "ALTER EXTENSION neon UPDATE";
|
// DISABLED due to compute node unpinning epic
|
||||||
info!("update neon extension version with query: {}", query);
|
// let query = "ALTER EXTENSION neon UPDATE";
|
||||||
client.simple_query(query)?;
|
// info!("update neon extension version with query: {}", query);
|
||||||
|
// client.simple_query(query)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -774,66 +770,87 @@ pub fn handle_migrations(client: &mut Client) -> Result<()> {
|
|||||||
// !BE SURE TO ONLY ADD MIGRATIONS TO THE END OF THIS ARRAY. IF YOU DO NOT, VERY VERY BAD THINGS MAY HAPPEN!
|
// !BE SURE TO ONLY ADD MIGRATIONS TO THE END OF THIS ARRAY. IF YOU DO NOT, VERY VERY BAD THINGS MAY HAPPEN!
|
||||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
|
||||||
// Add new migrations in numerical order.
|
|
||||||
let migrations = [
|
let migrations = [
|
||||||
include_str!("./migrations/0000-neon_superuser_bypass_rls.sql"),
|
"ALTER ROLE neon_superuser BYPASSRLS",
|
||||||
include_str!("./migrations/0001-alter_roles.sql"),
|
r#"
|
||||||
include_str!("./migrations/0002-grant_pg_create_subscription_to_neon_superuser.sql"),
|
DO $$
|
||||||
include_str!("./migrations/0003-grant_pg_monitor_to_neon_superuser.sql"),
|
DECLARE
|
||||||
include_str!("./migrations/0004-grant_all_on_tables_to_neon_superuser.sql"),
|
role_name text;
|
||||||
include_str!("./migrations/0005-grant_all_on_sequences_to_neon_superuser.sql"),
|
BEGIN
|
||||||
include_str!(
|
FOR role_name IN SELECT rolname FROM pg_roles WHERE pg_has_role(rolname, 'neon_superuser', 'member')
|
||||||
"./migrations/0006-grant_all_on_tables_to_neon_superuser_with_grant_option.sql"
|
LOOP
|
||||||
),
|
RAISE NOTICE 'EXECUTING ALTER ROLE % INHERIT', quote_ident(role_name);
|
||||||
include_str!(
|
EXECUTE 'ALTER ROLE ' || quote_ident(role_name) || ' INHERIT';
|
||||||
"./migrations/0007-grant_all_on_sequences_to_neon_superuser_with_grant_option.sql"
|
END LOOP;
|
||||||
),
|
|
||||||
include_str!("./migrations/0008-revoke_replication_for_previously_allowed_roles.sql"),
|
FOR role_name IN SELECT rolname FROM pg_roles
|
||||||
|
WHERE
|
||||||
|
NOT pg_has_role(rolname, 'neon_superuser', 'member') AND NOT starts_with(rolname, 'pg_')
|
||||||
|
LOOP
|
||||||
|
RAISE NOTICE 'EXECUTING ALTER ROLE % NOBYPASSRLS', quote_ident(role_name);
|
||||||
|
EXECUTE 'ALTER ROLE ' || quote_ident(role_name) || ' NOBYPASSRLS';
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
"#,
|
||||||
|
r#"
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF (SELECT setting::numeric >= 160000 FROM pg_settings WHERE name = 'server_version_num') THEN
|
||||||
|
EXECUTE 'GRANT pg_create_subscription TO neon_superuser';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;"#,
|
||||||
|
"GRANT pg_monitor TO neon_superuser WITH ADMIN OPTION",
|
||||||
|
// Don't remove: these are some SQLs that we originally applied in migrations but turned out to execute somewhere else.
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
// Add new migrations below.
|
||||||
|
r#"
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
role_name TEXT;
|
||||||
|
BEGIN
|
||||||
|
FOR role_name IN SELECT rolname FROM pg_roles WHERE rolreplication IS TRUE
|
||||||
|
LOOP
|
||||||
|
RAISE NOTICE 'EXECUTING ALTER ROLE % NOREPLICATION', quote_ident(role_name);
|
||||||
|
EXECUTE 'ALTER ROLE ' || quote_ident(role_name) || ' NOREPLICATION';
|
||||||
|
END LOOP;
|
||||||
|
END
|
||||||
|
$$;"#,
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut func = || {
|
let mut query = "CREATE SCHEMA IF NOT EXISTS neon_migration";
|
||||||
let query = "CREATE SCHEMA IF NOT EXISTS neon_migration";
|
client.simple_query(query)?;
|
||||||
client.simple_query(query)?;
|
|
||||||
|
|
||||||
let query = "CREATE TABLE IF NOT EXISTS neon_migration.migration_id (key INT NOT NULL PRIMARY KEY, id bigint NOT NULL DEFAULT 0)";
|
query = "CREATE TABLE IF NOT EXISTS neon_migration.migration_id (key INT NOT NULL PRIMARY KEY, id bigint NOT NULL DEFAULT 0)";
|
||||||
client.simple_query(query)?;
|
client.simple_query(query)?;
|
||||||
|
|
||||||
let query = "INSERT INTO neon_migration.migration_id VALUES (0, 0) ON CONFLICT DO NOTHING";
|
query = "INSERT INTO neon_migration.migration_id VALUES (0, 0) ON CONFLICT DO NOTHING";
|
||||||
client.simple_query(query)?;
|
client.simple_query(query)?;
|
||||||
|
|
||||||
let query = "ALTER SCHEMA neon_migration OWNER TO cloud_admin";
|
query = "ALTER SCHEMA neon_migration OWNER TO cloud_admin";
|
||||||
client.simple_query(query)?;
|
client.simple_query(query)?;
|
||||||
|
|
||||||
let query = "REVOKE ALL ON SCHEMA neon_migration FROM PUBLIC";
|
query = "REVOKE ALL ON SCHEMA neon_migration FROM PUBLIC";
|
||||||
client.simple_query(query)?;
|
client.simple_query(query)?;
|
||||||
Ok::<_, anyhow::Error>(())
|
|
||||||
};
|
|
||||||
func().context("handle_migrations prepare")?;
|
|
||||||
|
|
||||||
let query = "SELECT id FROM neon_migration.migration_id";
|
query = "SELECT id FROM neon_migration.migration_id";
|
||||||
let row = client
|
let row = client.query_one(query, &[])?;
|
||||||
.query_one(query, &[])
|
|
||||||
.context("handle_migrations get migration_id")?;
|
|
||||||
let mut current_migration: usize = row.get::<&str, i64>("id") as usize;
|
let mut current_migration: usize = row.get::<&str, i64>("id") as usize;
|
||||||
let starting_migration_id = current_migration;
|
let starting_migration_id = current_migration;
|
||||||
|
|
||||||
let query = "BEGIN";
|
query = "BEGIN";
|
||||||
client
|
client.simple_query(query)?;
|
||||||
.simple_query(query)
|
|
||||||
.context("handle_migrations begin")?;
|
|
||||||
|
|
||||||
while current_migration < migrations.len() {
|
while current_migration < migrations.len() {
|
||||||
let migration = &migrations[current_migration];
|
let migration = &migrations[current_migration];
|
||||||
if migration.starts_with("-- SKIP") {
|
if migration.is_empty() {
|
||||||
info!("Skipping migration id={}", current_migration);
|
info!("Skip migration id={}", current_migration);
|
||||||
} else {
|
} else {
|
||||||
info!(
|
info!("Running migration:\n{}\n", migration);
|
||||||
"Running migration id={}:\n{}\n",
|
client.simple_query(migration)?;
|
||||||
current_migration, migration
|
|
||||||
);
|
|
||||||
client.simple_query(migration).with_context(|| {
|
|
||||||
format!("handle_migrations current_migration={}", current_migration)
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
current_migration += 1;
|
current_migration += 1;
|
||||||
}
|
}
|
||||||
@@ -841,14 +858,10 @@ pub fn handle_migrations(client: &mut Client) -> Result<()> {
|
|||||||
"UPDATE neon_migration.migration_id SET id={}",
|
"UPDATE neon_migration.migration_id SET id={}",
|
||||||
migrations.len()
|
migrations.len()
|
||||||
);
|
);
|
||||||
client
|
client.simple_query(&setval)?;
|
||||||
.simple_query(&setval)
|
|
||||||
.context("handle_migrations update id")?;
|
|
||||||
|
|
||||||
let query = "COMMIT";
|
query = "COMMIT";
|
||||||
client
|
client.simple_query(query)?;
|
||||||
.simple_query(query)
|
|
||||||
.context("handle_migrations commit")?;
|
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Ran {} migrations",
|
"Ran {} migrations",
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Context};
|
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
pub const RESIZE_SWAP_BIN: &str = "/neonvm/bin/resize-swap";
|
|
||||||
|
|
||||||
pub fn resize_swap(size_bytes: u64) -> anyhow::Result<()> {
|
|
||||||
// run `/neonvm/bin/resize-swap --once {size_bytes}`
|
|
||||||
//
|
|
||||||
// Passing '--once' causes resize-swap to delete itself after successful completion, which
|
|
||||||
// means that if compute_ctl restarts later, we won't end up calling 'swapoff' while
|
|
||||||
// postgres is running.
|
|
||||||
//
|
|
||||||
// NOTE: resize-swap is not very clever. If present, --once MUST be the first arg.
|
|
||||||
let child_result = std::process::Command::new("/usr/bin/sudo")
|
|
||||||
.arg(RESIZE_SWAP_BIN)
|
|
||||||
.arg("--once")
|
|
||||||
.arg(size_bytes.to_string())
|
|
||||||
.spawn();
|
|
||||||
|
|
||||||
child_result
|
|
||||||
.context("spawn() failed")
|
|
||||||
.and_then(|mut child| child.wait().context("wait() failed"))
|
|
||||||
.and_then(|status| match status.success() {
|
|
||||||
true => Ok(()),
|
|
||||||
false => {
|
|
||||||
// The command failed. Maybe it was because the resize-swap file doesn't exist?
|
|
||||||
// The --once flag causes it to delete itself on success so we don't disable swap
|
|
||||||
// while postgres is running; maybe this is fine.
|
|
||||||
match Path::new(RESIZE_SWAP_BIN).try_exists() {
|
|
||||||
Err(_) | Ok(true) => Err(anyhow!("process exited with {status}")),
|
|
||||||
// The path doesn't exist; we're actually ok
|
|
||||||
Ok(false) => {
|
|
||||||
warn!("ignoring \"not found\" error from resize-swap to avoid swapoff while compute is running");
|
|
||||||
Ok(())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// wrap any prior error with the overall context that we couldn't run the command
|
|
||||||
.with_context(|| {
|
|
||||||
format!("could not run `/usr/bin/sudo {RESIZE_SWAP_BIN} --once {size_bytes}`")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,6 @@ nix.workspace = true
|
|||||||
once_cell.workspace = true
|
once_cell.workspace = true
|
||||||
postgres.workspace = true
|
postgres.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
humantime-serde.workspace = true
|
|
||||||
hyper.workspace = true
|
hyper.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
reqwest = { workspace = true, features = ["blocking", "json"] }
|
reqwest = { workspace = true, features = ["blocking", "json"] }
|
||||||
@@ -28,7 +27,6 @@ serde_with.workspace = true
|
|||||||
tar.workspace = true
|
tar.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
toml_edit.workspace = true
|
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tokio-postgres.workspace = true
|
tokio-postgres.workspace = true
|
||||||
tokio-util.workspace = true
|
tokio-util.workspace = true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "storage_controller"
|
name = "attachment_service"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
@@ -25,13 +25,12 @@ git-version.workspace = true
|
|||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
hyper.workspace = true
|
hyper.workspace = true
|
||||||
humantime.workspace = true
|
humantime.workspace = true
|
||||||
itertools.workspace = true
|
|
||||||
lasso.workspace = true
|
lasso.workspace = true
|
||||||
once_cell.workspace = true
|
once_cell.workspace = true
|
||||||
pageserver_api.workspace = true
|
pageserver_api.workspace = true
|
||||||
pageserver_client.workspace = true
|
pageserver_client.workspace = true
|
||||||
postgres_connection.workspace = true
|
postgres_connection.workspace = true
|
||||||
reqwest = { workspace = true, features = ["stream"] }
|
reqwest.workspace = true
|
||||||
routerify.workspace = true
|
routerify.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
@@ -40,16 +39,13 @@ tokio.workspace = true
|
|||||||
tokio-util.workspace = true
|
tokio-util.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
measured.workspace = true
|
measured.workspace = true
|
||||||
scopeguard.workspace = true
|
|
||||||
strum.workspace = true
|
|
||||||
strum_macros.workspace = true
|
|
||||||
|
|
||||||
diesel = { version = "2.1.4", features = ["serde_json", "postgres", "r2d2"] }
|
diesel = { version = "2.1.4", features = ["serde_json", "postgres", "r2d2"] }
|
||||||
diesel_migrations = { version = "2.1.0" }
|
diesel_migrations = { version = "2.1.0" }
|
||||||
r2d2 = { version = "0.8.10" }
|
r2d2 = { version = "0.8.10" }
|
||||||
|
|
||||||
utils = { path = "../libs/utils/" }
|
utils = { path = "../../libs/utils/" }
|
||||||
metrics = { path = "../libs/metrics/" }
|
metrics = { path = "../../libs/metrics/" }
|
||||||
control_plane = { path = "../control_plane" }
|
control_plane = { path = ".." }
|
||||||
workspace_hack = { version = "0.1", path = "../workspace_hack" }
|
workspace_hack = { version = "0.1", path = "../../workspace_hack" }
|
||||||
|
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
use std::{collections::HashMap, time::Duration};
|
||||||
|
|
||||||
|
use control_plane::endpoint::{ComputeControlPlane, EndpointStatus};
|
||||||
|
use control_plane::local_env::LocalEnv;
|
||||||
|
use hyper::{Method, StatusCode};
|
||||||
|
use pageserver_api::shard::{ShardCount, ShardNumber, ShardStripeSize, TenantShardId};
|
||||||
|
use postgres_connection::parse_host_port;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use utils::{
|
||||||
|
backoff::{self},
|
||||||
|
id::{NodeId, TenantId},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::service::Config;
|
||||||
|
|
||||||
|
const BUSY_DELAY: Duration = Duration::from_secs(1);
|
||||||
|
const SLOWDOWN_DELAY: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
pub(crate) const API_CONCURRENCY: usize = 32;
|
||||||
|
|
||||||
|
struct ShardedComputeHookTenant {
|
||||||
|
stripe_size: ShardStripeSize,
|
||||||
|
shard_count: ShardCount,
|
||||||
|
shards: Vec<(ShardNumber, NodeId)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ComputeHookTenant {
|
||||||
|
Unsharded(NodeId),
|
||||||
|
Sharded(ShardedComputeHookTenant),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComputeHookTenant {
|
||||||
|
/// Construct with at least one shard's information
|
||||||
|
fn new(tenant_shard_id: TenantShardId, stripe_size: ShardStripeSize, node_id: NodeId) -> Self {
|
||||||
|
if tenant_shard_id.shard_count.count() > 1 {
|
||||||
|
Self::Sharded(ShardedComputeHookTenant {
|
||||||
|
shards: vec![(tenant_shard_id.shard_number, node_id)],
|
||||||
|
stripe_size,
|
||||||
|
shard_count: tenant_shard_id.shard_count,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Self::Unsharded(node_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set one shard's location. If stripe size or shard count have changed, Self is reset
|
||||||
|
/// and drops existing content.
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tenant_shard_id: TenantShardId,
|
||||||
|
stripe_size: ShardStripeSize,
|
||||||
|
node_id: NodeId,
|
||||||
|
) {
|
||||||
|
match self {
|
||||||
|
Self::Unsharded(existing_node_id) if tenant_shard_id.shard_count.count() == 1 => {
|
||||||
|
*existing_node_id = node_id
|
||||||
|
}
|
||||||
|
Self::Sharded(sharded_tenant)
|
||||||
|
if sharded_tenant.stripe_size == stripe_size
|
||||||
|
&& sharded_tenant.shard_count == tenant_shard_id.shard_count =>
|
||||||
|
{
|
||||||
|
if let Some(existing) = sharded_tenant
|
||||||
|
.shards
|
||||||
|
.iter()
|
||||||
|
.position(|s| s.0 == tenant_shard_id.shard_number)
|
||||||
|
{
|
||||||
|
sharded_tenant.shards.get_mut(existing).unwrap().1 = node_id;
|
||||||
|
} else {
|
||||||
|
sharded_tenant
|
||||||
|
.shards
|
||||||
|
.push((tenant_shard_id.shard_number, node_id));
|
||||||
|
sharded_tenant.shards.sort_by_key(|s| s.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Shard count changed: reset struct.
|
||||||
|
*self = Self::new(tenant_shard_id, stripe_size, node_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct ComputeHookNotifyRequestShard {
|
||||||
|
node_id: NodeId,
|
||||||
|
shard_number: ShardNumber,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request body that we send to the control plane to notify it of where a tenant is attached
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct ComputeHookNotifyRequest {
|
||||||
|
tenant_id: TenantId,
|
||||||
|
stripe_size: Option<ShardStripeSize>,
|
||||||
|
shards: Vec<ComputeHookNotifyRequestShard>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error type for attempts to call into the control plane compute notification hook
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub(crate) enum NotifyError {
|
||||||
|
// Request was not send successfully, e.g. transport error
|
||||||
|
#[error("Sending request: {0}")]
|
||||||
|
Request(#[from] reqwest::Error),
|
||||||
|
// Request could not be serviced right now due to ongoing Operation in control plane, but should be possible soon.
|
||||||
|
#[error("Control plane tenant busy")]
|
||||||
|
Busy,
|
||||||
|
// Explicit 429 response asking us to retry less frequently
|
||||||
|
#[error("Control plane overloaded")]
|
||||||
|
SlowDown,
|
||||||
|
// A 503 response indicates the control plane can't handle the request right now
|
||||||
|
#[error("Control plane unavailable (status {0})")]
|
||||||
|
Unavailable(StatusCode),
|
||||||
|
// API returned unexpected non-success status. We will retry, but log a warning.
|
||||||
|
#[error("Control plane returned unexpected status {0}")]
|
||||||
|
Unexpected(StatusCode),
|
||||||
|
// We shutdown while sending
|
||||||
|
#[error("Shutting down")]
|
||||||
|
ShuttingDown,
|
||||||
|
// A response indicates we will never succeed, such as 400 or 404
|
||||||
|
#[error("Non-retryable error {0}")]
|
||||||
|
Fatal(StatusCode),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComputeHookTenant {
|
||||||
|
fn maybe_reconfigure(&self, tenant_id: TenantId) -> Option<ComputeHookNotifyRequest> {
|
||||||
|
match self {
|
||||||
|
Self::Unsharded(node_id) => Some(ComputeHookNotifyRequest {
|
||||||
|
tenant_id,
|
||||||
|
shards: vec![ComputeHookNotifyRequestShard {
|
||||||
|
shard_number: ShardNumber(0),
|
||||||
|
node_id: *node_id,
|
||||||
|
}],
|
||||||
|
stripe_size: None,
|
||||||
|
}),
|
||||||
|
Self::Sharded(sharded_tenant)
|
||||||
|
if sharded_tenant.shards.len() == sharded_tenant.shard_count.count() as usize =>
|
||||||
|
{
|
||||||
|
Some(ComputeHookNotifyRequest {
|
||||||
|
tenant_id,
|
||||||
|
shards: sharded_tenant
|
||||||
|
.shards
|
||||||
|
.iter()
|
||||||
|
.map(|(shard_number, node_id)| ComputeHookNotifyRequestShard {
|
||||||
|
shard_number: *shard_number,
|
||||||
|
node_id: *node_id,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
stripe_size: Some(sharded_tenant.stripe_size),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Self::Sharded(sharded_tenant) => {
|
||||||
|
// Sharded tenant doesn't yet have information for all its shards
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"ComputeHookTenant::maybe_reconfigure: not enough shards ({}/{})",
|
||||||
|
sharded_tenant.shards.len(),
|
||||||
|
sharded_tenant.shard_count.count()
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The compute hook is a destination for notifications about changes to tenant:pageserver
|
||||||
|
/// mapping. It aggregates updates for the shards in a tenant, and when appropriate reconfigures
|
||||||
|
/// the compute connection string.
|
||||||
|
pub(super) struct ComputeHook {
|
||||||
|
config: Config,
|
||||||
|
state: tokio::sync::Mutex<HashMap<TenantId, ComputeHookTenant>>,
|
||||||
|
authorization_header: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComputeHook {
|
||||||
|
pub(super) fn new(config: Config) -> Self {
|
||||||
|
let authorization_header = config
|
||||||
|
.control_plane_jwt_token
|
||||||
|
.clone()
|
||||||
|
.map(|jwt| format!("Bearer {}", jwt));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
state: Default::default(),
|
||||||
|
config,
|
||||||
|
authorization_header,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For test environments: use neon_local's LocalEnv to update compute
|
||||||
|
async fn do_notify_local(
|
||||||
|
&self,
|
||||||
|
reconfigure_request: ComputeHookNotifyRequest,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let env = match LocalEnv::load_config() {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Couldn't load neon_local config, skipping compute update ({e})");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let cplane =
|
||||||
|
ComputeControlPlane::load(env.clone()).expect("Error loading compute control plane");
|
||||||
|
let ComputeHookNotifyRequest {
|
||||||
|
tenant_id,
|
||||||
|
shards,
|
||||||
|
stripe_size,
|
||||||
|
} = reconfigure_request;
|
||||||
|
|
||||||
|
let compute_pageservers = shards
|
||||||
|
.into_iter()
|
||||||
|
.map(|shard| {
|
||||||
|
let ps_conf = env
|
||||||
|
.get_pageserver_conf(shard.node_id)
|
||||||
|
.expect("Unknown pageserver");
|
||||||
|
let (pg_host, pg_port) = parse_host_port(&ps_conf.listen_pg_addr)
|
||||||
|
.expect("Unable to parse listen_pg_addr");
|
||||||
|
(pg_host, pg_port.unwrap_or(5432))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for (endpoint_name, endpoint) in &cplane.endpoints {
|
||||||
|
if endpoint.tenant_id == tenant_id && endpoint.status() == EndpointStatus::Running {
|
||||||
|
tracing::info!("Reconfiguring endpoint {}", endpoint_name,);
|
||||||
|
endpoint
|
||||||
|
.reconfigure(compute_pageservers.clone(), stripe_size)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_notify_iteration(
|
||||||
|
&self,
|
||||||
|
client: &reqwest::Client,
|
||||||
|
url: &String,
|
||||||
|
reconfigure_request: &ComputeHookNotifyRequest,
|
||||||
|
cancel: &CancellationToken,
|
||||||
|
) -> Result<(), NotifyError> {
|
||||||
|
let req = client.request(Method::PUT, url);
|
||||||
|
let req = if let Some(value) = &self.authorization_header {
|
||||||
|
req.header(reqwest::header::AUTHORIZATION, value)
|
||||||
|
} else {
|
||||||
|
req
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"Sending notify request to {} ({:?})",
|
||||||
|
url,
|
||||||
|
reconfigure_request
|
||||||
|
);
|
||||||
|
let send_result = req.json(&reconfigure_request).send().await;
|
||||||
|
let response = match send_result {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Treat all 2xx responses as success
|
||||||
|
if response.status() >= StatusCode::OK && response.status() < StatusCode::MULTIPLE_CHOICES {
|
||||||
|
if response.status() != StatusCode::OK {
|
||||||
|
// Non-200 2xx response: it doesn't make sense to retry, but this is unexpected, so
|
||||||
|
// log a warning.
|
||||||
|
tracing::warn!(
|
||||||
|
"Unexpected 2xx response code {} from control plane",
|
||||||
|
response.status()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error response codes
|
||||||
|
match response.status() {
|
||||||
|
StatusCode::TOO_MANY_REQUESTS => {
|
||||||
|
// TODO: 429 handling should be global: set some state visible to other requests
|
||||||
|
// so that they will delay before starting, rather than all notifications trying
|
||||||
|
// once before backing off.
|
||||||
|
tokio::time::timeout(SLOWDOWN_DELAY, cancel.cancelled())
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
Err(NotifyError::SlowDown)
|
||||||
|
}
|
||||||
|
StatusCode::LOCKED => {
|
||||||
|
// Delay our retry if busy: the usual fast exponential backoff in backoff::retry
|
||||||
|
// is not appropriate
|
||||||
|
tokio::time::timeout(BUSY_DELAY, cancel.cancelled())
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
Err(NotifyError::Busy)
|
||||||
|
}
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE
|
||||||
|
| StatusCode::GATEWAY_TIMEOUT
|
||||||
|
| StatusCode::BAD_GATEWAY => Err(NotifyError::Unavailable(response.status())),
|
||||||
|
StatusCode::BAD_REQUEST | StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
|
||||||
|
Err(NotifyError::Fatal(response.status()))
|
||||||
|
}
|
||||||
|
_ => Err(NotifyError::Unexpected(response.status())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_notify(
|
||||||
|
&self,
|
||||||
|
url: &String,
|
||||||
|
reconfigure_request: ComputeHookNotifyRequest,
|
||||||
|
cancel: &CancellationToken,
|
||||||
|
) -> Result<(), NotifyError> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
backoff::retry(
|
||||||
|
|| self.do_notify_iteration(&client, url, &reconfigure_request, cancel),
|
||||||
|
|e| matches!(e, NotifyError::Fatal(_) | NotifyError::Unexpected(_)),
|
||||||
|
3,
|
||||||
|
10,
|
||||||
|
"Send compute notification",
|
||||||
|
cancel,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| NotifyError::ShuttingDown)
|
||||||
|
.and_then(|x| x)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call this to notify the compute (postgres) tier of new pageservers to use
|
||||||
|
/// for a tenant. notify() is called by each shard individually, and this function
|
||||||
|
/// will decide whether an update to the tenant is sent. An update is sent on the
|
||||||
|
/// condition that:
|
||||||
|
/// - We know a pageserver for every shard.
|
||||||
|
/// - All the shards have the same shard_count (i.e. we are not mid-split)
|
||||||
|
///
|
||||||
|
/// Cancellation token enables callers to drop out, e.g. if calling from a Reconciler
|
||||||
|
/// that is cancelled.
|
||||||
|
///
|
||||||
|
/// This function is fallible, including in the case that the control plane is transiently
|
||||||
|
/// unavailable. A limited number of retries are done internally to efficiently hide short unavailability
|
||||||
|
/// periods, but we don't retry forever. The **caller** is responsible for handling failures and
|
||||||
|
/// ensuring that they eventually call again to ensure that the compute is eventually notified of
|
||||||
|
/// the proper pageserver nodes for a tenant.
|
||||||
|
#[tracing::instrument(skip_all, fields(tenant_id=%tenant_shard_id.tenant_id, shard_id=%tenant_shard_id.shard_slug(), node_id))]
|
||||||
|
pub(super) async fn notify(
|
||||||
|
&self,
|
||||||
|
tenant_shard_id: TenantShardId,
|
||||||
|
node_id: NodeId,
|
||||||
|
stripe_size: ShardStripeSize,
|
||||||
|
cancel: &CancellationToken,
|
||||||
|
) -> Result<(), NotifyError> {
|
||||||
|
let mut locked = self.state.lock().await;
|
||||||
|
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
let tenant = match locked.entry(tenant_shard_id.tenant_id) {
|
||||||
|
Entry::Vacant(e) => e.insert(ComputeHookTenant::new(
|
||||||
|
tenant_shard_id,
|
||||||
|
stripe_size,
|
||||||
|
node_id,
|
||||||
|
)),
|
||||||
|
Entry::Occupied(e) => {
|
||||||
|
let tenant = e.into_mut();
|
||||||
|
tenant.update(tenant_shard_id, stripe_size, node_id);
|
||||||
|
tenant
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let reconfigure_request = tenant.maybe_reconfigure(tenant_shard_id.tenant_id);
|
||||||
|
let Some(reconfigure_request) = reconfigure_request else {
|
||||||
|
// The tenant doesn't yet have pageservers for all its shards: we won't notify anything
|
||||||
|
// until it does.
|
||||||
|
tracing::info!("Tenant isn't yet ready to emit a notification");
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(notify_url) = &self.config.compute_hook_url {
|
||||||
|
self.do_notify(notify_url, reconfigure_request, cancel)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
self.do_notify_local(reconfigure_request)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
// This path is for testing only, so munge the error into our prod-style error type.
|
||||||
|
tracing::error!("Local notification hook failed: {e}");
|
||||||
|
NotifyError::Fatal(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod tests {
|
||||||
|
use pageserver_api::shard::{ShardCount, ShardNumber};
|
||||||
|
use utils::id::TenantId;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tenant_updates() -> anyhow::Result<()> {
|
||||||
|
let tenant_id = TenantId::generate();
|
||||||
|
let mut tenant_state = ComputeHookTenant::new(
|
||||||
|
TenantShardId {
|
||||||
|
tenant_id,
|
||||||
|
shard_count: ShardCount::new(0),
|
||||||
|
shard_number: ShardNumber(0),
|
||||||
|
},
|
||||||
|
ShardStripeSize(12345),
|
||||||
|
NodeId(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
// An unsharded tenant is always ready to emit a notification
|
||||||
|
assert!(tenant_state.maybe_reconfigure(tenant_id).is_some());
|
||||||
|
assert_eq!(
|
||||||
|
tenant_state
|
||||||
|
.maybe_reconfigure(tenant_id)
|
||||||
|
.unwrap()
|
||||||
|
.shards
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert!(tenant_state
|
||||||
|
.maybe_reconfigure(tenant_id)
|
||||||
|
.unwrap()
|
||||||
|
.stripe_size
|
||||||
|
.is_none());
|
||||||
|
|
||||||
|
// Writing the first shard of a multi-sharded situation (i.e. in a split)
|
||||||
|
// resets the tenant state and puts it in an non-notifying state (need to
|
||||||
|
// see all shards)
|
||||||
|
tenant_state.update(
|
||||||
|
TenantShardId {
|
||||||
|
tenant_id,
|
||||||
|
shard_count: ShardCount::new(2),
|
||||||
|
shard_number: ShardNumber(1),
|
||||||
|
},
|
||||||
|
ShardStripeSize(32768),
|
||||||
|
NodeId(1),
|
||||||
|
);
|
||||||
|
assert!(tenant_state.maybe_reconfigure(tenant_id).is_none());
|
||||||
|
|
||||||
|
// Writing the second shard makes it ready to notify
|
||||||
|
tenant_state.update(
|
||||||
|
TenantShardId {
|
||||||
|
tenant_id,
|
||||||
|
shard_count: ShardCount::new(2),
|
||||||
|
shard_number: ShardNumber(0),
|
||||||
|
},
|
||||||
|
ShardStripeSize(32768),
|
||||||
|
NodeId(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(tenant_state.maybe_reconfigure(tenant_id).is_some());
|
||||||
|
assert_eq!(
|
||||||
|
tenant_state
|
||||||
|
.maybe_reconfigure(tenant_id)
|
||||||
|
.unwrap()
|
||||||
|
.shards
|
||||||
|
.len(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tenant_state
|
||||||
|
.maybe_reconfigure(tenant_id)
|
||||||
|
.unwrap()
|
||||||
|
.stripe_size,
|
||||||
|
Some(ShardStripeSize(32768))
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
-17
@@ -31,7 +31,6 @@ pub(crate) enum PageserverState {
|
|||||||
Available {
|
Available {
|
||||||
last_seen_at: Instant,
|
last_seen_at: Instant,
|
||||||
utilization: PageserverUtilization,
|
utilization: PageserverUtilization,
|
||||||
new: bool,
|
|
||||||
},
|
},
|
||||||
Offline,
|
Offline,
|
||||||
}
|
}
|
||||||
@@ -128,7 +127,6 @@ impl HeartbeaterTask {
|
|||||||
heartbeat_futs.push({
|
heartbeat_futs.push({
|
||||||
let jwt_token = self.jwt_token.clone();
|
let jwt_token = self.jwt_token.clone();
|
||||||
let cancel = self.cancel.clone();
|
let cancel = self.cancel.clone();
|
||||||
let new_node = !self.state.contains_key(node_id);
|
|
||||||
|
|
||||||
// Clone the node and mark it as available such that the request
|
// Clone the node and mark it as available such that the request
|
||||||
// goes through to the pageserver even when the node is marked offline.
|
// goes through to the pageserver even when the node is marked offline.
|
||||||
@@ -161,7 +159,6 @@ impl HeartbeaterTask {
|
|||||||
PageserverState::Available {
|
PageserverState::Available {
|
||||||
last_seen_at: Instant::now(),
|
last_seen_at: Instant::now(),
|
||||||
utilization,
|
utilization,
|
||||||
new: new_node,
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
PageserverState::Offline
|
PageserverState::Offline
|
||||||
@@ -187,19 +184,6 @@ impl HeartbeaterTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tracing::info!(
|
|
||||||
"Heartbeat round complete for {} nodes, {} offline",
|
|
||||||
new_state.len(),
|
|
||||||
new_state
|
|
||||||
.values()
|
|
||||||
.filter(|s| match s {
|
|
||||||
PageserverState::Available { .. } => {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
PageserverState::Offline => true,
|
|
||||||
})
|
|
||||||
.count()
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut deltas = Vec::new();
|
let mut deltas = Vec::new();
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
@@ -223,7 +207,6 @@ impl HeartbeaterTask {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Vacant(_) => {
|
Vacant(_) => {
|
||||||
// This is a new node. Don't generate a delta for it.
|
|
||||||
deltas.push((node_id, ps_state.clone()));
|
deltas.push((node_id, ps_state.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,12 +4,10 @@ use crate::metrics::{
|
|||||||
};
|
};
|
||||||
use crate::reconciler::ReconcileError;
|
use crate::reconciler::ReconcileError;
|
||||||
use crate::service::{Service, STARTUP_RECONCILE_TIMEOUT};
|
use crate::service::{Service, STARTUP_RECONCILE_TIMEOUT};
|
||||||
use anyhow::Context;
|
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use hyper::header::CONTENT_TYPE;
|
use hyper::header::CONTENT_TYPE;
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
use hyper::{StatusCode, Uri};
|
use hyper::{StatusCode, Uri};
|
||||||
use metrics::{BuildInfo, NeonMetrics};
|
|
||||||
use pageserver_api::models::{
|
use pageserver_api::models::{
|
||||||
TenantConfigRequest, TenantCreateRequest, TenantLocationConfigRequest, TenantShardSplitRequest,
|
TenantConfigRequest, TenantCreateRequest, TenantLocationConfigRequest, TenantShardSplitRequest,
|
||||||
TenantTimeTravelRequest, TimelineCreateRequest,
|
TenantTimeTravelRequest, TimelineCreateRequest,
|
||||||
@@ -36,8 +34,7 @@ use utils::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use pageserver_api::controller_api::{
|
use pageserver_api::controller_api::{
|
||||||
NodeAvailability, NodeConfigureRequest, NodeRegisterRequest, TenantPolicyRequest,
|
NodeAvailability, NodeConfigureRequest, NodeRegisterRequest, TenantShardMigrateRequest,
|
||||||
TenantShardMigrateRequest,
|
|
||||||
};
|
};
|
||||||
use pageserver_api::upcall_api::{ReAttachRequest, ValidateRequest};
|
use pageserver_api::upcall_api::{ReAttachRequest, ValidateRequest};
|
||||||
|
|
||||||
@@ -46,19 +43,15 @@ use control_plane::storage_controller::{AttachHookRequest, InspectRequest};
|
|||||||
use routerify::Middleware;
|
use routerify::Middleware;
|
||||||
|
|
||||||
/// State available to HTTP request handlers
|
/// State available to HTTP request handlers
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct HttpState {
|
pub struct HttpState {
|
||||||
service: Arc<crate::service::Service>,
|
service: Arc<crate::service::Service>,
|
||||||
auth: Option<Arc<SwappableJwtAuth>>,
|
auth: Option<Arc<SwappableJwtAuth>>,
|
||||||
neon_metrics: NeonMetrics,
|
|
||||||
allowlist_routes: Vec<Uri>,
|
allowlist_routes: Vec<Uri>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HttpState {
|
impl HttpState {
|
||||||
pub fn new(
|
pub fn new(service: Arc<crate::service::Service>, auth: Option<Arc<SwappableJwtAuth>>) -> Self {
|
||||||
service: Arc<crate::service::Service>,
|
|
||||||
auth: Option<Arc<SwappableJwtAuth>>,
|
|
||||||
build_info: BuildInfo,
|
|
||||||
) -> Self {
|
|
||||||
let allowlist_routes = ["/status", "/ready", "/metrics"]
|
let allowlist_routes = ["/status", "/ready", "/metrics"]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|v| v.parse().unwrap())
|
.map(|v| v.parse().unwrap())
|
||||||
@@ -66,7 +59,6 @@ impl HttpState {
|
|||||||
Self {
|
Self {
|
||||||
service,
|
service,
|
||||||
auth,
|
auth,
|
||||||
neon_metrics: NeonMetrics::new(build_info),
|
|
||||||
allowlist_routes,
|
allowlist_routes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,6 +134,52 @@ async fn handle_tenant_create(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For tenant and timeline deletions, which both implement an "initially return 202, then 404 once
|
||||||
|
// we're done" semantic, we wrap with a retry loop to expose a simpler API upstream. This avoids
|
||||||
|
// needing to track a "deleting" state for tenants.
|
||||||
|
async fn deletion_wrapper<R, F>(service: Arc<Service>, f: F) -> Result<Response<Body>, ApiError>
|
||||||
|
where
|
||||||
|
R: std::future::Future<Output = Result<StatusCode, ApiError>> + Send + 'static,
|
||||||
|
F: Fn(Arc<Service>) -> R + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let started_at = Instant::now();
|
||||||
|
// To keep deletion reasonably snappy for small tenants, initially check after 1 second if deletion
|
||||||
|
// completed.
|
||||||
|
let mut retry_period = Duration::from_secs(1);
|
||||||
|
// On subsequent retries, wait longer.
|
||||||
|
let max_retry_period = Duration::from_secs(5);
|
||||||
|
// Enable callers with a 30 second request timeout to reliably get a response
|
||||||
|
let max_wait = Duration::from_secs(25);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let status = f(service.clone()).await?;
|
||||||
|
match status {
|
||||||
|
StatusCode::ACCEPTED => {
|
||||||
|
tracing::info!("Deletion accepted, waiting to try again...");
|
||||||
|
tokio::time::sleep(retry_period).await;
|
||||||
|
retry_period = max_retry_period;
|
||||||
|
}
|
||||||
|
StatusCode::NOT_FOUND => {
|
||||||
|
tracing::info!("Deletion complete");
|
||||||
|
return json_response(StatusCode::OK, ());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
tracing::warn!("Unexpected status {status}");
|
||||||
|
return json_response(status, ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
if now + retry_period > started_at + max_wait {
|
||||||
|
tracing::info!("Deletion timed out waiting for 404");
|
||||||
|
// REQUEST_TIMEOUT would be more appropriate, but CONFLICT is already part of
|
||||||
|
// the pageserver's swagger definition for this endpoint, and has the same desired
|
||||||
|
// effect of causing the control plane to retry later.
|
||||||
|
return json_response(StatusCode::CONFLICT, ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_tenant_location_config(
|
async fn handle_tenant_location_config(
|
||||||
service: Arc<Service>,
|
service: Arc<Service>,
|
||||||
mut req: Request<Body>,
|
mut req: Request<Body>,
|
||||||
@@ -213,12 +251,6 @@ async fn handle_tenant_time_travel_remote_storage(
|
|||||||
json_response(StatusCode::OK, ())
|
json_response(StatusCode::OK, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_reqwest_hyper_status(status: reqwest::StatusCode) -> Result<hyper::StatusCode, ApiError> {
|
|
||||||
hyper::StatusCode::from_u16(status.as_u16())
|
|
||||||
.context("invalid status code")
|
|
||||||
.map_err(ApiError::InternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_tenant_secondary_download(
|
async fn handle_tenant_secondary_download(
|
||||||
service: Arc<Service>,
|
service: Arc<Service>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
@@ -227,7 +259,7 @@ async fn handle_tenant_secondary_download(
|
|||||||
let wait = parse_query_param(&req, "wait_ms")?.map(Duration::from_millis);
|
let wait = parse_query_param(&req, "wait_ms")?.map(Duration::from_millis);
|
||||||
|
|
||||||
let (status, progress) = service.tenant_secondary_download(tenant_id, wait).await?;
|
let (status, progress) = service.tenant_secondary_download(tenant_id, wait).await?;
|
||||||
json_response(map_reqwest_hyper_status(status)?, progress)
|
json_response(status, progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_tenant_delete(
|
async fn handle_tenant_delete(
|
||||||
@@ -237,17 +269,10 @@ async fn handle_tenant_delete(
|
|||||||
let tenant_id: TenantId = parse_request_param(&req, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&req, "tenant_id")?;
|
||||||
check_permissions(&req, Scope::PageServerApi)?;
|
check_permissions(&req, Scope::PageServerApi)?;
|
||||||
|
|
||||||
let status_code = service
|
deletion_wrapper(service, move |service| async move {
|
||||||
.tenant_delete(tenant_id)
|
service.tenant_delete(tenant_id).await
|
||||||
.await
|
})
|
||||||
.and_then(map_reqwest_hyper_status)?;
|
.await
|
||||||
|
|
||||||
if status_code == StatusCode::NOT_FOUND {
|
|
||||||
// The pageserver uses 404 for successful deletion, but we use 200
|
|
||||||
json_response(StatusCode::OK, ())
|
|
||||||
} else {
|
|
||||||
json_response(status_code, ())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_tenant_timeline_create(
|
async fn handle_tenant_timeline_create(
|
||||||
@@ -275,56 +300,8 @@ async fn handle_tenant_timeline_delete(
|
|||||||
|
|
||||||
let timeline_id: TimelineId = parse_request_param(&req, "timeline_id")?;
|
let timeline_id: TimelineId = parse_request_param(&req, "timeline_id")?;
|
||||||
|
|
||||||
// For timeline deletions, which both implement an "initially return 202, then 404 once
|
|
||||||
// we're done" semantic, we wrap with a retry loop to expose a simpler API upstream.
|
|
||||||
async fn deletion_wrapper<R, F>(service: Arc<Service>, f: F) -> Result<Response<Body>, ApiError>
|
|
||||||
where
|
|
||||||
R: std::future::Future<Output = Result<StatusCode, ApiError>> + Send + 'static,
|
|
||||||
F: Fn(Arc<Service>) -> R + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
let started_at = Instant::now();
|
|
||||||
// To keep deletion reasonably snappy for small tenants, initially check after 1 second if deletion
|
|
||||||
// completed.
|
|
||||||
let mut retry_period = Duration::from_secs(1);
|
|
||||||
// On subsequent retries, wait longer.
|
|
||||||
let max_retry_period = Duration::from_secs(5);
|
|
||||||
// Enable callers with a 30 second request timeout to reliably get a response
|
|
||||||
let max_wait = Duration::from_secs(25);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let status = f(service.clone()).await?;
|
|
||||||
match status {
|
|
||||||
StatusCode::ACCEPTED => {
|
|
||||||
tracing::info!("Deletion accepted, waiting to try again...");
|
|
||||||
tokio::time::sleep(retry_period).await;
|
|
||||||
retry_period = max_retry_period;
|
|
||||||
}
|
|
||||||
StatusCode::NOT_FOUND => {
|
|
||||||
tracing::info!("Deletion complete");
|
|
||||||
return json_response(StatusCode::OK, ());
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
tracing::warn!("Unexpected status {status}");
|
|
||||||
return json_response(status, ());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let now = Instant::now();
|
|
||||||
if now + retry_period > started_at + max_wait {
|
|
||||||
tracing::info!("Deletion timed out waiting for 404");
|
|
||||||
// REQUEST_TIMEOUT would be more appropriate, but CONFLICT is already part of
|
|
||||||
// the pageserver's swagger definition for this endpoint, and has the same desired
|
|
||||||
// effect of causing the control plane to retry later.
|
|
||||||
return json_response(StatusCode::CONFLICT, ());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deletion_wrapper(service, move |service| async move {
|
deletion_wrapper(service, move |service| async move {
|
||||||
service
|
service.tenant_timeline_delete(tenant_id, timeline_id).await
|
||||||
.tenant_timeline_delete(tenant_id, timeline_id)
|
|
||||||
.await
|
|
||||||
.and_then(map_reqwest_hyper_status)
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -387,9 +364,11 @@ async fn handle_tenant_timeline_passthrough(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We have a reqest::Response, would like a http::Response
|
// We have a reqest::Response, would like a http::Response
|
||||||
let mut builder = hyper::Response::builder().status(map_reqwest_hyper_status(resp.status())?);
|
let mut builder = hyper::Response::builder()
|
||||||
|
.status(resp.status())
|
||||||
|
.version(resp.version());
|
||||||
for (k, v) in resp.headers() {
|
for (k, v) in resp.headers() {
|
||||||
builder = builder.header(k.as_str(), v.as_bytes());
|
builder = builder.header(k, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = builder
|
let response = builder
|
||||||
@@ -419,15 +398,6 @@ async fn handle_tenant_describe(
|
|||||||
json_response(StatusCode::OK, service.tenant_describe(tenant_id)?)
|
json_response(StatusCode::OK, service.tenant_describe(tenant_id)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_tenant_list(
|
|
||||||
service: Arc<Service>,
|
|
||||||
req: Request<Body>,
|
|
||||||
) -> Result<Response<Body>, ApiError> {
|
|
||||||
check_permissions(&req, Scope::Admin)?;
|
|
||||||
|
|
||||||
json_response(StatusCode::OK, service.tenant_list())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_node_register(mut req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn handle_node_register(mut req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
||||||
check_permissions(&req, Scope::Admin)?;
|
check_permissions(&req, Scope::Admin)?;
|
||||||
|
|
||||||
@@ -441,10 +411,7 @@ async fn handle_node_list(req: Request<Body>) -> Result<Response<Body>, ApiError
|
|||||||
check_permissions(&req, Scope::Admin)?;
|
check_permissions(&req, Scope::Admin)?;
|
||||||
|
|
||||||
let state = get_state(&req);
|
let state = get_state(&req);
|
||||||
let nodes = state.service.node_list().await?;
|
json_response(StatusCode::OK, state.service.node_list().await?)
|
||||||
let api_nodes = nodes.into_iter().map(|n| n.describe()).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
json_response(StatusCode::OK, api_nodes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_node_drop(req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn handle_node_drop(req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
||||||
@@ -480,39 +447,6 @@ async fn handle_node_configure(mut req: Request<Body>) -> Result<Response<Body>,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_node_status(req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
|
||||||
check_permissions(&req, Scope::Admin)?;
|
|
||||||
|
|
||||||
let state = get_state(&req);
|
|
||||||
let node_id: NodeId = parse_request_param(&req, "node_id")?;
|
|
||||||
|
|
||||||
let node_status = state.service.get_node(node_id).await?;
|
|
||||||
|
|
||||||
json_response(StatusCode::OK, node_status)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_node_drain(req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
|
||||||
check_permissions(&req, Scope::Admin)?;
|
|
||||||
|
|
||||||
let state = get_state(&req);
|
|
||||||
let node_id: NodeId = parse_request_param(&req, "node_id")?;
|
|
||||||
|
|
||||||
state.service.start_node_drain(node_id).await?;
|
|
||||||
|
|
||||||
json_response(StatusCode::ACCEPTED, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_node_fill(req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
|
||||||
check_permissions(&req, Scope::Admin)?;
|
|
||||||
|
|
||||||
let state = get_state(&req);
|
|
||||||
let node_id: NodeId = parse_request_param(&req, "node_id")?;
|
|
||||||
|
|
||||||
state.service.start_node_fill(node_id).await?;
|
|
||||||
|
|
||||||
json_response(StatusCode::ACCEPTED, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_tenant_shard_split(
|
async fn handle_tenant_shard_split(
|
||||||
service: Arc<Service>,
|
service: Arc<Service>,
|
||||||
mut req: Request<Body>,
|
mut req: Request<Body>,
|
||||||
@@ -544,22 +478,6 @@ async fn handle_tenant_shard_migrate(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_tenant_update_policy(mut req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
|
||||||
check_permissions(&req, Scope::Admin)?;
|
|
||||||
|
|
||||||
let tenant_id: TenantId = parse_request_param(&req, "tenant_id")?;
|
|
||||||
let update_req = json_request::<TenantPolicyRequest>(&mut req).await?;
|
|
||||||
let state = get_state(&req);
|
|
||||||
|
|
||||||
json_response(
|
|
||||||
StatusCode::OK,
|
|
||||||
state
|
|
||||||
.service
|
|
||||||
.tenant_update_policy(tenant_id, update_req)
|
|
||||||
.await?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_tenant_drop(req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn handle_tenant_drop(req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
||||||
let tenant_id: TenantId = parse_request_param(&req, "tenant_id")?;
|
let tenant_id: TenantId = parse_request_param(&req, "tenant_id")?;
|
||||||
check_permissions(&req, Scope::PageServerApi)?;
|
check_permissions(&req, Scope::PageServerApi)?;
|
||||||
@@ -569,18 +487,6 @@ async fn handle_tenant_drop(req: Request<Body>) -> Result<Response<Body>, ApiErr
|
|||||||
json_response(StatusCode::OK, state.service.tenant_drop(tenant_id).await?)
|
json_response(StatusCode::OK, state.service.tenant_drop(tenant_id).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_tenant_import(req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
|
||||||
let tenant_id: TenantId = parse_request_param(&req, "tenant_id")?;
|
|
||||||
check_permissions(&req, Scope::PageServerApi)?;
|
|
||||||
|
|
||||||
let state = get_state(&req);
|
|
||||||
|
|
||||||
json_response(
|
|
||||||
StatusCode::OK,
|
|
||||||
state.service.tenant_import(tenant_id).await?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_tenants_dump(req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn handle_tenants_dump(req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
||||||
check_permissions(&req, Scope::Admin)?;
|
check_permissions(&req, Scope::Admin)?;
|
||||||
|
|
||||||
@@ -603,14 +509,6 @@ async fn handle_consistency_check(req: Request<Body>) -> Result<Response<Body>,
|
|||||||
json_response(StatusCode::OK, state.service.consistency_check().await?)
|
json_response(StatusCode::OK, state.service.consistency_check().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_reconcile_all(req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
|
||||||
check_permissions(&req, Scope::Admin)?;
|
|
||||||
|
|
||||||
let state = get_state(&req);
|
|
||||||
|
|
||||||
json_response(StatusCode::OK, state.service.reconcile_all_now().await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Status endpoint is just used for checking that our HTTP listener is up
|
/// Status endpoint is just used for checking that our HTTP listener is up
|
||||||
async fn handle_status(_req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn handle_status(_req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
||||||
json_response(StatusCode::OK, ())
|
json_response(StatusCode::OK, ())
|
||||||
@@ -667,17 +565,9 @@ where
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the required scope is held in the request's token, or if the request has
|
|
||||||
/// a token with 'admin' scope then always permit it.
|
|
||||||
fn check_permissions(request: &Request<Body>, required_scope: Scope) -> Result<(), ApiError> {
|
fn check_permissions(request: &Request<Body>, required_scope: Scope) -> Result<(), ApiError> {
|
||||||
check_permission_with(request, |claims| {
|
check_permission_with(request, |claims| {
|
||||||
match crate::auth::check_permission(claims, required_scope) {
|
crate::auth::check_permission(claims, required_scope)
|
||||||
Err(e) => match crate::auth::check_permission(claims, Scope::Admin) {
|
|
||||||
Ok(()) => Ok(()),
|
|
||||||
Err(_) => Err(e),
|
|
||||||
},
|
|
||||||
Ok(()) => Ok(()),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -737,11 +627,10 @@ fn epilogue_metrics_middleware<B: hyper::body::HttpBody + Send + Sync + 'static>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn measured_metrics_handler(req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
pub async fn measured_metrics_handler(_req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
||||||
pub const TEXT_FORMAT: &str = "text/plain; version=0.0.4";
|
pub const TEXT_FORMAT: &str = "text/plain; version=0.0.4";
|
||||||
|
|
||||||
let state = get_state(&req);
|
let payload = crate::metrics::METRICS_REGISTRY.encode();
|
||||||
let payload = crate::metrics::METRICS_REGISTRY.encode(&state.neon_metrics);
|
|
||||||
let response = Response::builder()
|
let response = Response::builder()
|
||||||
.status(200)
|
.status(200)
|
||||||
.header(CONTENT_TYPE, TEXT_FORMAT)
|
.header(CONTENT_TYPE, TEXT_FORMAT)
|
||||||
@@ -770,7 +659,6 @@ where
|
|||||||
pub fn make_router(
|
pub fn make_router(
|
||||||
service: Arc<Service>,
|
service: Arc<Service>,
|
||||||
auth: Option<Arc<SwappableJwtAuth>>,
|
auth: Option<Arc<SwappableJwtAuth>>,
|
||||||
build_info: BuildInfo,
|
|
||||||
) -> RouterBuilder<hyper::Body, ApiError> {
|
) -> RouterBuilder<hyper::Body, ApiError> {
|
||||||
let mut router = endpoint::make_router()
|
let mut router = endpoint::make_router()
|
||||||
.middleware(prologue_metrics_middleware())
|
.middleware(prologue_metrics_middleware())
|
||||||
@@ -787,7 +675,7 @@ pub fn make_router(
|
|||||||
}
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
.data(Arc::new(HttpState::new(service, auth, build_info)))
|
.data(Arc::new(HttpState::new(service, auth)))
|
||||||
.get("/metrics", |r| {
|
.get("/metrics", |r| {
|
||||||
named_request_span(r, measured_metrics_handler, RequestName("metrics"))
|
named_request_span(r, measured_metrics_handler, RequestName("metrics"))
|
||||||
})
|
})
|
||||||
@@ -818,13 +706,6 @@ pub fn make_router(
|
|||||||
.post("/debug/v1/node/:node_id/drop", |r| {
|
.post("/debug/v1/node/:node_id/drop", |r| {
|
||||||
named_request_span(r, handle_node_drop, RequestName("debug_v1_node_drop"))
|
named_request_span(r, handle_node_drop, RequestName("debug_v1_node_drop"))
|
||||||
})
|
})
|
||||||
.post("/debug/v1/tenant/:tenant_id/import", |r| {
|
|
||||||
named_request_span(
|
|
||||||
r,
|
|
||||||
handle_tenant_import,
|
|
||||||
RequestName("debug_v1_tenant_import"),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.get("/debug/v1/tenant", |r| {
|
.get("/debug/v1/tenant", |r| {
|
||||||
named_request_span(r, handle_tenants_dump, RequestName("debug_v1_tenant"))
|
named_request_span(r, handle_tenants_dump, RequestName("debug_v1_tenant"))
|
||||||
})
|
})
|
||||||
@@ -845,9 +726,6 @@ pub fn make_router(
|
|||||||
RequestName("debug_v1_consistency_check"),
|
RequestName("debug_v1_consistency_check"),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.post("/debug/v1/reconcile_all", |r| {
|
|
||||||
request_span(r, handle_reconcile_all)
|
|
||||||
})
|
|
||||||
.put("/debug/v1/failpoints", |r| {
|
.put("/debug/v1/failpoints", |r| {
|
||||||
request_span(r, |r| failpoints_handler(r, CancellationToken::new()))
|
request_span(r, |r| failpoints_handler(r, CancellationToken::new()))
|
||||||
})
|
})
|
||||||
@@ -865,16 +743,6 @@ pub fn make_router(
|
|||||||
RequestName("control_v1_node_config"),
|
RequestName("control_v1_node_config"),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.get("/control/v1/node/:node_id", |r| {
|
|
||||||
named_request_span(r, handle_node_status, RequestName("control_v1_node_status"))
|
|
||||||
})
|
|
||||||
.put("/control/v1/node/:node_id/drain", |r| {
|
|
||||||
named_request_span(r, handle_node_drain, RequestName("control_v1_node_drain"))
|
|
||||||
})
|
|
||||||
.put("/control/v1/node/:node_id/fill", |r| {
|
|
||||||
named_request_span(r, handle_node_fill, RequestName("control_v1_node_fill"))
|
|
||||||
})
|
|
||||||
// TODO(vlad): endpoint for cancelling drain and fill
|
|
||||||
// Tenant Shard operations
|
// Tenant Shard operations
|
||||||
.put("/control/v1/tenant/:tenant_shard_id/migrate", |r| {
|
.put("/control/v1/tenant/:tenant_shard_id/migrate", |r| {
|
||||||
tenant_service_handler(
|
tenant_service_handler(
|
||||||
@@ -897,16 +765,6 @@ pub fn make_router(
|
|||||||
RequestName("control_v1_tenant_describe"),
|
RequestName("control_v1_tenant_describe"),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.get("/control/v1/tenant", |r| {
|
|
||||||
tenant_service_handler(r, handle_tenant_list, RequestName("control_v1_tenant_list"))
|
|
||||||
})
|
|
||||||
.put("/control/v1/tenant/:tenant_id/policy", |r| {
|
|
||||||
named_request_span(
|
|
||||||
r,
|
|
||||||
handle_tenant_update_policy,
|
|
||||||
RequestName("control_v1_tenant_policy"),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
// Tenant operations
|
// Tenant operations
|
||||||
// The ^/v1/ endpoints act as a "Virtual Pageserver", enabling shard-naive clients to call into
|
// The ^/v1/ endpoints act as a "Virtual Pageserver", enabling shard-naive clients to call into
|
||||||
// this service to manage tenants that actually consist of many tenant shards, as if they are a single entity.
|
// this service to manage tenants that actually consist of many tenant shards, as if they are a single entity.
|
||||||
@@ -958,7 +816,7 @@ pub fn make_router(
|
|||||||
RequestName("v1_tenant_timeline"),
|
RequestName("v1_tenant_timeline"),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
// Tenant detail GET passthrough to shard zero:
|
// Tenant detail GET passthrough to shard zero
|
||||||
.get("/v1/tenant/:tenant_id", |r| {
|
.get("/v1/tenant/:tenant_id", |r| {
|
||||||
tenant_service_handler(
|
tenant_service_handler(
|
||||||
r,
|
r,
|
||||||
@@ -966,14 +824,13 @@ pub fn make_router(
|
|||||||
RequestName("v1_tenant_passthrough"),
|
RequestName("v1_tenant_passthrough"),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
// The `*` in the URL is a wildcard: any tenant/timeline GET APIs on the pageserver
|
// Timeline GET passthrough to shard zero. Note that the `*` in the URL is a wildcard: any future
|
||||||
// are implicitly exposed here. This must be last in the list to avoid
|
// timeline GET APIs will be implicitly included.
|
||||||
// taking precedence over other GET methods we might implement by hand.
|
.get("/v1/tenant/:tenant_id/timeline*", |r| {
|
||||||
.get("/v1/tenant/:tenant_id/*", |r| {
|
|
||||||
tenant_service_handler(
|
tenant_service_handler(
|
||||||
r,
|
r,
|
||||||
handle_tenant_timeline_passthrough,
|
handle_tenant_timeline_passthrough,
|
||||||
RequestName("v1_tenant_passthrough"),
|
RequestName("v1_tenant_timeline_passthrough"),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
/// A map of locks covering some arbitrary identifiers. Useful if you have a collection of objects but don't
|
||||||
|
/// want to embed a lock in each one, or if your locking granularity is different to your object granularity.
|
||||||
|
/// For example, used in the storage controller where the objects are tenant shards, but sometimes locking
|
||||||
|
/// is needed at a tenant-wide granularity.
|
||||||
|
pub(crate) struct IdLockMap<T>
|
||||||
|
where
|
||||||
|
T: Eq + PartialEq + std::hash::Hash,
|
||||||
|
{
|
||||||
|
/// A synchronous lock for getting/setting the async locks that our callers will wait on.
|
||||||
|
entities: std::sync::Mutex<std::collections::HashMap<T, Arc<tokio::sync::RwLock<()>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> IdLockMap<T>
|
||||||
|
where
|
||||||
|
T: Eq + PartialEq + std::hash::Hash,
|
||||||
|
{
|
||||||
|
pub(crate) fn shared(
|
||||||
|
&self,
|
||||||
|
key: T,
|
||||||
|
) -> impl std::future::Future<Output = tokio::sync::OwnedRwLockReadGuard<()>> {
|
||||||
|
let mut locked = self.entities.lock().unwrap();
|
||||||
|
let entry = locked.entry(key).or_default();
|
||||||
|
entry.clone().read_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn exclusive(
|
||||||
|
&self,
|
||||||
|
key: T,
|
||||||
|
) -> impl std::future::Future<Output = tokio::sync::OwnedRwLockWriteGuard<()>> {
|
||||||
|
let mut locked = self.entities.lock().unwrap();
|
||||||
|
let entry = locked.entry(key).or_default();
|
||||||
|
entry.clone().write_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rather than building a lock guard that re-takes the [`Self::entities`] lock, we just do
|
||||||
|
/// periodic housekeeping to avoid the map growing indefinitely
|
||||||
|
pub(crate) fn housekeeping(&self) {
|
||||||
|
let mut locked = self.entities.lock().unwrap();
|
||||||
|
locked.retain(|_k, lock| lock.try_write().is_err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for IdLockMap<T>
|
||||||
|
where
|
||||||
|
T: Eq + PartialEq + std::hash::Hash,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
entities: std::sync::Mutex::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ use serde::Serialize;
|
|||||||
use utils::seqwait::MonotonicCounter;
|
use utils::seqwait::MonotonicCounter;
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
mod background_node_operations;
|
|
||||||
mod compute_hook;
|
mod compute_hook;
|
||||||
mod heartbeater;
|
mod heartbeater;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
@@ -15,7 +14,7 @@ mod reconciler;
|
|||||||
mod scheduler;
|
mod scheduler;
|
||||||
mod schema;
|
mod schema;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
mod tenant_shard;
|
mod tenant_state;
|
||||||
|
|
||||||
#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Serialize)]
|
#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Serialize)]
|
||||||
struct Sequence(u64);
|
struct Sequence(u64);
|
||||||
@@ -1,23 +1,18 @@
|
|||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
|
use attachment_service::http::make_router;
|
||||||
|
use attachment_service::metrics::preinitialize_metrics;
|
||||||
|
use attachment_service::persistence::Persistence;
|
||||||
|
use attachment_service::service::{Config, Service, MAX_UNAVAILABLE_INTERVAL_DEFAULT};
|
||||||
use camino::Utf8PathBuf;
|
use camino::Utf8PathBuf;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use metrics::launch_timestamp::LaunchTimestamp;
|
use metrics::launch_timestamp::LaunchTimestamp;
|
||||||
use metrics::BuildInfo;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use storage_controller::http::make_router;
|
|
||||||
use storage_controller::metrics::preinitialize_metrics;
|
|
||||||
use storage_controller::persistence::Persistence;
|
|
||||||
use storage_controller::service::{
|
|
||||||
Config, Service, MAX_UNAVAILABLE_INTERVAL_DEFAULT, RECONCILER_CONCURRENCY_DEFAULT,
|
|
||||||
};
|
|
||||||
use tokio::signal::unix::SignalKind;
|
use tokio::signal::unix::SignalKind;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use utils::auth::{JwtAuth, SwappableJwtAuth};
|
use utils::auth::{JwtAuth, SwappableJwtAuth};
|
||||||
use utils::logging::{self, LogFormat};
|
use utils::logging::{self, LogFormat};
|
||||||
|
|
||||||
use utils::sentry_init::init_sentry;
|
|
||||||
use utils::{project_build_tag, project_git_version, tcp_listener};
|
use utils::{project_build_tag, project_git_version, tcp_listener};
|
||||||
|
|
||||||
project_git_version!(GIT_VERSION);
|
project_git_version!(GIT_VERSION);
|
||||||
@@ -55,7 +50,7 @@ struct Cli {
|
|||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
path: Option<Utf8PathBuf>,
|
path: Option<Utf8PathBuf>,
|
||||||
|
|
||||||
/// URL to connect to postgres, like postgresql://localhost:1234/storage_controller
|
/// URL to connect to postgres, like postgresql://localhost:1234/attachment_service
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
database_url: Option<String>,
|
database_url: Option<String>,
|
||||||
|
|
||||||
@@ -66,24 +61,6 @@ struct Cli {
|
|||||||
/// Grace period before marking unresponsive pageserver offline
|
/// Grace period before marking unresponsive pageserver offline
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
max_unavailable_interval: Option<humantime::Duration>,
|
max_unavailable_interval: Option<humantime::Duration>,
|
||||||
|
|
||||||
/// Size threshold for automatically splitting shards (disabled by default)
|
|
||||||
#[arg(long)]
|
|
||||||
split_threshold: Option<u64>,
|
|
||||||
|
|
||||||
/// Maximum number of reconcilers that may run in parallel
|
|
||||||
#[arg(long)]
|
|
||||||
reconciler_concurrency: Option<usize>,
|
|
||||||
|
|
||||||
/// How long to wait for the initial database connection to be available.
|
|
||||||
#[arg(long, default_value = "5s")]
|
|
||||||
db_connect_timeout: humantime::Duration,
|
|
||||||
|
|
||||||
/// `neon_local` sets this to the path of the neon_local repo dir.
|
|
||||||
/// Only relevant for testing.
|
|
||||||
// TODO: make `cfg(feature = "testing")`
|
|
||||||
#[arg(long)]
|
|
||||||
neon_local_repo_dir: Option<PathBuf>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum StrictMode {
|
enum StrictMode {
|
||||||
@@ -181,8 +158,6 @@ fn main() -> anyhow::Result<()> {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let _sentry_guard = init_sentry(Some(GIT_VERSION.into()), &[]);
|
|
||||||
|
|
||||||
tokio::runtime::Builder::new_current_thread()
|
tokio::runtime::Builder::new_current_thread()
|
||||||
// We use spawn_blocking for database operations, so require approximately
|
// We use spawn_blocking for database operations, so require approximately
|
||||||
// as many blocking threads as we will open database connections.
|
// as many blocking threads as we will open database connections.
|
||||||
@@ -214,11 +189,6 @@ async fn async_main() -> anyhow::Result<()> {
|
|||||||
args.listen
|
args.listen
|
||||||
);
|
);
|
||||||
|
|
||||||
let build_info = BuildInfo {
|
|
||||||
revision: GIT_VERSION,
|
|
||||||
build_tag: BUILD_TAG,
|
|
||||||
};
|
|
||||||
|
|
||||||
let strict_mode = if args.dev {
|
let strict_mode = if args.dev {
|
||||||
StrictMode::Dev
|
StrictMode::Dev
|
||||||
} else {
|
} else {
|
||||||
@@ -263,16 +233,9 @@ async fn async_main() -> anyhow::Result<()> {
|
|||||||
.max_unavailable_interval
|
.max_unavailable_interval
|
||||||
.map(humantime::Duration::into)
|
.map(humantime::Duration::into)
|
||||||
.unwrap_or(MAX_UNAVAILABLE_INTERVAL_DEFAULT),
|
.unwrap_or(MAX_UNAVAILABLE_INTERVAL_DEFAULT),
|
||||||
reconciler_concurrency: args
|
|
||||||
.reconciler_concurrency
|
|
||||||
.unwrap_or(RECONCILER_CONCURRENCY_DEFAULT),
|
|
||||||
split_threshold: args.split_threshold,
|
|
||||||
neon_local_repo_dir: args.neon_local_repo_dir,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// After loading secrets & config, but before starting anything else, apply database migrations
|
// After loading secrets & config, but before starting anything else, apply database migrations
|
||||||
Persistence::await_connection(&secrets.database_url, args.db_connect_timeout.into()).await?;
|
|
||||||
|
|
||||||
migration_run(&secrets.database_url)
|
migration_run(&secrets.database_url)
|
||||||
.await
|
.await
|
||||||
.context("Running database migrations")?;
|
.context("Running database migrations")?;
|
||||||
@@ -287,7 +250,7 @@ async fn async_main() -> anyhow::Result<()> {
|
|||||||
let auth = secrets
|
let auth = secrets
|
||||||
.public_key
|
.public_key
|
||||||
.map(|jwt_auth| Arc::new(SwappableJwtAuth::new(jwt_auth)));
|
.map(|jwt_auth| Arc::new(SwappableJwtAuth::new(jwt_auth)));
|
||||||
let router = make_router(service.clone(), auth, build_info)
|
let router = make_router(service.clone(), auth)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|err| anyhow!(err))?;
|
.map_err(|err| anyhow!(err))?;
|
||||||
let router_service = utils::http::RouterService::new(router).unwrap();
|
let router_service = utils::http::RouterService::new(router).unwrap();
|
||||||
+88
-36
@@ -8,8 +8,10 @@
|
|||||||
//! The rest of the code defines label group types and deals with converting outer types to labels.
|
//! The rest of the code defines label group types and deals with converting outer types to labels.
|
||||||
//!
|
//!
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use measured::{label::LabelValue, metric::histogram, FixedCardinalityLabel, MetricGroup};
|
use measured::{
|
||||||
use metrics::NeonMetrics;
|
label::{LabelValue, StaticLabelSet},
|
||||||
|
FixedCardinalityLabel, MetricGroup,
|
||||||
|
};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
@@ -24,28 +26,21 @@ pub fn preinitialize_metrics() {
|
|||||||
|
|
||||||
pub(crate) struct StorageControllerMetrics {
|
pub(crate) struct StorageControllerMetrics {
|
||||||
pub(crate) metrics_group: StorageControllerMetricGroup,
|
pub(crate) metrics_group: StorageControllerMetricGroup,
|
||||||
encoder: Mutex<measured::text::BufferedTextEncoder>,
|
encoder: Mutex<measured::text::TextEncoder>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(measured::MetricGroup)]
|
#[derive(measured::MetricGroup)]
|
||||||
#[metric(new())]
|
|
||||||
pub(crate) struct StorageControllerMetricGroup {
|
pub(crate) struct StorageControllerMetricGroup {
|
||||||
/// Count of how many times we spawn a reconcile task
|
/// Count of how many times we spawn a reconcile task
|
||||||
pub(crate) storage_controller_reconcile_spawn: measured::Counter,
|
pub(crate) storage_controller_reconcile_spawn: measured::Counter,
|
||||||
|
|
||||||
/// Reconciler tasks completed, broken down by success/failure/cancelled
|
/// Reconciler tasks completed, broken down by success/failure/cancelled
|
||||||
pub(crate) storage_controller_reconcile_complete:
|
pub(crate) storage_controller_reconcile_complete:
|
||||||
measured::CounterVec<ReconcileCompleteLabelGroupSet>,
|
measured::CounterVec<ReconcileCompleteLabelGroupSet>,
|
||||||
|
|
||||||
/// Count of how many times we make an optimization change to a tenant's scheduling
|
|
||||||
pub(crate) storage_controller_schedule_optimization: measured::Counter,
|
|
||||||
|
|
||||||
/// HTTP request status counters for handled requests
|
/// HTTP request status counters for handled requests
|
||||||
pub(crate) storage_controller_http_request_status:
|
pub(crate) storage_controller_http_request_status:
|
||||||
measured::CounterVec<HttpRequestStatusLabelGroupSet>,
|
measured::CounterVec<HttpRequestStatusLabelGroupSet>,
|
||||||
|
|
||||||
/// HTTP request handler latency across all status codes
|
/// HTTP request handler latency across all status codes
|
||||||
#[metric(metadata = histogram::Thresholds::exponential_buckets(0.1, 2.0))]
|
|
||||||
pub(crate) storage_controller_http_request_latency:
|
pub(crate) storage_controller_http_request_latency:
|
||||||
measured::HistogramVec<HttpRequestLatencyLabelGroupSet, 5>,
|
measured::HistogramVec<HttpRequestLatencyLabelGroupSet, 5>,
|
||||||
|
|
||||||
@@ -57,7 +52,6 @@ pub(crate) struct StorageControllerMetricGroup {
|
|||||||
/// Latency of HTTP requests to the pageserver, broken down by pageserver
|
/// Latency of HTTP requests to the pageserver, broken down by pageserver
|
||||||
/// node id, request name and method. This include both successful and unsuccessful
|
/// node id, request name and method. This include both successful and unsuccessful
|
||||||
/// requests.
|
/// requests.
|
||||||
#[metric(metadata = histogram::Thresholds::exponential_buckets(0.1, 2.0))]
|
|
||||||
pub(crate) storage_controller_pageserver_request_latency:
|
pub(crate) storage_controller_pageserver_request_latency:
|
||||||
measured::HistogramVec<PageserverRequestLabelGroupSet, 5>,
|
measured::HistogramVec<PageserverRequestLabelGroupSet, 5>,
|
||||||
|
|
||||||
@@ -69,7 +63,6 @@ pub(crate) struct StorageControllerMetricGroup {
|
|||||||
/// Latency of pass-through HTTP requests to the pageserver, broken down by pageserver
|
/// Latency of pass-through HTTP requests to the pageserver, broken down by pageserver
|
||||||
/// node id, request name and method. This include both successful and unsuccessful
|
/// node id, request name and method. This include both successful and unsuccessful
|
||||||
/// requests.
|
/// requests.
|
||||||
#[metric(metadata = histogram::Thresholds::exponential_buckets(0.1, 2.0))]
|
|
||||||
pub(crate) storage_controller_passthrough_request_latency:
|
pub(crate) storage_controller_passthrough_request_latency:
|
||||||
measured::HistogramVec<PageserverRequestLabelGroupSet, 5>,
|
measured::HistogramVec<PageserverRequestLabelGroupSet, 5>,
|
||||||
|
|
||||||
@@ -78,34 +71,75 @@ pub(crate) struct StorageControllerMetricGroup {
|
|||||||
measured::CounterVec<DatabaseQueryErrorLabelGroupSet>,
|
measured::CounterVec<DatabaseQueryErrorLabelGroupSet>,
|
||||||
|
|
||||||
/// Latency of database queries, broken down by operation.
|
/// Latency of database queries, broken down by operation.
|
||||||
#[metric(metadata = histogram::Thresholds::exponential_buckets(0.1, 2.0))]
|
|
||||||
pub(crate) storage_controller_database_query_latency:
|
pub(crate) storage_controller_database_query_latency:
|
||||||
measured::HistogramVec<DatabaseQueryLatencyLabelGroupSet, 5>,
|
measured::HistogramVec<DatabaseQueryLatencyLabelGroupSet, 5>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StorageControllerMetrics {
|
impl StorageControllerMetrics {
|
||||||
pub(crate) fn encode(&self, neon_metrics: &NeonMetrics) -> Bytes {
|
pub(crate) fn encode(&self) -> Bytes {
|
||||||
let mut encoder = self.encoder.lock().unwrap();
|
let mut encoder = self.encoder.lock().unwrap();
|
||||||
neon_metrics
|
self.metrics_group.collect_into(&mut *encoder);
|
||||||
.collect_group_into(&mut *encoder)
|
|
||||||
.unwrap_or_else(|infallible| match infallible {});
|
|
||||||
self.metrics_group
|
|
||||||
.collect_group_into(&mut *encoder)
|
|
||||||
.unwrap_or_else(|infallible| match infallible {});
|
|
||||||
encoder.finish()
|
encoder.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for StorageControllerMetrics {
|
impl Default for StorageControllerMetrics {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let mut metrics_group = StorageControllerMetricGroup::new();
|
|
||||||
metrics_group
|
|
||||||
.storage_controller_reconcile_complete
|
|
||||||
.init_all_dense();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
metrics_group,
|
metrics_group: StorageControllerMetricGroup::new(),
|
||||||
encoder: Mutex::new(measured::text::BufferedTextEncoder::new()),
|
encoder: Mutex::new(measured::text::TextEncoder::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StorageControllerMetricGroup {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
storage_controller_reconcile_spawn: measured::Counter::new(),
|
||||||
|
storage_controller_reconcile_complete: measured::CounterVec::new(
|
||||||
|
ReconcileCompleteLabelGroupSet {
|
||||||
|
status: StaticLabelSet::new(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
storage_controller_http_request_status: measured::CounterVec::new(
|
||||||
|
HttpRequestStatusLabelGroupSet {
|
||||||
|
path: lasso::ThreadedRodeo::new(),
|
||||||
|
method: StaticLabelSet::new(),
|
||||||
|
status: StaticLabelSet::new(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
storage_controller_http_request_latency: measured::HistogramVec::new(
|
||||||
|
measured::metric::histogram::Thresholds::exponential_buckets(0.1, 2.0),
|
||||||
|
),
|
||||||
|
storage_controller_pageserver_request_error: measured::CounterVec::new(
|
||||||
|
PageserverRequestLabelGroupSet {
|
||||||
|
pageserver_id: lasso::ThreadedRodeo::new(),
|
||||||
|
path: lasso::ThreadedRodeo::new(),
|
||||||
|
method: StaticLabelSet::new(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
storage_controller_pageserver_request_latency: measured::HistogramVec::new(
|
||||||
|
measured::metric::histogram::Thresholds::exponential_buckets(0.1, 2.0),
|
||||||
|
),
|
||||||
|
storage_controller_passthrough_request_error: measured::CounterVec::new(
|
||||||
|
PageserverRequestLabelGroupSet {
|
||||||
|
pageserver_id: lasso::ThreadedRodeo::new(),
|
||||||
|
path: lasso::ThreadedRodeo::new(),
|
||||||
|
method: StaticLabelSet::new(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
storage_controller_passthrough_request_latency: measured::HistogramVec::new(
|
||||||
|
measured::metric::histogram::Thresholds::exponential_buckets(0.1, 2.0),
|
||||||
|
),
|
||||||
|
storage_controller_database_query_error: measured::CounterVec::new(
|
||||||
|
DatabaseQueryErrorLabelGroupSet {
|
||||||
|
operation: StaticLabelSet::new(),
|
||||||
|
error_type: StaticLabelSet::new(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
storage_controller_database_query_latency: measured::HistogramVec::new(
|
||||||
|
measured::metric::histogram::Thresholds::exponential_buckets(0.1, 2.0),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,7 +153,7 @@ pub(crate) struct ReconcileCompleteLabelGroup {
|
|||||||
#[derive(measured::LabelGroup)]
|
#[derive(measured::LabelGroup)]
|
||||||
#[label(set = HttpRequestStatusLabelGroupSet)]
|
#[label(set = HttpRequestStatusLabelGroupSet)]
|
||||||
pub(crate) struct HttpRequestStatusLabelGroup<'a> {
|
pub(crate) struct HttpRequestStatusLabelGroup<'a> {
|
||||||
#[label(dynamic_with = lasso::ThreadedRodeo, default)]
|
#[label(dynamic_with = lasso::ThreadedRodeo)]
|
||||||
pub(crate) path: &'a str,
|
pub(crate) path: &'a str,
|
||||||
pub(crate) method: Method,
|
pub(crate) method: Method,
|
||||||
pub(crate) status: StatusCode,
|
pub(crate) status: StatusCode,
|
||||||
@@ -128,21 +162,40 @@ pub(crate) struct HttpRequestStatusLabelGroup<'a> {
|
|||||||
#[derive(measured::LabelGroup)]
|
#[derive(measured::LabelGroup)]
|
||||||
#[label(set = HttpRequestLatencyLabelGroupSet)]
|
#[label(set = HttpRequestLatencyLabelGroupSet)]
|
||||||
pub(crate) struct HttpRequestLatencyLabelGroup<'a> {
|
pub(crate) struct HttpRequestLatencyLabelGroup<'a> {
|
||||||
#[label(dynamic_with = lasso::ThreadedRodeo, default)]
|
#[label(dynamic_with = lasso::ThreadedRodeo)]
|
||||||
pub(crate) path: &'a str,
|
pub(crate) path: &'a str,
|
||||||
pub(crate) method: Method,
|
pub(crate) method: Method,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for HttpRequestLatencyLabelGroupSet {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
path: lasso::ThreadedRodeo::new(),
|
||||||
|
method: StaticLabelSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(measured::LabelGroup, Clone)]
|
#[derive(measured::LabelGroup, Clone)]
|
||||||
#[label(set = PageserverRequestLabelGroupSet)]
|
#[label(set = PageserverRequestLabelGroupSet)]
|
||||||
pub(crate) struct PageserverRequestLabelGroup<'a> {
|
pub(crate) struct PageserverRequestLabelGroup<'a> {
|
||||||
#[label(dynamic_with = lasso::ThreadedRodeo, default)]
|
#[label(dynamic_with = lasso::ThreadedRodeo)]
|
||||||
pub(crate) pageserver_id: &'a str,
|
pub(crate) pageserver_id: &'a str,
|
||||||
#[label(dynamic_with = lasso::ThreadedRodeo, default)]
|
#[label(dynamic_with = lasso::ThreadedRodeo)]
|
||||||
pub(crate) path: &'a str,
|
pub(crate) path: &'a str,
|
||||||
pub(crate) method: Method,
|
pub(crate) method: Method,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for PageserverRequestLabelGroupSet {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
pageserver_id: lasso::ThreadedRodeo::new(),
|
||||||
|
path: lasso::ThreadedRodeo::new(),
|
||||||
|
method: StaticLabelSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(measured::LabelGroup)]
|
#[derive(measured::LabelGroup)]
|
||||||
#[label(set = DatabaseQueryErrorLabelGroupSet)]
|
#[label(set = DatabaseQueryErrorLabelGroupSet)]
|
||||||
pub(crate) struct DatabaseQueryErrorLabelGroup {
|
pub(crate) struct DatabaseQueryErrorLabelGroup {
|
||||||
@@ -156,7 +209,7 @@ pub(crate) struct DatabaseQueryLatencyLabelGroup {
|
|||||||
pub(crate) operation: DatabaseOperation,
|
pub(crate) operation: DatabaseOperation,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FixedCardinalityLabel, Clone, Copy)]
|
#[derive(FixedCardinalityLabel)]
|
||||||
pub(crate) enum ReconcileOutcome {
|
pub(crate) enum ReconcileOutcome {
|
||||||
#[label(rename = "ok")]
|
#[label(rename = "ok")]
|
||||||
Success,
|
Success,
|
||||||
@@ -164,7 +217,7 @@ pub(crate) enum ReconcileOutcome {
|
|||||||
Cancel,
|
Cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FixedCardinalityLabel, Copy, Clone)]
|
#[derive(FixedCardinalityLabel, Clone)]
|
||||||
pub(crate) enum Method {
|
pub(crate) enum Method {
|
||||||
Get,
|
Get,
|
||||||
Put,
|
Put,
|
||||||
@@ -189,12 +242,11 @@ impl From<hyper::Method> for Method {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub(crate) struct StatusCode(pub(crate) hyper::http::StatusCode);
|
pub(crate) struct StatusCode(pub(crate) hyper::http::StatusCode);
|
||||||
|
|
||||||
impl LabelValue for StatusCode {
|
impl LabelValue for StatusCode {
|
||||||
fn visit<V: measured::label::LabelVisitor>(&self, v: V) -> V::Output {
|
fn visit<V: measured::label::LabelVisitor>(&self, v: V) -> V::Output {
|
||||||
v.write_int(self.0.as_u16() as i64)
|
v.write_int(self.0.as_u16() as u64)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +264,7 @@ impl FixedCardinalityLabel for StatusCode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FixedCardinalityLabel, Clone, Copy)]
|
#[derive(FixedCardinalityLabel)]
|
||||||
pub(crate) enum DatabaseErrorLabel {
|
pub(crate) enum DatabaseErrorLabel {
|
||||||
Query,
|
Query,
|
||||||
Connection,
|
Connection,
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
use std::{str::FromStr, time::Duration};
|
use std::{str::FromStr, time::Duration};
|
||||||
|
|
||||||
|
use hyper::StatusCode;
|
||||||
use pageserver_api::{
|
use pageserver_api::{
|
||||||
controller_api::{
|
controller_api::{
|
||||||
NodeAvailability, NodeDescribeResponse, NodeRegisterRequest, NodeSchedulingPolicy,
|
NodeAvailability, NodeRegisterRequest, NodeSchedulingPolicy, TenantLocateResponseShard,
|
||||||
TenantLocateResponseShard, UtilizationScore,
|
|
||||||
},
|
},
|
||||||
shard::TenantShardId,
|
shard::TenantShardId,
|
||||||
};
|
};
|
||||||
use pageserver_client::mgmt_api;
|
use pageserver_client::mgmt_api;
|
||||||
use reqwest::StatusCode;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use utils::{backoff, id::NodeId};
|
use utils::{backoff, id::NodeId};
|
||||||
@@ -59,10 +58,6 @@ impl Node {
|
|||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_scheduling(&self) -> NodeSchedulingPolicy {
|
|
||||||
self.scheduling
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn set_scheduling(&mut self, scheduling: NodeSchedulingPolicy) {
|
pub(crate) fn set_scheduling(&mut self, scheduling: NodeSchedulingPolicy) {
|
||||||
self.scheduling = scheduling
|
self.scheduling = scheduling
|
||||||
}
|
}
|
||||||
@@ -120,16 +115,6 @@ impl Node {
|
|||||||
match (self.availability, availability) {
|
match (self.availability, availability) {
|
||||||
(Offline, Active(_)) => ToActive,
|
(Offline, Active(_)) => ToActive,
|
||||||
(Active(_), Offline) => ToOffline,
|
(Active(_), Offline) => ToOffline,
|
||||||
// Consider the case when the storage controller handles the re-attach of a node
|
|
||||||
// before the heartbeats detect that the node is back online. We still need
|
|
||||||
// [`Service::node_configure`] to attempt reconciliations for shards with an
|
|
||||||
// unknown observed location.
|
|
||||||
// The unsavoury match arm below handles this situation.
|
|
||||||
(Active(lhs), Active(rhs))
|
|
||||||
if lhs == UtilizationScore::worst() && rhs < UtilizationScore::worst() =>
|
|
||||||
{
|
|
||||||
ToActive
|
|
||||||
}
|
|
||||||
_ => Unchanged,
|
_ => Unchanged,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +140,6 @@ impl Node {
|
|||||||
NodeSchedulingPolicy::Draining => MaySchedule::No,
|
NodeSchedulingPolicy::Draining => MaySchedule::No,
|
||||||
NodeSchedulingPolicy::Filling => MaySchedule::Yes(score),
|
NodeSchedulingPolicy::Filling => MaySchedule::Yes(score),
|
||||||
NodeSchedulingPolicy::Pause => MaySchedule::No,
|
NodeSchedulingPolicy::Pause => MaySchedule::No,
|
||||||
NodeSchedulingPolicy::PauseForRestart => MaySchedule::No,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +156,7 @@ impl Node {
|
|||||||
listen_http_port,
|
listen_http_port,
|
||||||
listen_pg_addr,
|
listen_pg_addr,
|
||||||
listen_pg_port,
|
listen_pg_port,
|
||||||
scheduling: NodeSchedulingPolicy::Active,
|
scheduling: NodeSchedulingPolicy::Filling,
|
||||||
availability: NodeAvailability::Offline,
|
availability: NodeAvailability::Offline,
|
||||||
cancel: CancellationToken::new(),
|
cancel: CancellationToken::new(),
|
||||||
}
|
}
|
||||||
@@ -272,19 +256,6 @@ impl Node {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the simplified API-friendly description of a node's state
|
|
||||||
pub(crate) fn describe(&self) -> NodeDescribeResponse {
|
|
||||||
NodeDescribeResponse {
|
|
||||||
id: self.id,
|
|
||||||
availability: self.availability.into(),
|
|
||||||
scheduling: self.scheduling,
|
|
||||||
listen_http_addr: self.listen_http_addr.clone(),
|
|
||||||
listen_http_port: self.listen_http_port,
|
|
||||||
listen_pg_addr: self.listen_pg_addr.clone(),
|
|
||||||
listen_pg_port: self.listen_pg_port,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Node {
|
impl std::fmt::Display for Node {
|
||||||
+2
-48
@@ -1,14 +1,13 @@
|
|||||||
use pageserver_api::{
|
use pageserver_api::{
|
||||||
models::{
|
models::{
|
||||||
LocationConfig, LocationConfigListResponse, PageserverUtilization, SecondaryProgress,
|
LocationConfig, LocationConfigListResponse, PageserverUtilization, SecondaryProgress,
|
||||||
TenantScanRemoteStorageResponse, TenantShardSplitRequest, TenantShardSplitResponse,
|
TenantShardSplitRequest, TenantShardSplitResponse, TimelineCreateRequest, TimelineInfo,
|
||||||
TimelineCreateRequest, TimelineInfo, TopTenantShardsRequest, TopTenantShardsResponse,
|
|
||||||
},
|
},
|
||||||
shard::TenantShardId,
|
shard::TenantShardId,
|
||||||
};
|
};
|
||||||
use pageserver_client::mgmt_api::{Client, Result};
|
use pageserver_client::mgmt_api::{Client, Result};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use utils::id::{NodeId, TenantId, TimelineId};
|
use utils::id::{NodeId, TimelineId};
|
||||||
|
|
||||||
/// Thin wrapper around [`pageserver_client::mgmt_api::Client`]. It allows the storage
|
/// Thin wrapper around [`pageserver_client::mgmt_api::Client`]. It allows the storage
|
||||||
/// controller to collect metrics in a non-intrusive manner.
|
/// controller to collect metrics in a non-intrusive manner.
|
||||||
@@ -89,18 +88,6 @@ impl PageserverClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn tenant_scan_remote_storage(
|
|
||||||
&self,
|
|
||||||
tenant_id: TenantId,
|
|
||||||
) -> Result<TenantScanRemoteStorageResponse> {
|
|
||||||
measured_request!(
|
|
||||||
"tenant_scan_remote_storage",
|
|
||||||
crate::metrics::Method::Get,
|
|
||||||
&self.node_id_label,
|
|
||||||
self.inner.tenant_scan_remote_storage(tenant_id).await
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn tenant_secondary_download(
|
pub(crate) async fn tenant_secondary_download(
|
||||||
&self,
|
&self,
|
||||||
tenant_id: TenantShardId,
|
tenant_id: TenantShardId,
|
||||||
@@ -114,27 +101,6 @@ impl PageserverClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn tenant_secondary_status(
|
|
||||||
&self,
|
|
||||||
tenant_shard_id: TenantShardId,
|
|
||||||
) -> Result<SecondaryProgress> {
|
|
||||||
measured_request!(
|
|
||||||
"tenant_secondary_status",
|
|
||||||
crate::metrics::Method::Get,
|
|
||||||
&self.node_id_label,
|
|
||||||
self.inner.tenant_secondary_status(tenant_shard_id).await
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn tenant_heatmap_upload(&self, tenant_id: TenantShardId) -> Result<()> {
|
|
||||||
measured_request!(
|
|
||||||
"tenant_heatmap_upload",
|
|
||||||
crate::metrics::Method::Post,
|
|
||||||
&self.node_id_label,
|
|
||||||
self.inner.tenant_heatmap_upload(tenant_id).await
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn location_config(
|
pub(crate) async fn location_config(
|
||||||
&self,
|
&self,
|
||||||
tenant_shard_id: TenantShardId,
|
tenant_shard_id: TenantShardId,
|
||||||
@@ -234,16 +200,4 @@ impl PageserverClient {
|
|||||||
self.inner.get_utilization().await
|
self.inner.get_utilization().await
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn top_tenant_shards(
|
|
||||||
&self,
|
|
||||||
request: TopTenantShardsRequest,
|
|
||||||
) -> Result<TopTenantShardsResponse> {
|
|
||||||
measured_request!(
|
|
||||||
"top_tenants",
|
|
||||||
crate::metrics::Method::Post,
|
|
||||||
&self.node_id_label,
|
|
||||||
self.inner.top_tenant_shards(request).await
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+153
-215
@@ -2,7 +2,6 @@ pub(crate) mod split_state;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use self::split_state::SplitState;
|
use self::split_state::SplitState;
|
||||||
use camino::Utf8Path;
|
use camino::Utf8Path;
|
||||||
@@ -10,7 +9,6 @@ use camino::Utf8PathBuf;
|
|||||||
use diesel::pg::PgConnection;
|
use diesel::pg::PgConnection;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use pageserver_api::controller_api::ShardSchedulingPolicy;
|
|
||||||
use pageserver_api::controller_api::{NodeSchedulingPolicy, PlacementPolicy};
|
use pageserver_api::controller_api::{NodeSchedulingPolicy, PlacementPolicy};
|
||||||
use pageserver_api::models::TenantConfig;
|
use pageserver_api::models::TenantConfig;
|
||||||
use pageserver_api::shard::ShardConfigError;
|
use pageserver_api::shard::ShardConfigError;
|
||||||
@@ -80,7 +78,7 @@ pub(crate) enum DatabaseError {
|
|||||||
Logical(String),
|
Logical(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(measured::FixedCardinalityLabel, Copy, Clone)]
|
#[derive(measured::FixedCardinalityLabel, Clone)]
|
||||||
pub(crate) enum DatabaseOperation {
|
pub(crate) enum DatabaseOperation {
|
||||||
InsertNode,
|
InsertNode,
|
||||||
UpdateNode,
|
UpdateNode,
|
||||||
@@ -109,12 +107,6 @@ pub(crate) enum AbortShardSplitStatus {
|
|||||||
|
|
||||||
pub(crate) type DatabaseResult<T> = Result<T, DatabaseError>;
|
pub(crate) type DatabaseResult<T> = Result<T, DatabaseError>;
|
||||||
|
|
||||||
/// Some methods can operate on either a whole tenant or a single shard
|
|
||||||
pub(crate) enum TenantFilter {
|
|
||||||
Tenant(TenantId),
|
|
||||||
Shard(TenantShardId),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Persistence {
|
impl Persistence {
|
||||||
// The default postgres connection limit is 100. We use up to 99, to leave one free for a human admin under
|
// The default postgres connection limit is 100. We use up to 99, to leave one free for a human admin under
|
||||||
// normal circumstances. This assumes we have exclusive use of the database cluster to which we connect.
|
// normal circumstances. This assumes we have exclusive use of the database cluster to which we connect.
|
||||||
@@ -145,31 +137,6 @@ impl Persistence {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A helper for use during startup, where we would like to tolerate concurrent restarts of the
|
|
||||||
/// database and the storage controller, therefore the database might not be available right away
|
|
||||||
pub async fn await_connection(
|
|
||||||
database_url: &str,
|
|
||||||
timeout: Duration,
|
|
||||||
) -> Result<(), diesel::ConnectionError> {
|
|
||||||
let started_at = Instant::now();
|
|
||||||
loop {
|
|
||||||
match PgConnection::establish(database_url) {
|
|
||||||
Ok(_) => {
|
|
||||||
tracing::info!("Connected to database.");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
if started_at.elapsed() > timeout {
|
|
||||||
return Err(e);
|
|
||||||
} else {
|
|
||||||
tracing::info!("Database not yet available, waiting... ({e})");
|
|
||||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wraps `with_conn` in order to collect latency and error metrics
|
/// Wraps `with_conn` in order to collect latency and error metrics
|
||||||
async fn with_measured_conn<F, R>(&self, op: DatabaseOperation, func: F) -> DatabaseResult<R>
|
async fn with_measured_conn<F, R>(&self, op: DatabaseOperation, func: F) -> DatabaseResult<R>
|
||||||
where
|
where
|
||||||
@@ -179,7 +146,9 @@ impl Persistence {
|
|||||||
let latency = &METRICS_REGISTRY
|
let latency = &METRICS_REGISTRY
|
||||||
.metrics_group
|
.metrics_group
|
||||||
.storage_controller_database_query_latency;
|
.storage_controller_database_query_latency;
|
||||||
let _timer = latency.start_timer(DatabaseQueryLatencyLabelGroup { operation: op });
|
let _timer = latency.start_timer(DatabaseQueryLatencyLabelGroup {
|
||||||
|
operation: op.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
let res = self.with_conn(func).await;
|
let res = self.with_conn(func).await;
|
||||||
|
|
||||||
@@ -202,45 +171,10 @@ impl Persistence {
|
|||||||
F: Fn(&mut PgConnection) -> DatabaseResult<R> + Send + 'static,
|
F: Fn(&mut PgConnection) -> DatabaseResult<R> + Send + 'static,
|
||||||
R: Send + 'static,
|
R: Send + 'static,
|
||||||
{
|
{
|
||||||
// A generous allowance for how many times we may retry serializable transactions
|
|
||||||
// before giving up. This is not expected to be hit: it is a defensive measure in case we
|
|
||||||
// somehow engineer a situation where duelling transactions might otherwise live-lock.
|
|
||||||
const MAX_RETRIES: usize = 128;
|
|
||||||
|
|
||||||
let mut conn = self.connection_pool.get()?;
|
let mut conn = self.connection_pool.get()?;
|
||||||
tokio::task::spawn_blocking(move || -> DatabaseResult<R> {
|
tokio::task::spawn_blocking(move || -> DatabaseResult<R> { func(&mut conn) })
|
||||||
let mut retry_count = 0;
|
.await
|
||||||
loop {
|
.expect("Task panic")
|
||||||
match conn.build_transaction().serializable().run(|c| func(c)) {
|
|
||||||
Ok(r) => break Ok(r),
|
|
||||||
Err(
|
|
||||||
err @ DatabaseError::Query(diesel::result::Error::DatabaseError(
|
|
||||||
diesel::result::DatabaseErrorKind::SerializationFailure,
|
|
||||||
_,
|
|
||||||
)),
|
|
||||||
) => {
|
|
||||||
retry_count += 1;
|
|
||||||
if retry_count > MAX_RETRIES {
|
|
||||||
tracing::error!(
|
|
||||||
"Exceeded max retries on SerializationFailure errors: {err:?}"
|
|
||||||
);
|
|
||||||
break Err(err);
|
|
||||||
} else {
|
|
||||||
// Retry on serialization errors: these are expected, because even though our
|
|
||||||
// transactions don't fight for the same rows, they will occasionally collide
|
|
||||||
// on index pages (e.g. increment_generation for unrelated shards can collide)
|
|
||||||
tracing::debug!(
|
|
||||||
"Retrying transaction on serialization failure {err:?}"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => break Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("Task panic")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When a node is first registered, persist it before using it for anything
|
/// When a node is first registered, persist it before using it for anything
|
||||||
@@ -341,11 +275,6 @@ impl Persistence {
|
|||||||
// Backward compat for test data after PR https://github.com/neondatabase/neon/pull/7165
|
// Backward compat for test data after PR https://github.com/neondatabase/neon/pull/7165
|
||||||
shard.placement_policy = "{\"Attached\":0}".to_string();
|
shard.placement_policy = "{\"Attached\":0}".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
if shard.scheduling_policy.is_empty() {
|
|
||||||
shard.scheduling_policy =
|
|
||||||
serde_json::to_string(&ShardSchedulingPolicy::default()).unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let tenants: Vec<TenantShardPersistence> = decoded.tenants.into_values().collect();
|
let tenants: Vec<TenantShardPersistence> = decoded.tenants.into_values().collect();
|
||||||
@@ -393,11 +322,14 @@ impl Persistence {
|
|||||||
self.with_measured_conn(
|
self.with_measured_conn(
|
||||||
DatabaseOperation::InsertTenantShards,
|
DatabaseOperation::InsertTenantShards,
|
||||||
move |conn| -> DatabaseResult<()> {
|
move |conn| -> DatabaseResult<()> {
|
||||||
for tenant in &shards {
|
conn.transaction(|conn| -> QueryResult<()> {
|
||||||
diesel::insert_into(tenant_shards)
|
for tenant in &shards {
|
||||||
.values(tenant)
|
diesel::insert_into(tenant_shards)
|
||||||
.execute(conn)?;
|
.values(tenant)
|
||||||
}
|
.execute(conn)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -442,15 +374,13 @@ impl Persistence {
|
|||||||
#[tracing::instrument(skip_all, fields(node_id))]
|
#[tracing::instrument(skip_all, fields(node_id))]
|
||||||
pub(crate) async fn re_attach(
|
pub(crate) async fn re_attach(
|
||||||
&self,
|
&self,
|
||||||
input_node_id: NodeId,
|
node_id: NodeId,
|
||||||
) -> DatabaseResult<HashMap<TenantShardId, Generation>> {
|
) -> DatabaseResult<HashMap<TenantShardId, Generation>> {
|
||||||
use crate::schema::nodes::dsl::scheduling_policy;
|
|
||||||
use crate::schema::nodes::dsl::*;
|
|
||||||
use crate::schema::tenant_shards::dsl::*;
|
use crate::schema::tenant_shards::dsl::*;
|
||||||
let updated = self
|
let updated = self
|
||||||
.with_measured_conn(DatabaseOperation::ReAttach, move |conn| {
|
.with_measured_conn(DatabaseOperation::ReAttach, move |conn| {
|
||||||
let rows_updated = diesel::update(tenant_shards)
|
let rows_updated = diesel::update(tenant_shards)
|
||||||
.filter(generation_pageserver.eq(input_node_id.0 as i64))
|
.filter(generation_pageserver.eq(node_id.0 as i64))
|
||||||
.set(generation.eq(generation + 1))
|
.set(generation.eq(generation + 1))
|
||||||
.execute(conn)?;
|
.execute(conn)?;
|
||||||
|
|
||||||
@@ -459,23 +389,9 @@ impl Persistence {
|
|||||||
// TODO: UPDATE+SELECT in one query
|
// TODO: UPDATE+SELECT in one query
|
||||||
|
|
||||||
let updated = tenant_shards
|
let updated = tenant_shards
|
||||||
.filter(generation_pageserver.eq(input_node_id.0 as i64))
|
.filter(generation_pageserver.eq(node_id.0 as i64))
|
||||||
.select(TenantShardPersistence::as_select())
|
.select(TenantShardPersistence::as_select())
|
||||||
.load(conn)?;
|
.load(conn)?;
|
||||||
|
|
||||||
// If the node went through a drain and restart phase before re-attaching,
|
|
||||||
// then reset it's node scheduling policy to active.
|
|
||||||
diesel::update(nodes)
|
|
||||||
.filter(node_id.eq(input_node_id.0 as i64))
|
|
||||||
.filter(
|
|
||||||
scheduling_policy
|
|
||||||
.eq(String::from(NodeSchedulingPolicy::PauseForRestart))
|
|
||||||
.or(scheduling_policy.eq(String::from(NodeSchedulingPolicy::Draining)))
|
|
||||||
.or(scheduling_policy.eq(String::from(NodeSchedulingPolicy::Filling))),
|
|
||||||
)
|
|
||||||
.set(scheduling_policy.eq(String::from(NodeSchedulingPolicy::Active)))
|
|
||||||
.execute(conn)?;
|
|
||||||
|
|
||||||
Ok(updated)
|
Ok(updated)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@@ -549,48 +465,59 @@ impl Persistence {
|
|||||||
/// that we only do the first time a tenant is set to an attached policy via /location_config.
|
/// that we only do the first time a tenant is set to an attached policy via /location_config.
|
||||||
pub(crate) async fn update_tenant_shard(
|
pub(crate) async fn update_tenant_shard(
|
||||||
&self,
|
&self,
|
||||||
tenant: TenantFilter,
|
tenant_shard_id: TenantShardId,
|
||||||
input_placement_policy: Option<PlacementPolicy>,
|
input_placement_policy: PlacementPolicy,
|
||||||
input_config: Option<TenantConfig>,
|
input_config: TenantConfig,
|
||||||
input_generation: Option<Generation>,
|
input_generation: Option<Generation>,
|
||||||
input_scheduling_policy: Option<ShardSchedulingPolicy>,
|
|
||||||
) -> DatabaseResult<()> {
|
) -> DatabaseResult<()> {
|
||||||
use crate::schema::tenant_shards::dsl::*;
|
use crate::schema::tenant_shards::dsl::*;
|
||||||
|
|
||||||
self.with_measured_conn(DatabaseOperation::UpdateTenantShard, move |conn| {
|
self.with_measured_conn(DatabaseOperation::UpdateTenantShard, move |conn| {
|
||||||
let query = match tenant {
|
let query = diesel::update(tenant_shards)
|
||||||
TenantFilter::Shard(tenant_shard_id) => diesel::update(tenant_shards)
|
.filter(tenant_id.eq(tenant_shard_id.tenant_id.to_string()))
|
||||||
.filter(tenant_id.eq(tenant_shard_id.tenant_id.to_string()))
|
.filter(shard_number.eq(tenant_shard_id.shard_number.0 as i32))
|
||||||
.filter(shard_number.eq(tenant_shard_id.shard_number.0 as i32))
|
.filter(shard_count.eq(tenant_shard_id.shard_count.literal() as i32));
|
||||||
.filter(shard_count.eq(tenant_shard_id.shard_count.literal() as i32))
|
|
||||||
.into_boxed(),
|
|
||||||
TenantFilter::Tenant(input_tenant_id) => diesel::update(tenant_shards)
|
|
||||||
.filter(tenant_id.eq(input_tenant_id.to_string()))
|
|
||||||
.into_boxed(),
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(AsChangeset)]
|
if let Some(input_generation) = input_generation {
|
||||||
#[diesel(table_name = crate::schema::tenant_shards)]
|
// Update includes generation column
|
||||||
struct ShardUpdate {
|
query
|
||||||
generation: Option<i32>,
|
.set((
|
||||||
placement_policy: Option<String>,
|
generation.eq(Some(input_generation.into().unwrap() as i32)),
|
||||||
config: Option<String>,
|
placement_policy
|
||||||
scheduling_policy: Option<String>,
|
.eq(serde_json::to_string(&input_placement_policy).unwrap()),
|
||||||
|
config.eq(serde_json::to_string(&input_config).unwrap()),
|
||||||
|
))
|
||||||
|
.execute(conn)?;
|
||||||
|
} else {
|
||||||
|
// Update does not include generation column
|
||||||
|
query
|
||||||
|
.set((
|
||||||
|
placement_policy
|
||||||
|
.eq(serde_json::to_string(&input_placement_policy).unwrap()),
|
||||||
|
config.eq(serde_json::to_string(&input_config).unwrap()),
|
||||||
|
))
|
||||||
|
.execute(conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let update = ShardUpdate {
|
Ok(())
|
||||||
generation: input_generation.map(|g| g.into().unwrap() as i32),
|
})
|
||||||
placement_policy: input_placement_policy
|
.await?;
|
||||||
.as_ref()
|
|
||||||
.map(|p| serde_json::to_string(&p).unwrap()),
|
|
||||||
config: input_config
|
|
||||||
.as_ref()
|
|
||||||
.map(|c| serde_json::to_string(&c).unwrap()),
|
|
||||||
scheduling_policy: input_scheduling_policy
|
|
||||||
.map(|p| serde_json::to_string(&p).unwrap()),
|
|
||||||
};
|
|
||||||
|
|
||||||
query.set(update).execute(conn)?;
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn update_tenant_config(
|
||||||
|
&self,
|
||||||
|
input_tenant_id: TenantId,
|
||||||
|
input_config: TenantConfig,
|
||||||
|
) -> DatabaseResult<()> {
|
||||||
|
use crate::schema::tenant_shards::dsl::*;
|
||||||
|
|
||||||
|
self.with_measured_conn(DatabaseOperation::UpdateTenantConfig, move |conn| {
|
||||||
|
diesel::update(tenant_shards)
|
||||||
|
.filter(tenant_id.eq(input_tenant_id.to_string()))
|
||||||
|
.set((config.eq(serde_json::to_string(&input_config).unwrap()),))
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -632,51 +559,55 @@ impl Persistence {
|
|||||||
) -> DatabaseResult<()> {
|
) -> DatabaseResult<()> {
|
||||||
use crate::schema::tenant_shards::dsl::*;
|
use crate::schema::tenant_shards::dsl::*;
|
||||||
self.with_measured_conn(DatabaseOperation::BeginShardSplit, move |conn| -> DatabaseResult<()> {
|
self.with_measured_conn(DatabaseOperation::BeginShardSplit, move |conn| -> DatabaseResult<()> {
|
||||||
// Mark parent shards as splitting
|
conn.transaction(|conn| -> DatabaseResult<()> {
|
||||||
|
// Mark parent shards as splitting
|
||||||
|
|
||||||
let updated = diesel::update(tenant_shards)
|
let updated = diesel::update(tenant_shards)
|
||||||
.filter(tenant_id.eq(split_tenant_id.to_string()))
|
.filter(tenant_id.eq(split_tenant_id.to_string()))
|
||||||
.filter(shard_count.eq(old_shard_count.literal() as i32))
|
.filter(shard_count.eq(old_shard_count.literal() as i32))
|
||||||
.set((splitting.eq(1),))
|
.set((splitting.eq(1),))
|
||||||
.execute(conn)?;
|
.execute(conn)?;
|
||||||
if u8::try_from(updated)
|
if u8::try_from(updated)
|
||||||
.map_err(|_| DatabaseError::Logical(
|
.map_err(|_| DatabaseError::Logical(
|
||||||
format!("Overflow existing shard count {} while splitting", updated))
|
format!("Overflow existing shard count {} while splitting", updated))
|
||||||
)? != old_shard_count.count() {
|
)? != old_shard_count.count() {
|
||||||
// Perhaps a deletion or another split raced with this attempt to split, mutating
|
// Perhaps a deletion or another split raced with this attempt to split, mutating
|
||||||
// the parent shards that we intend to split. In this case the split request should fail.
|
// the parent shards that we intend to split. In this case the split request should fail.
|
||||||
return Err(DatabaseError::Logical(
|
return Err(DatabaseError::Logical(
|
||||||
format!("Unexpected existing shard count {updated} when preparing tenant for split (expected {})", old_shard_count.count())
|
format!("Unexpected existing shard count {updated} when preparing tenant for split (expected {})", old_shard_count.count())
|
||||||
));
|
));
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: spurious clone to sidestep closure move rules
|
|
||||||
let parent_to_children = parent_to_children.clone();
|
|
||||||
|
|
||||||
// Insert child shards
|
|
||||||
for (parent_shard_id, children) in parent_to_children {
|
|
||||||
let mut parent = crate::schema::tenant_shards::table
|
|
||||||
.filter(tenant_id.eq(parent_shard_id.tenant_id.to_string()))
|
|
||||||
.filter(shard_number.eq(parent_shard_id.shard_number.0 as i32))
|
|
||||||
.filter(shard_count.eq(parent_shard_id.shard_count.literal() as i32))
|
|
||||||
.load::<TenantShardPersistence>(conn)?;
|
|
||||||
let parent = if parent.len() != 1 {
|
|
||||||
return Err(DatabaseError::Logical(format!(
|
|
||||||
"Parent shard {parent_shard_id} not found"
|
|
||||||
)));
|
|
||||||
} else {
|
|
||||||
parent.pop().unwrap()
|
|
||||||
};
|
|
||||||
for mut shard in children {
|
|
||||||
// Carry the parent's generation into the child
|
|
||||||
shard.generation = parent.generation;
|
|
||||||
|
|
||||||
debug_assert!(shard.splitting == SplitState::Splitting);
|
|
||||||
diesel::insert_into(tenant_shards)
|
|
||||||
.values(shard)
|
|
||||||
.execute(conn)?;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// FIXME: spurious clone to sidestep closure move rules
|
||||||
|
let parent_to_children = parent_to_children.clone();
|
||||||
|
|
||||||
|
// Insert child shards
|
||||||
|
for (parent_shard_id, children) in parent_to_children {
|
||||||
|
let mut parent = crate::schema::tenant_shards::table
|
||||||
|
.filter(tenant_id.eq(parent_shard_id.tenant_id.to_string()))
|
||||||
|
.filter(shard_number.eq(parent_shard_id.shard_number.0 as i32))
|
||||||
|
.filter(shard_count.eq(parent_shard_id.shard_count.literal() as i32))
|
||||||
|
.load::<TenantShardPersistence>(conn)?;
|
||||||
|
let parent = if parent.len() != 1 {
|
||||||
|
return Err(DatabaseError::Logical(format!(
|
||||||
|
"Parent shard {parent_shard_id} not found"
|
||||||
|
)));
|
||||||
|
} else {
|
||||||
|
parent.pop().unwrap()
|
||||||
|
};
|
||||||
|
for mut shard in children {
|
||||||
|
// Carry the parent's generation into the child
|
||||||
|
shard.generation = parent.generation;
|
||||||
|
|
||||||
|
debug_assert!(shard.splitting == SplitState::Splitting);
|
||||||
|
diesel::insert_into(tenant_shards)
|
||||||
|
.values(shard)
|
||||||
|
.execute(conn)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -694,18 +625,22 @@ impl Persistence {
|
|||||||
self.with_measured_conn(
|
self.with_measured_conn(
|
||||||
DatabaseOperation::CompleteShardSplit,
|
DatabaseOperation::CompleteShardSplit,
|
||||||
move |conn| -> DatabaseResult<()> {
|
move |conn| -> DatabaseResult<()> {
|
||||||
// Drop parent shards
|
conn.transaction(|conn| -> QueryResult<()> {
|
||||||
diesel::delete(tenant_shards)
|
// Drop parent shards
|
||||||
.filter(tenant_id.eq(split_tenant_id.to_string()))
|
diesel::delete(tenant_shards)
|
||||||
.filter(shard_count.eq(old_shard_count.literal() as i32))
|
.filter(tenant_id.eq(split_tenant_id.to_string()))
|
||||||
.execute(conn)?;
|
.filter(shard_count.eq(old_shard_count.literal() as i32))
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
// Clear sharding flag
|
// Clear sharding flag
|
||||||
let updated = diesel::update(tenant_shards)
|
let updated = diesel::update(tenant_shards)
|
||||||
.filter(tenant_id.eq(split_tenant_id.to_string()))
|
.filter(tenant_id.eq(split_tenant_id.to_string()))
|
||||||
.set((splitting.eq(0),))
|
.set((splitting.eq(0),))
|
||||||
.execute(conn)?;
|
.execute(conn)?;
|
||||||
debug_assert!(updated > 0);
|
debug_assert!(updated > 0);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
@@ -724,41 +659,46 @@ impl Persistence {
|
|||||||
self.with_measured_conn(
|
self.with_measured_conn(
|
||||||
DatabaseOperation::AbortShardSplit,
|
DatabaseOperation::AbortShardSplit,
|
||||||
move |conn| -> DatabaseResult<AbortShardSplitStatus> {
|
move |conn| -> DatabaseResult<AbortShardSplitStatus> {
|
||||||
// Clear the splitting state on parent shards
|
let aborted =
|
||||||
let updated = diesel::update(tenant_shards)
|
conn.transaction(|conn| -> DatabaseResult<AbortShardSplitStatus> {
|
||||||
.filter(tenant_id.eq(split_tenant_id.to_string()))
|
// Clear the splitting state on parent shards
|
||||||
.filter(shard_count.ne(new_shard_count.literal() as i32))
|
let updated = diesel::update(tenant_shards)
|
||||||
.set((splitting.eq(0),))
|
.filter(tenant_id.eq(split_tenant_id.to_string()))
|
||||||
.execute(conn)?;
|
.filter(shard_count.ne(new_shard_count.literal() as i32))
|
||||||
|
.set((splitting.eq(0),))
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
// Parent shards are already gone: we cannot abort.
|
// Parent shards are already gone: we cannot abort.
|
||||||
if updated == 0 {
|
if updated == 0 {
|
||||||
return Ok(AbortShardSplitStatus::Complete);
|
return Ok(AbortShardSplitStatus::Complete);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check: if parent shards were present, their cardinality should
|
// Sanity check: if parent shards were present, their cardinality should
|
||||||
// be less than the number of child shards.
|
// be less than the number of child shards.
|
||||||
if updated >= new_shard_count.count() as usize {
|
if updated >= new_shard_count.count() as usize {
|
||||||
return Err(DatabaseError::Logical(format!(
|
return Err(DatabaseError::Logical(format!(
|
||||||
"Unexpected parent shard count {updated} while aborting split to \
|
"Unexpected parent shard count {updated} while aborting split to \
|
||||||
count {new_shard_count:?} on tenant {split_tenant_id}"
|
count {new_shard_count:?} on tenant {split_tenant_id}"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erase child shards
|
// Erase child shards
|
||||||
diesel::delete(tenant_shards)
|
diesel::delete(tenant_shards)
|
||||||
.filter(tenant_id.eq(split_tenant_id.to_string()))
|
.filter(tenant_id.eq(split_tenant_id.to_string()))
|
||||||
.filter(shard_count.eq(new_shard_count.literal() as i32))
|
.filter(shard_count.eq(new_shard_count.literal() as i32))
|
||||||
.execute(conn)?;
|
.execute(conn)?;
|
||||||
|
|
||||||
Ok(AbortShardSplitStatus::Aborted)
|
Ok(AbortShardSplitStatus::Aborted)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(aborted)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parts of [`crate::tenant_shard::TenantShard`] that are stored durably
|
/// Parts of [`crate::tenant_state::TenantState`] that are stored durably
|
||||||
#[derive(Queryable, Selectable, Insertable, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
#[derive(Queryable, Selectable, Insertable, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||||
#[diesel(table_name = crate::schema::tenant_shards)]
|
#[diesel(table_name = crate::schema::tenant_shards)]
|
||||||
pub(crate) struct TenantShardPersistence {
|
pub(crate) struct TenantShardPersistence {
|
||||||
@@ -788,8 +728,6 @@ pub(crate) struct TenantShardPersistence {
|
|||||||
pub(crate) splitting: SplitState,
|
pub(crate) splitting: SplitState,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) config: String,
|
pub(crate) config: String,
|
||||||
#[serde(default)]
|
|
||||||
pub(crate) scheduling_policy: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TenantShardPersistence {
|
impl TenantShardPersistence {
|
||||||
+7
-28
@@ -1,12 +1,12 @@
|
|||||||
use crate::pageserver_client::PageserverClient;
|
use crate::pageserver_client::PageserverClient;
|
||||||
use crate::persistence::Persistence;
|
use crate::persistence::Persistence;
|
||||||
use crate::service;
|
use crate::service;
|
||||||
|
use hyper::StatusCode;
|
||||||
use pageserver_api::models::{
|
use pageserver_api::models::{
|
||||||
LocationConfig, LocationConfigMode, LocationConfigSecondary, TenantConfig,
|
LocationConfig, LocationConfigMode, LocationConfigSecondary, TenantConfig,
|
||||||
};
|
};
|
||||||
use pageserver_api::shard::{ShardIdentity, TenantShardId};
|
use pageserver_api::shard::{ShardIdentity, TenantShardId};
|
||||||
use pageserver_client::mgmt_api;
|
use pageserver_client::mgmt_api;
|
||||||
use reqwest::StatusCode;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -18,14 +18,14 @@ use utils::sync::gate::GateGuard;
|
|||||||
|
|
||||||
use crate::compute_hook::{ComputeHook, NotifyError};
|
use crate::compute_hook::{ComputeHook, NotifyError};
|
||||||
use crate::node::Node;
|
use crate::node::Node;
|
||||||
use crate::tenant_shard::{IntentState, ObservedState, ObservedStateLocation};
|
use crate::tenant_state::{IntentState, ObservedState, ObservedStateLocation};
|
||||||
|
|
||||||
const DEFAULT_HEATMAP_PERIOD: &str = "60s";
|
const DEFAULT_HEATMAP_PERIOD: &str = "60s";
|
||||||
|
|
||||||
/// Object with the lifetime of the background reconcile task that is created
|
/// Object with the lifetime of the background reconcile task that is created
|
||||||
/// for tenants which have a difference between their intent and observed states.
|
/// for tenants which have a difference between their intent and observed states.
|
||||||
pub(super) struct Reconciler {
|
pub(super) struct Reconciler {
|
||||||
/// See [`crate::tenant_shard::TenantShard`] for the meanings of these fields: they are a snapshot
|
/// See [`crate::tenant_state::TenantState`] for the meanings of these fields: they are a snapshot
|
||||||
/// of a tenant's state from when we spawned a reconcile task.
|
/// of a tenant's state from when we spawned a reconcile task.
|
||||||
pub(super) tenant_shard_id: TenantShardId,
|
pub(super) tenant_shard_id: TenantShardId,
|
||||||
pub(crate) shard: ShardIdentity,
|
pub(crate) shard: ShardIdentity,
|
||||||
@@ -48,15 +48,11 @@ pub(super) struct Reconciler {
|
|||||||
|
|
||||||
/// To avoid stalling if the cloud control plane is unavailable, we may proceed
|
/// To avoid stalling if the cloud control plane is unavailable, we may proceed
|
||||||
/// past failures in [`ComputeHook::notify`], but we _must_ remember that we failed
|
/// past failures in [`ComputeHook::notify`], but we _must_ remember that we failed
|
||||||
/// so that we can set [`crate::tenant_shard::TenantShard::pending_compute_notification`] to ensure a later retry.
|
/// so that we can set [`crate::tenant_state::TenantState::pending_compute_notification`] to ensure a later retry.
|
||||||
pub(crate) compute_notify_failure: bool,
|
pub(crate) compute_notify_failure: bool,
|
||||||
|
|
||||||
/// Reconciler is responsible for keeping alive semaphore units that limit concurrency on how many
|
|
||||||
/// we will spawn.
|
|
||||||
pub(crate) _resource_units: ReconcileUnits,
|
|
||||||
|
|
||||||
/// A means to abort background reconciliation: it is essential to
|
/// A means to abort background reconciliation: it is essential to
|
||||||
/// call this when something changes in the original TenantShard that
|
/// call this when something changes in the original TenantState that
|
||||||
/// will make this reconciliation impossible or unnecessary, for
|
/// will make this reconciliation impossible or unnecessary, for
|
||||||
/// example when a pageserver node goes offline, or the PlacementPolicy for
|
/// example when a pageserver node goes offline, or the PlacementPolicy for
|
||||||
/// the tenant is changed.
|
/// the tenant is changed.
|
||||||
@@ -70,20 +66,7 @@ pub(super) struct Reconciler {
|
|||||||
pub(crate) persistence: Arc<Persistence>,
|
pub(crate) persistence: Arc<Persistence>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RAII resource units granted to a Reconciler, which it should keep alive until it finishes doing I/O
|
/// This is a snapshot of [`crate::tenant_state::IntentState`], but it does not do any
|
||||||
pub(crate) struct ReconcileUnits {
|
|
||||||
_sem_units: tokio::sync::OwnedSemaphorePermit,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ReconcileUnits {
|
|
||||||
pub(crate) fn new(sem_units: tokio::sync::OwnedSemaphorePermit) -> Self {
|
|
||||||
Self {
|
|
||||||
_sem_units: sem_units,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This is a snapshot of [`crate::tenant_shard::IntentState`], but it does not do any
|
|
||||||
/// reference counting for Scheduler. The IntentState is what the scheduler works with,
|
/// reference counting for Scheduler. The IntentState is what the scheduler works with,
|
||||||
/// and the TargetState is just the instruction for a particular Reconciler run.
|
/// and the TargetState is just the instruction for a particular Reconciler run.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -504,7 +487,6 @@ impl Reconciler {
|
|||||||
while let Err(e) = self.compute_notify().await {
|
while let Err(e) = self.compute_notify().await {
|
||||||
match e {
|
match e {
|
||||||
NotifyError::Fatal(_) => return Err(ReconcileError::Notify(e)),
|
NotifyError::Fatal(_) => return Err(ReconcileError::Notify(e)),
|
||||||
NotifyError::ShuttingDown => return Err(ReconcileError::Cancel),
|
|
||||||
_ => {
|
_ => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"Live migration blocked by compute notification error, retrying: {e}"
|
"Live migration blocked by compute notification error, retrying: {e}"
|
||||||
@@ -767,10 +749,7 @@ impl Reconciler {
|
|||||||
// It is up to the caller whether they want to drop out on this error, but they don't have to:
|
// It is up to the caller whether they want to drop out on this error, but they don't have to:
|
||||||
// in general we should avoid letting unavailability of the cloud control plane stop us from
|
// in general we should avoid letting unavailability of the cloud control plane stop us from
|
||||||
// making progress.
|
// making progress.
|
||||||
if !matches!(e, NotifyError::ShuttingDown) {
|
tracing::warn!("Failed to notify compute of attached pageserver {node}: {e}");
|
||||||
tracing::warn!("Failed to notify compute of attached pageserver {node}: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set this flag so that in our ReconcileResult we will set the flag on the shard that it
|
// Set this flag so that in our ReconcileResult we will set the flag on the shard that it
|
||||||
// needs to retry at some point.
|
// needs to retry at some point.
|
||||||
self.compute_notify_failure = true;
|
self.compute_notify_failure = true;
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
use crate::{node::Node, tenant_state::TenantState};
|
||||||
|
use pageserver_api::controller_api::UtilizationScore;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use utils::{http::error::ApiError, id::NodeId};
|
||||||
|
|
||||||
|
/// Scenarios in which we cannot find a suitable location for a tenant shard
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ScheduleError {
|
||||||
|
#[error("No pageservers found")]
|
||||||
|
NoPageservers,
|
||||||
|
#[error("No pageserver found matching constraint")]
|
||||||
|
ImpossibleConstraint,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ScheduleError> for ApiError {
|
||||||
|
fn from(value: ScheduleError) -> Self {
|
||||||
|
ApiError::Conflict(format!("Scheduling error: {}", value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Eq, PartialEq)]
|
||||||
|
pub enum MaySchedule {
|
||||||
|
Yes(UtilizationScore),
|
||||||
|
No,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SchedulerNode {
|
||||||
|
/// How many shards are currently scheduled on this node, via their [`crate::tenant_state::IntentState`].
|
||||||
|
shard_count: usize,
|
||||||
|
|
||||||
|
/// Whether this node is currently elegible to have new shards scheduled (this is derived
|
||||||
|
/// from a node's availability state and scheduling policy).
|
||||||
|
may_schedule: MaySchedule,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for SchedulerNode {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
let may_schedule_matches = matches!(
|
||||||
|
(&self.may_schedule, &other.may_schedule),
|
||||||
|
(MaySchedule::Yes(_), MaySchedule::Yes(_)) | (MaySchedule::No, MaySchedule::No)
|
||||||
|
);
|
||||||
|
|
||||||
|
may_schedule_matches && self.shard_count == other.shard_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for SchedulerNode {}
|
||||||
|
|
||||||
|
/// This type is responsible for selecting which node is used when a tenant shard needs to choose a pageserver
|
||||||
|
/// on which to run.
|
||||||
|
///
|
||||||
|
/// The type has no persistent state of its own: this is all populated at startup. The Serialize
|
||||||
|
/// impl is only for debug dumps.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub(crate) struct Scheduler {
|
||||||
|
nodes: HashMap<NodeId, SchedulerNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scheduler {
|
||||||
|
pub(crate) fn new<'a>(nodes: impl Iterator<Item = &'a Node>) -> Self {
|
||||||
|
let mut scheduler_nodes = HashMap::new();
|
||||||
|
for node in nodes {
|
||||||
|
scheduler_nodes.insert(
|
||||||
|
node.get_id(),
|
||||||
|
SchedulerNode {
|
||||||
|
shard_count: 0,
|
||||||
|
may_schedule: node.may_schedule(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
nodes: scheduler_nodes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For debug/support: check that our internal statistics are in sync with the state of
|
||||||
|
/// the nodes & tenant shards.
|
||||||
|
///
|
||||||
|
/// If anything is inconsistent, log details and return an error.
|
||||||
|
pub(crate) fn consistency_check<'a>(
|
||||||
|
&self,
|
||||||
|
nodes: impl Iterator<Item = &'a Node>,
|
||||||
|
shards: impl Iterator<Item = &'a TenantState>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut expect_nodes: HashMap<NodeId, SchedulerNode> = HashMap::new();
|
||||||
|
for node in nodes {
|
||||||
|
expect_nodes.insert(
|
||||||
|
node.get_id(),
|
||||||
|
SchedulerNode {
|
||||||
|
shard_count: 0,
|
||||||
|
may_schedule: node.may_schedule(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for shard in shards {
|
||||||
|
if let Some(node_id) = shard.intent.get_attached() {
|
||||||
|
match expect_nodes.get_mut(node_id) {
|
||||||
|
Some(node) => node.shard_count += 1,
|
||||||
|
None => anyhow::bail!(
|
||||||
|
"Tenant {} references nonexistent node {}",
|
||||||
|
shard.tenant_shard_id,
|
||||||
|
node_id
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for node_id in shard.intent.get_secondary() {
|
||||||
|
match expect_nodes.get_mut(node_id) {
|
||||||
|
Some(node) => node.shard_count += 1,
|
||||||
|
None => anyhow::bail!(
|
||||||
|
"Tenant {} references nonexistent node {}",
|
||||||
|
shard.tenant_shard_id,
|
||||||
|
node_id
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (node_id, expect_node) in &expect_nodes {
|
||||||
|
let Some(self_node) = self.nodes.get(node_id) else {
|
||||||
|
anyhow::bail!("Node {node_id} not found in Self")
|
||||||
|
};
|
||||||
|
|
||||||
|
if self_node != expect_node {
|
||||||
|
tracing::error!("Inconsistency detected in scheduling state for node {node_id}");
|
||||||
|
tracing::error!("Expected state: {}", serde_json::to_string(expect_node)?);
|
||||||
|
tracing::error!("Self state: {}", serde_json::to_string(self_node)?);
|
||||||
|
|
||||||
|
anyhow::bail!("Inconsistent state on {node_id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if expect_nodes.len() != self.nodes.len() {
|
||||||
|
// We just checked that all the expected nodes are present. If the lengths don't match,
|
||||||
|
// it means that we have nodes in Self that are unexpected.
|
||||||
|
for node_id in self.nodes.keys() {
|
||||||
|
if !expect_nodes.contains_key(node_id) {
|
||||||
|
anyhow::bail!("Node {node_id} found in Self but not in expected nodes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment the reference count of a node. This reference count is used to guide scheduling
|
||||||
|
/// decisions, not for memory management: it represents one tenant shard whose IntentState targets
|
||||||
|
/// this node.
|
||||||
|
///
|
||||||
|
/// It is an error to call this for a node that is not known to the scheduler (i.e. passed into
|
||||||
|
/// [`Self::new`] or [`Self::node_upsert`])
|
||||||
|
pub(crate) fn node_inc_ref(&mut self, node_id: NodeId) {
|
||||||
|
let Some(node) = self.nodes.get_mut(&node_id) else {
|
||||||
|
tracing::error!("Scheduler missing node {node_id}");
|
||||||
|
debug_assert!(false);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
node.shard_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrement a node's reference count. Inverse of [`Self::node_inc_ref`].
|
||||||
|
pub(crate) fn node_dec_ref(&mut self, node_id: NodeId) {
|
||||||
|
let Some(node) = self.nodes.get_mut(&node_id) else {
|
||||||
|
debug_assert!(false);
|
||||||
|
tracing::error!("Scheduler missing node {node_id}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
node.shard_count -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn node_upsert(&mut self, node: &Node) {
|
||||||
|
use std::collections::hash_map::Entry::*;
|
||||||
|
match self.nodes.entry(node.get_id()) {
|
||||||
|
Occupied(mut entry) => {
|
||||||
|
entry.get_mut().may_schedule = node.may_schedule();
|
||||||
|
}
|
||||||
|
Vacant(entry) => {
|
||||||
|
entry.insert(SchedulerNode {
|
||||||
|
shard_count: 0,
|
||||||
|
may_schedule: node.may_schedule(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn node_remove(&mut self, node_id: NodeId) {
|
||||||
|
if self.nodes.remove(&node_id).is_none() {
|
||||||
|
tracing::warn!(node_id=%node_id, "Removed non-existent node from scheduler");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where we have several nodes to choose from, for example when picking a secondary location
|
||||||
|
/// to promote to an attached location, this method may be used to pick the best choice based
|
||||||
|
/// on the scheduler's knowledge of utilization and availability.
|
||||||
|
///
|
||||||
|
/// If the input is empty, or all the nodes are not elegible for scheduling, return None: the
|
||||||
|
/// caller can pick a node some other way.
|
||||||
|
pub(crate) fn node_preferred(&self, nodes: &[NodeId]) -> Option<NodeId> {
|
||||||
|
if nodes.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: When the utilization score returned by the pageserver becomes meaningful,
|
||||||
|
// schedule based on that instead of the shard count.
|
||||||
|
let node = nodes
|
||||||
|
.iter()
|
||||||
|
.map(|node_id| {
|
||||||
|
let may_schedule = self
|
||||||
|
.nodes
|
||||||
|
.get(node_id)
|
||||||
|
.map(|n| n.may_schedule != MaySchedule::No)
|
||||||
|
.unwrap_or(false);
|
||||||
|
(*node_id, may_schedule)
|
||||||
|
})
|
||||||
|
.max_by_key(|(_n, may_schedule)| *may_schedule);
|
||||||
|
|
||||||
|
// If even the preferred node has may_schedule==false, return None
|
||||||
|
node.and_then(|(node_id, may_schedule)| if may_schedule { Some(node_id) } else { None })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn schedule_shard(&self, hard_exclude: &[NodeId]) -> Result<NodeId, ScheduleError> {
|
||||||
|
if self.nodes.is_empty() {
|
||||||
|
return Err(ScheduleError::NoPageservers);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tenant_counts: Vec<(NodeId, usize)> = self
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(k, v)| {
|
||||||
|
if hard_exclude.contains(k) || v.may_schedule == MaySchedule::No {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some((*k, v.shard_count))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Sort by tenant count. Nodes with the same tenant count are sorted by ID.
|
||||||
|
tenant_counts.sort_by_key(|i| (i.1, i.0));
|
||||||
|
|
||||||
|
if tenant_counts.is_empty() {
|
||||||
|
// After applying constraints, no pageservers were left. We log some detail about
|
||||||
|
// the state of nodes to help understand why this happened. This is not logged as an error because
|
||||||
|
// it is legitimately possible for enough nodes to be Offline to prevent scheduling a shard.
|
||||||
|
tracing::info!("Scheduling failure, while excluding {hard_exclude:?}, node states:");
|
||||||
|
for (node_id, node) in &self.nodes {
|
||||||
|
tracing::info!(
|
||||||
|
"Node {node_id}: may_schedule={} shards={}",
|
||||||
|
node.may_schedule != MaySchedule::No,
|
||||||
|
node.shard_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(ScheduleError::ImpossibleConstraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
let node_id = tenant_counts.first().unwrap().0;
|
||||||
|
tracing::info!(
|
||||||
|
"scheduler selected node {node_id} (elegible nodes {:?}, exclude: {hard_exclude:?})",
|
||||||
|
tenant_counts.iter().map(|i| i.0 .0).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Note that we do not update shard count here to reflect the scheduling: that
|
||||||
|
// is IntentState's job when the scheduled location is used.
|
||||||
|
|
||||||
|
Ok(node_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod test_utils {
|
||||||
|
|
||||||
|
use crate::node::Node;
|
||||||
|
use pageserver_api::controller_api::{NodeAvailability, UtilizationScore};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use utils::id::NodeId;
|
||||||
|
/// Test helper: synthesize the requested number of nodes, all in active state.
|
||||||
|
///
|
||||||
|
/// Node IDs start at one.
|
||||||
|
pub(crate) fn make_test_nodes(n: u64) -> HashMap<NodeId, Node> {
|
||||||
|
(1..n + 1)
|
||||||
|
.map(|i| {
|
||||||
|
(NodeId(i), {
|
||||||
|
let mut node = Node::new(
|
||||||
|
NodeId(i),
|
||||||
|
format!("httphost-{i}"),
|
||||||
|
80 + i as u16,
|
||||||
|
format!("pghost-{i}"),
|
||||||
|
5432 + i as u16,
|
||||||
|
);
|
||||||
|
node.set_availability(NodeAvailability::Active(UtilizationScore::worst()));
|
||||||
|
assert!(node.is_available());
|
||||||
|
node
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use crate::tenant_state::IntentState;
|
||||||
|
#[test]
|
||||||
|
fn scheduler_basic() -> anyhow::Result<()> {
|
||||||
|
let nodes = test_utils::make_test_nodes(2);
|
||||||
|
|
||||||
|
let mut scheduler = Scheduler::new(nodes.values());
|
||||||
|
let mut t1_intent = IntentState::new();
|
||||||
|
let mut t2_intent = IntentState::new();
|
||||||
|
|
||||||
|
let scheduled = scheduler.schedule_shard(&[])?;
|
||||||
|
t1_intent.set_attached(&mut scheduler, Some(scheduled));
|
||||||
|
let scheduled = scheduler.schedule_shard(&[])?;
|
||||||
|
t2_intent.set_attached(&mut scheduler, Some(scheduled));
|
||||||
|
|
||||||
|
assert_eq!(scheduler.nodes.get(&NodeId(1)).unwrap().shard_count, 1);
|
||||||
|
assert_eq!(scheduler.nodes.get(&NodeId(2)).unwrap().shard_count, 1);
|
||||||
|
|
||||||
|
let scheduled = scheduler.schedule_shard(&t1_intent.all_pageservers())?;
|
||||||
|
t1_intent.push_secondary(&mut scheduler, scheduled);
|
||||||
|
|
||||||
|
assert_eq!(scheduler.nodes.get(&NodeId(1)).unwrap().shard_count, 1);
|
||||||
|
assert_eq!(scheduler.nodes.get(&NodeId(2)).unwrap().shard_count, 2);
|
||||||
|
|
||||||
|
t1_intent.clear(&mut scheduler);
|
||||||
|
assert_eq!(scheduler.nodes.get(&NodeId(1)).unwrap().shard_count, 0);
|
||||||
|
assert_eq!(scheduler.nodes.get(&NodeId(2)).unwrap().shard_count, 1);
|
||||||
|
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
// Dropping an IntentState without clearing it causes a panic in debug mode,
|
||||||
|
// because we have failed to properly update scheduler shard counts.
|
||||||
|
let result = std::panic::catch_unwind(move || {
|
||||||
|
drop(t2_intent);
|
||||||
|
});
|
||||||
|
assert!(result.is_err());
|
||||||
|
} else {
|
||||||
|
t2_intent.clear(&mut scheduler);
|
||||||
|
assert_eq!(scheduler.nodes.get(&NodeId(1)).unwrap().shard_count, 0);
|
||||||
|
assert_eq!(scheduler.nodes.get(&NodeId(2)).unwrap().shard_count, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,6 @@ diesel::table! {
|
|||||||
placement_policy -> Varchar,
|
placement_policy -> Varchar,
|
||||||
splitting -> Int2,
|
splitting -> Int2,
|
||||||
config -> Text,
|
config -> Text,
|
||||||
scheduling_policy -> Varchar,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+377
-1955
File diff suppressed because it is too large
Load Diff
+103
-788
File diff suppressed because it is too large
Load Diff
@@ -69,9 +69,6 @@ where
|
|||||||
// Not generic AsRef<OsStr>, otherwise empty `envs` prevents type inference
|
// Not generic AsRef<OsStr>, otherwise empty `envs` prevents type inference
|
||||||
EI: IntoIterator<Item = (String, String)>,
|
EI: IntoIterator<Item = (String, String)>,
|
||||||
{
|
{
|
||||||
if !datadir.metadata().context("stat datadir")?.is_dir() {
|
|
||||||
anyhow::bail!("`datadir` must be a directory when calling this function: {datadir:?}");
|
|
||||||
}
|
|
||||||
let log_path = datadir.join(format!("{process_name}.log"));
|
let log_path = datadir.join(format!("{process_name}.log"));
|
||||||
let process_log_file = fs::OpenOptions::new()
|
let process_log_file = fs::OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
@@ -88,17 +85,8 @@ where
|
|||||||
let background_command = command
|
let background_command = command
|
||||||
.stdout(process_log_file)
|
.stdout(process_log_file)
|
||||||
.stderr(same_file_for_stderr)
|
.stderr(same_file_for_stderr)
|
||||||
.args(args)
|
.args(args);
|
||||||
// spawn all child processes in their datadir, useful for all kinds of things,
|
let filled_cmd = fill_remote_storage_secrets_vars(fill_rust_env_vars(background_command));
|
||||||
// not least cleaning up child processes e.g. after an unclean exit from the test suite:
|
|
||||||
// ```
|
|
||||||
// lsof -d cwd -a +D Users/cs/src/neon/test_output
|
|
||||||
// ```
|
|
||||||
.current_dir(datadir);
|
|
||||||
|
|
||||||
let filled_cmd = fill_env_vars_prefixed_neon(fill_remote_storage_secrets_vars(
|
|
||||||
fill_rust_env_vars(background_command),
|
|
||||||
));
|
|
||||||
filled_cmd.envs(envs);
|
filled_cmd.envs(envs);
|
||||||
|
|
||||||
let pid_file_to_check = match &initial_pid_file {
|
let pid_file_to_check = match &initial_pid_file {
|
||||||
@@ -280,15 +268,6 @@ fn fill_remote_storage_secrets_vars(mut cmd: &mut Command) -> &mut Command {
|
|||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_env_vars_prefixed_neon(mut cmd: &mut Command) -> &mut Command {
|
|
||||||
for (var, val) in std::env::vars() {
|
|
||||||
if var.starts_with("NEON_PAGESERVER_") {
|
|
||||||
cmd = cmd.env(var, val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a `pre_exec` to the cmd that, inbetween fork() and exec(),
|
/// Add a `pre_exec` to the cmd that, inbetween fork() and exec(),
|
||||||
/// 1. Claims a pidfile with a fcntl lock on it and
|
/// 1. Claims a pidfile with a fcntl lock on it and
|
||||||
/// 2. Sets up the pidfile's file descriptor so that it (and the lock)
|
/// 2. Sets up the pidfile's file descriptor so that it (and the lock)
|
||||||
|
|||||||
+150
-147
@@ -9,23 +9,22 @@ use anyhow::{anyhow, bail, Context, Result};
|
|||||||
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command, ValueEnum};
|
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command, ValueEnum};
|
||||||
use compute_api::spec::ComputeMode;
|
use compute_api::spec::ComputeMode;
|
||||||
use control_plane::endpoint::ComputeControlPlane;
|
use control_plane::endpoint::ComputeControlPlane;
|
||||||
use control_plane::local_env::{
|
use control_plane::local_env::{InitForceMode, LocalEnv};
|
||||||
InitForceMode, LocalEnv, NeonBroker, NeonLocalInitConf, NeonLocalInitPageserverConf,
|
use control_plane::pageserver::{PageServerNode, PAGESERVER_REMOTE_STORAGE_DIR};
|
||||||
SafekeeperConf,
|
|
||||||
};
|
|
||||||
use control_plane::pageserver::PageServerNode;
|
|
||||||
use control_plane::safekeeper::SafekeeperNode;
|
use control_plane::safekeeper::SafekeeperNode;
|
||||||
use control_plane::storage_controller::StorageController;
|
use control_plane::storage_controller::StorageController;
|
||||||
use control_plane::{broker, local_env};
|
use control_plane::{broker, local_env};
|
||||||
use pageserver_api::config::{
|
use pageserver_api::controller_api::{
|
||||||
DEFAULT_HTTP_LISTEN_PORT as DEFAULT_PAGESERVER_HTTP_PORT,
|
NodeAvailability, NodeConfigureRequest, NodeSchedulingPolicy, PlacementPolicy,
|
||||||
DEFAULT_PG_LISTEN_PORT as DEFAULT_PAGESERVER_PG_PORT,
|
|
||||||
};
|
};
|
||||||
use pageserver_api::controller_api::PlacementPolicy;
|
|
||||||
use pageserver_api::models::{
|
use pageserver_api::models::{
|
||||||
ShardParameters, TenantCreateRequest, TimelineCreateRequest, TimelineInfo,
|
ShardParameters, TenantCreateRequest, TimelineCreateRequest, TimelineInfo,
|
||||||
};
|
};
|
||||||
use pageserver_api::shard::{ShardCount, ShardStripeSize, TenantShardId};
|
use pageserver_api::shard::{ShardCount, ShardStripeSize, TenantShardId};
|
||||||
|
use pageserver_api::{
|
||||||
|
DEFAULT_HTTP_LISTEN_PORT as DEFAULT_PAGESERVER_HTTP_PORT,
|
||||||
|
DEFAULT_PG_LISTEN_PORT as DEFAULT_PAGESERVER_PG_PORT,
|
||||||
|
};
|
||||||
use postgres_backend::AuthType;
|
use postgres_backend::AuthType;
|
||||||
use postgres_connection::parse_host_port;
|
use postgres_connection::parse_host_port;
|
||||||
use safekeeper_api::{
|
use safekeeper_api::{
|
||||||
@@ -55,6 +54,44 @@ const DEFAULT_PG_VERSION: &str = "15";
|
|||||||
|
|
||||||
const DEFAULT_PAGESERVER_CONTROL_PLANE_API: &str = "http://127.0.0.1:1234/upcall/v1/";
|
const DEFAULT_PAGESERVER_CONTROL_PLANE_API: &str = "http://127.0.0.1:1234/upcall/v1/";
|
||||||
|
|
||||||
|
fn default_conf(num_pageservers: u16) -> String {
|
||||||
|
let mut template = format!(
|
||||||
|
r#"
|
||||||
|
# Default built-in configuration, defined in main.rs
|
||||||
|
control_plane_api = '{DEFAULT_PAGESERVER_CONTROL_PLANE_API}'
|
||||||
|
|
||||||
|
[broker]
|
||||||
|
listen_addr = '{DEFAULT_BROKER_ADDR}'
|
||||||
|
|
||||||
|
[[safekeepers]]
|
||||||
|
id = {DEFAULT_SAFEKEEPER_ID}
|
||||||
|
pg_port = {DEFAULT_SAFEKEEPER_PG_PORT}
|
||||||
|
http_port = {DEFAULT_SAFEKEEPER_HTTP_PORT}
|
||||||
|
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
for i in 0..num_pageservers {
|
||||||
|
let pageserver_id = NodeId(DEFAULT_PAGESERVER_ID.0 + i as u64);
|
||||||
|
let pg_port = DEFAULT_PAGESERVER_PG_PORT + i;
|
||||||
|
let http_port = DEFAULT_PAGESERVER_HTTP_PORT + i;
|
||||||
|
|
||||||
|
template += &format!(
|
||||||
|
r#"
|
||||||
|
[[pageservers]]
|
||||||
|
id = {pageserver_id}
|
||||||
|
listen_pg_addr = '127.0.0.1:{pg_port}'
|
||||||
|
listen_http_addr = '127.0.0.1:{http_port}'
|
||||||
|
pg_auth_type = '{trust_auth}'
|
||||||
|
http_auth_type = '{trust_auth}'
|
||||||
|
"#,
|
||||||
|
trust_auth = AuthType::Trust,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
template
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Timelines tree element used as a value in the HashMap.
|
/// Timelines tree element used as a value in the HashMap.
|
||||||
///
|
///
|
||||||
@@ -87,8 +124,7 @@ fn main() -> Result<()> {
|
|||||||
handle_init(sub_args).map(Some)
|
handle_init(sub_args).map(Some)
|
||||||
} else {
|
} else {
|
||||||
// all other commands need an existing config
|
// all other commands need an existing config
|
||||||
let mut env =
|
let mut env = LocalEnv::load_config().context("Error loading config")?;
|
||||||
LocalEnv::load_config(&local_env::base_path()).context("Error loading config")?;
|
|
||||||
let original_env = env.clone();
|
let original_env = env.clone();
|
||||||
|
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
@@ -99,7 +135,7 @@ fn main() -> Result<()> {
|
|||||||
let subcommand_result = match sub_name {
|
let subcommand_result = match sub_name {
|
||||||
"tenant" => rt.block_on(handle_tenant(sub_args, &mut env)),
|
"tenant" => rt.block_on(handle_tenant(sub_args, &mut env)),
|
||||||
"timeline" => rt.block_on(handle_timeline(sub_args, &mut env)),
|
"timeline" => rt.block_on(handle_timeline(sub_args, &mut env)),
|
||||||
"start" => rt.block_on(handle_start_all(&env)),
|
"start" => rt.block_on(handle_start_all(sub_args, &env)),
|
||||||
"stop" => rt.block_on(handle_stop_all(sub_args, &env)),
|
"stop" => rt.block_on(handle_stop_all(sub_args, &env)),
|
||||||
"pageserver" => rt.block_on(handle_pageserver(sub_args, &env)),
|
"pageserver" => rt.block_on(handle_pageserver(sub_args, &env)),
|
||||||
"storage_controller" => rt.block_on(handle_storage_controller(sub_args, &env)),
|
"storage_controller" => rt.block_on(handle_storage_controller(sub_args, &env)),
|
||||||
@@ -118,7 +154,7 @@ fn main() -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match subcommand_result {
|
match subcommand_result {
|
||||||
Ok(Some(updated_env)) => updated_env.persist_config()?,
|
Ok(Some(updated_env)) => updated_env.persist_config(&updated_env.base_data_dir)?,
|
||||||
Ok(None) => (),
|
Ok(None) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("command failed: {e:?}");
|
eprintln!("command failed: {e:?}");
|
||||||
@@ -307,66 +343,48 @@ fn parse_timeline_id(sub_match: &ArgMatches) -> anyhow::Result<Option<TimelineId
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_init(init_match: &ArgMatches) -> anyhow::Result<LocalEnv> {
|
fn handle_init(init_match: &ArgMatches) -> anyhow::Result<LocalEnv> {
|
||||||
let num_pageservers = init_match.get_one::<u16>("num-pageservers");
|
let num_pageservers = init_match
|
||||||
|
.get_one::<u16>("num-pageservers")
|
||||||
let force = init_match.get_one("force").expect("we set a default value");
|
.expect("num-pageservers arg has a default");
|
||||||
|
// Create config file
|
||||||
// Create the in-memory `LocalEnv` that we'd normally load from disk in `load_config`.
|
let toml_file: String = if let Some(config_path) = init_match.get_one::<PathBuf>("config") {
|
||||||
let init_conf: NeonLocalInitConf = if let Some(config_path) =
|
|
||||||
init_match.get_one::<PathBuf>("config")
|
|
||||||
{
|
|
||||||
// User (likely the Python test suite) provided a description of the environment.
|
|
||||||
if num_pageservers.is_some() {
|
|
||||||
bail!("Cannot specify both --num-pageservers and --config, use key `pageservers` in the --config file instead");
|
|
||||||
}
|
|
||||||
// load and parse the file
|
// load and parse the file
|
||||||
let contents = std::fs::read_to_string(config_path).with_context(|| {
|
std::fs::read_to_string(config_path).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Could not read configuration file '{}'",
|
"Could not read configuration file '{}'",
|
||||||
config_path.display()
|
config_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?
|
||||||
toml_edit::de::from_str(&contents)?
|
|
||||||
} else {
|
} else {
|
||||||
// User (likely interactive) did not provide a description of the environment, give them the default
|
// Built-in default config
|
||||||
NeonLocalInitConf {
|
default_conf(*num_pageservers)
|
||||||
control_plane_api: Some(Some(DEFAULT_PAGESERVER_CONTROL_PLANE_API.parse().unwrap())),
|
|
||||||
broker: NeonBroker {
|
|
||||||
listen_addr: DEFAULT_BROKER_ADDR.parse().unwrap(),
|
|
||||||
},
|
|
||||||
safekeepers: vec![SafekeeperConf {
|
|
||||||
id: DEFAULT_SAFEKEEPER_ID,
|
|
||||||
pg_port: DEFAULT_SAFEKEEPER_PG_PORT,
|
|
||||||
http_port: DEFAULT_SAFEKEEPER_HTTP_PORT,
|
|
||||||
..Default::default()
|
|
||||||
}],
|
|
||||||
pageservers: (0..num_pageservers.copied().unwrap_or(1))
|
|
||||||
.map(|i| {
|
|
||||||
let pageserver_id = NodeId(DEFAULT_PAGESERVER_ID.0 + i as u64);
|
|
||||||
let pg_port = DEFAULT_PAGESERVER_PG_PORT + i;
|
|
||||||
let http_port = DEFAULT_PAGESERVER_HTTP_PORT + i;
|
|
||||||
NeonLocalInitPageserverConf {
|
|
||||||
id: pageserver_id,
|
|
||||||
listen_pg_addr: format!("127.0.0.1:{pg_port}"),
|
|
||||||
listen_http_addr: format!("127.0.0.1:{http_port}"),
|
|
||||||
pg_auth_type: AuthType::Trust,
|
|
||||||
http_auth_type: AuthType::Trust,
|
|
||||||
other: Default::default(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
pg_distrib_dir: None,
|
|
||||||
neon_distrib_dir: None,
|
|
||||||
default_tenant_id: TenantId::from_array(std::array::from_fn(|_| 0)),
|
|
||||||
storage_controller: None,
|
|
||||||
control_plane_compute_hook_api: None,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
LocalEnv::init(init_conf, force)
|
let pg_version = init_match
|
||||||
.context("materialize initial neon_local environment on disk")?;
|
.get_one::<u32>("pg-version")
|
||||||
Ok(LocalEnv::load_config(&local_env::base_path())
|
.copied()
|
||||||
.expect("freshly written config should be loadable"))
|
.context("Failed to parse postgres version from the argument string")?;
|
||||||
|
|
||||||
|
let mut env =
|
||||||
|
LocalEnv::parse_config(&toml_file).context("Failed to create neon configuration")?;
|
||||||
|
let force = init_match.get_one("force").expect("we set a default value");
|
||||||
|
env.init(pg_version, force)
|
||||||
|
.context("Failed to initialize neon repository")?;
|
||||||
|
|
||||||
|
// Create remote storage location for default LocalFs remote storage
|
||||||
|
std::fs::create_dir_all(env.base_data_dir.join(PAGESERVER_REMOTE_STORAGE_DIR))?;
|
||||||
|
|
||||||
|
// Initialize pageserver, create initial tenant and timeline.
|
||||||
|
for ps_conf in &env.pageservers {
|
||||||
|
PageServerNode::from_env(&env, ps_conf)
|
||||||
|
.initialize(&pageserver_config_overrides(init_match))
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("pageserver init failed: {e:?}");
|
||||||
|
exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(env)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The default pageserver is the one where CLI tenant/timeline operations are sent by default.
|
/// The default pageserver is the one where CLI tenant/timeline operations are sent by default.
|
||||||
@@ -381,6 +399,15 @@ fn get_default_pageserver(env: &local_env::LocalEnv) -> PageServerNode {
|
|||||||
PageServerNode::from_env(env, ps_conf)
|
PageServerNode::from_env(env, ps_conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pageserver_config_overrides(init_match: &ArgMatches) -> Vec<&str> {
|
||||||
|
init_match
|
||||||
|
.get_many::<String>("pageserver-config-override")
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.map(String::as_str)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_tenant(
|
async fn handle_tenant(
|
||||||
tenant_match: &ArgMatches,
|
tenant_match: &ArgMatches,
|
||||||
env: &mut local_env::LocalEnv,
|
env: &mut local_env::LocalEnv,
|
||||||
@@ -392,54 +419,6 @@ async fn handle_tenant(
|
|||||||
println!("{} {:?}", t.id, t.state);
|
println!("{} {:?}", t.id, t.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(("import", import_match)) => {
|
|
||||||
let tenant_id = parse_tenant_id(import_match)?.unwrap_or_else(TenantId::generate);
|
|
||||||
|
|
||||||
let storage_controller = StorageController::from_env(env);
|
|
||||||
let create_response = storage_controller.tenant_import(tenant_id).await?;
|
|
||||||
|
|
||||||
let shard_zero = create_response
|
|
||||||
.shards
|
|
||||||
.first()
|
|
||||||
.expect("Import response omitted shards");
|
|
||||||
|
|
||||||
let attached_pageserver_id = shard_zero.node_id;
|
|
||||||
let pageserver =
|
|
||||||
PageServerNode::from_env(env, env.get_pageserver_conf(attached_pageserver_id)?);
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"Imported tenant {tenant_id}, attached to pageserver {attached_pageserver_id}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let timelines = pageserver
|
|
||||||
.http_client
|
|
||||||
.list_timelines(shard_zero.shard_id)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Pick a 'main' timeline that has no ancestors, the rest will get arbitrary names
|
|
||||||
let main_timeline = timelines
|
|
||||||
.iter()
|
|
||||||
.find(|t| t.ancestor_timeline_id.is_none())
|
|
||||||
.expect("No timelines found")
|
|
||||||
.timeline_id;
|
|
||||||
|
|
||||||
let mut branch_i = 0;
|
|
||||||
for timeline in timelines.iter() {
|
|
||||||
let branch_name = if timeline.timeline_id == main_timeline {
|
|
||||||
"main".to_string()
|
|
||||||
} else {
|
|
||||||
branch_i += 1;
|
|
||||||
format!("branch_{branch_i}")
|
|
||||||
};
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"Importing timeline {tenant_id}/{} as branch {branch_name}",
|
|
||||||
timeline.timeline_id
|
|
||||||
);
|
|
||||||
|
|
||||||
env.register_branch_mapping(branch_name, tenant_id, timeline.timeline_id)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(("create", create_match)) => {
|
Some(("create", create_match)) => {
|
||||||
let tenant_conf: HashMap<_, _> = create_match
|
let tenant_conf: HashMap<_, _> = create_match
|
||||||
.get_many::<String>("config")
|
.get_many::<String>("config")
|
||||||
@@ -812,8 +791,6 @@ async fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Re
|
|||||||
.copied()
|
.copied()
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
let allow_multiple = sub_args.get_flag("allow-multiple");
|
|
||||||
|
|
||||||
let mode = match (lsn, hot_standby) {
|
let mode = match (lsn, hot_standby) {
|
||||||
(Some(lsn), false) => ComputeMode::Static(lsn),
|
(Some(lsn), false) => ComputeMode::Static(lsn),
|
||||||
(None, true) => ComputeMode::Replica,
|
(None, true) => ComputeMode::Replica,
|
||||||
@@ -831,9 +808,7 @@ async fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Re
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !allow_multiple {
|
cplane.check_conflicting_endpoints(mode, tenant_id, timeline_id)?;
|
||||||
cplane.check_conflicting_endpoints(mode, tenant_id, timeline_id)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
cplane.new_endpoint(
|
cplane.new_endpoint(
|
||||||
&endpoint_id,
|
&endpoint_id,
|
||||||
@@ -862,8 +837,6 @@ async fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Re
|
|||||||
|
|
||||||
let remote_ext_config = sub_args.get_one::<String>("remote-ext-config");
|
let remote_ext_config = sub_args.get_one::<String>("remote-ext-config");
|
||||||
|
|
||||||
let allow_multiple = sub_args.get_flag("allow-multiple");
|
|
||||||
|
|
||||||
// If --safekeepers argument is given, use only the listed safekeeper nodes.
|
// If --safekeepers argument is given, use only the listed 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") {
|
||||||
@@ -889,13 +862,11 @@ async fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Re
|
|||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if !allow_multiple {
|
cplane.check_conflicting_endpoints(
|
||||||
cplane.check_conflicting_endpoints(
|
endpoint.mode,
|
||||||
endpoint.mode,
|
endpoint.tenant_id,
|
||||||
endpoint.tenant_id,
|
endpoint.timeline_id,
|
||||||
endpoint.timeline_id,
|
)?;
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (pageservers, stripe_size) = if let Some(pageserver_id) = pageserver_id {
|
let (pageservers, stripe_size) = if let Some(pageserver_id) = pageserver_id {
|
||||||
let conf = env.get_pageserver_conf(pageserver_id).unwrap();
|
let conf = env.get_pageserver_conf(pageserver_id).unwrap();
|
||||||
@@ -1051,7 +1022,10 @@ fn get_pageserver(env: &local_env::LocalEnv, args: &ArgMatches) -> Result<PageSe
|
|||||||
async fn handle_pageserver(sub_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<()> {
|
async fn handle_pageserver(sub_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<()> {
|
||||||
match sub_match.subcommand() {
|
match sub_match.subcommand() {
|
||||||
Some(("start", subcommand_args)) => {
|
Some(("start", subcommand_args)) => {
|
||||||
if let Err(e) = get_pageserver(env, subcommand_args)?.start().await {
|
if let Err(e) = get_pageserver(env, subcommand_args)?
|
||||||
|
.start(&pageserver_config_overrides(subcommand_args))
|
||||||
|
.await
|
||||||
|
{
|
||||||
eprintln!("pageserver start failed: {e}");
|
eprintln!("pageserver start failed: {e}");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
@@ -1077,12 +1051,30 @@ async fn handle_pageserver(sub_match: &ArgMatches, env: &local_env::LocalEnv) ->
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = pageserver.start().await {
|
if let Err(e) = pageserver
|
||||||
|
.start(&pageserver_config_overrides(subcommand_args))
|
||||||
|
.await
|
||||||
|
{
|
||||||
eprintln!("pageserver start failed: {e}");
|
eprintln!("pageserver start failed: {e}");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some(("set-state", subcommand_args)) => {
|
||||||
|
let pageserver = get_pageserver(env, subcommand_args)?;
|
||||||
|
let scheduling = subcommand_args.get_one("scheduling");
|
||||||
|
let availability = subcommand_args.get_one("availability");
|
||||||
|
|
||||||
|
let storage_controller = StorageController::from_env(env);
|
||||||
|
storage_controller
|
||||||
|
.node_configure(NodeConfigureRequest {
|
||||||
|
node_id: pageserver.conf.id,
|
||||||
|
scheduling: scheduling.cloned(),
|
||||||
|
availability: availability.cloned(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
Some(("status", subcommand_args)) => {
|
Some(("status", subcommand_args)) => {
|
||||||
match get_pageserver(env, subcommand_args)?.check_status().await {
|
match get_pageserver(env, subcommand_args)?.check_status().await {
|
||||||
Ok(_) => println!("Page server is up and running"),
|
Ok(_) => println!("Page server is up and running"),
|
||||||
@@ -1204,7 +1196,7 @@ async fn handle_safekeeper(sub_match: &ArgMatches, env: &local_env::LocalEnv) ->
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_start_all(env: &local_env::LocalEnv) -> anyhow::Result<()> {
|
async fn handle_start_all(sub_match: &ArgMatches, env: &local_env::LocalEnv) -> anyhow::Result<()> {
|
||||||
// Endpoints are not started automatically
|
// Endpoints are not started automatically
|
||||||
|
|
||||||
broker::start_broker_process(env).await?;
|
broker::start_broker_process(env).await?;
|
||||||
@@ -1221,7 +1213,10 @@ async fn handle_start_all(env: &local_env::LocalEnv) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
for ps_conf in &env.pageservers {
|
for ps_conf in &env.pageservers {
|
||||||
let pageserver = PageServerNode::from_env(env, ps_conf);
|
let pageserver = PageServerNode::from_env(env, ps_conf);
|
||||||
if let Err(e) = pageserver.start().await {
|
if let Err(e) = pageserver
|
||||||
|
.start(&pageserver_config_overrides(sub_match))
|
||||||
|
.await
|
||||||
|
{
|
||||||
eprintln!("pageserver {} start failed: {:#}", ps_conf.id, e);
|
eprintln!("pageserver {} start failed: {:#}", ps_conf.id, e);
|
||||||
try_stop_all(env, true).await;
|
try_stop_all(env, true).await;
|
||||||
exit(1);
|
exit(1);
|
||||||
@@ -1253,7 +1248,7 @@ async fn try_stop_all(env: &local_env::LocalEnv, immediate: bool) {
|
|||||||
match ComputeControlPlane::load(env.clone()) {
|
match ComputeControlPlane::load(env.clone()) {
|
||||||
Ok(cplane) => {
|
Ok(cplane) => {
|
||||||
for (_k, node) in cplane.endpoints {
|
for (_k, node) in cplane.endpoints {
|
||||||
if let Err(e) = node.stop(if immediate { "immediate" } else { "fast" }, false) {
|
if let Err(e) = node.stop(if immediate { "immediate" } else { "fast " }, false) {
|
||||||
eprintln!("postgres stop failed: {e:#}");
|
eprintln!("postgres stop failed: {e:#}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1362,6 +1357,13 @@ fn cli() -> Command {
|
|||||||
.required(false)
|
.required(false)
|
||||||
.value_name("stop-mode");
|
.value_name("stop-mode");
|
||||||
|
|
||||||
|
let pageserver_config_args = Arg::new("pageserver-config-override")
|
||||||
|
.long("pageserver-config-override")
|
||||||
|
.num_args(1)
|
||||||
|
.action(ArgAction::Append)
|
||||||
|
.help("Additional pageserver's configuration options or overrides, refer to pageserver's 'config-override' CLI parameter docs for more")
|
||||||
|
.required(false);
|
||||||
|
|
||||||
let remote_ext_config_args = Arg::new("remote-ext-config")
|
let remote_ext_config_args = Arg::new("remote-ext-config")
|
||||||
.long("remote-ext-config")
|
.long("remote-ext-config")
|
||||||
.num_args(1)
|
.num_args(1)
|
||||||
@@ -1395,7 +1397,9 @@ fn cli() -> Command {
|
|||||||
let num_pageservers_arg = Arg::new("num-pageservers")
|
let num_pageservers_arg = Arg::new("num-pageservers")
|
||||||
.value_parser(value_parser!(u16))
|
.value_parser(value_parser!(u16))
|
||||||
.long("num-pageservers")
|
.long("num-pageservers")
|
||||||
.help("How many pageservers to create (default 1)");
|
.help("How many pageservers to create (default 1)")
|
||||||
|
.required(false)
|
||||||
|
.default_value("1");
|
||||||
|
|
||||||
let update_catalog = Arg::new("update-catalog")
|
let update_catalog = Arg::new("update-catalog")
|
||||||
.value_parser(value_parser!(bool))
|
.value_parser(value_parser!(bool))
|
||||||
@@ -1409,25 +1413,20 @@ fn cli() -> Command {
|
|||||||
.help("If set, will create test user `user` and `neondb` database. Requires `update-catalog = true`")
|
.help("If set, will create test user `user` and `neondb` database. Requires `update-catalog = true`")
|
||||||
.required(false);
|
.required(false);
|
||||||
|
|
||||||
let allow_multiple = Arg::new("allow-multiple")
|
|
||||||
.help("Allow multiple primary endpoints running on the same branch. Shouldn't be used normally, but useful for tests.")
|
|
||||||
.long("allow-multiple")
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.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)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("init")
|
Command::new("init")
|
||||||
.about("Initialize a new Neon repository, preparing configs for services to start with")
|
.about("Initialize a new Neon repository, preparing configs for services to start with")
|
||||||
|
.arg(pageserver_config_args.clone())
|
||||||
.arg(num_pageservers_arg.clone())
|
.arg(num_pageservers_arg.clone())
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("config")
|
Arg::new("config")
|
||||||
.long("config")
|
.long("config")
|
||||||
.required(false)
|
.required(false)
|
||||||
.value_parser(value_parser!(PathBuf))
|
.value_parser(value_parser!(PathBuf))
|
||||||
.value_name("config")
|
.value_name("config"),
|
||||||
)
|
)
|
||||||
.arg(pg_version_arg.clone())
|
.arg(pg_version_arg.clone())
|
||||||
.arg(force_arg)
|
.arg(force_arg)
|
||||||
@@ -1435,7 +1434,6 @@ fn cli() -> Command {
|
|||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("timeline")
|
Command::new("timeline")
|
||||||
.about("Manage timelines")
|
.about("Manage timelines")
|
||||||
.arg_required_else_help(true)
|
|
||||||
.subcommand(Command::new("list")
|
.subcommand(Command::new("list")
|
||||||
.about("List all timelines, available to this pageserver")
|
.about("List all timelines, available to this pageserver")
|
||||||
.arg(tenant_id_arg.clone()))
|
.arg(tenant_id_arg.clone()))
|
||||||
@@ -1498,8 +1496,6 @@ fn cli() -> Command {
|
|||||||
.subcommand(Command::new("config")
|
.subcommand(Command::new("config")
|
||||||
.arg(tenant_id_arg.clone())
|
.arg(tenant_id_arg.clone())
|
||||||
.arg(Arg::new("config").short('c').num_args(1).action(ArgAction::Append).required(false)))
|
.arg(Arg::new("config").short('c').num_args(1).action(ArgAction::Append).required(false)))
|
||||||
.subcommand(Command::new("import").arg(tenant_id_arg.clone().required(true))
|
|
||||||
.about("Import a tenant that is present in remote storage, and create branches for its timelines"))
|
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("pageserver")
|
Command::new("pageserver")
|
||||||
@@ -1509,6 +1505,7 @@ fn cli() -> Command {
|
|||||||
.subcommand(Command::new("status"))
|
.subcommand(Command::new("status"))
|
||||||
.subcommand(Command::new("start")
|
.subcommand(Command::new("start")
|
||||||
.about("Start local pageserver")
|
.about("Start local pageserver")
|
||||||
|
.arg(pageserver_config_args.clone())
|
||||||
)
|
)
|
||||||
.subcommand(Command::new("stop")
|
.subcommand(Command::new("stop")
|
||||||
.about("Stop local pageserver")
|
.about("Stop local pageserver")
|
||||||
@@ -1516,14 +1513,21 @@ fn cli() -> Command {
|
|||||||
)
|
)
|
||||||
.subcommand(Command::new("restart")
|
.subcommand(Command::new("restart")
|
||||||
.about("Restart local pageserver")
|
.about("Restart local pageserver")
|
||||||
|
.arg(pageserver_config_args.clone())
|
||||||
|
)
|
||||||
|
.subcommand(Command::new("set-state")
|
||||||
|
.arg(Arg::new("availability").value_parser(value_parser!(NodeAvailability)).long("availability").action(ArgAction::Set).help("Availability state: offline,active"))
|
||||||
|
.arg(Arg::new("scheduling").value_parser(value_parser!(NodeSchedulingPolicy)).long("scheduling").action(ArgAction::Set).help("Scheduling state: draining,pause,filling,active"))
|
||||||
|
.about("Set scheduling or availability state of pageserver node")
|
||||||
|
.arg(pageserver_config_args.clone())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("storage_controller")
|
Command::new("storage_controller")
|
||||||
.arg_required_else_help(true)
|
.arg_required_else_help(true)
|
||||||
.about("Manage storage_controller")
|
.about("Manage storage_controller")
|
||||||
.subcommand(Command::new("start").about("Start storage controller"))
|
.subcommand(Command::new("start").about("Start local pageserver").arg(pageserver_config_args.clone()))
|
||||||
.subcommand(Command::new("stop").about("Stop storage controller")
|
.subcommand(Command::new("stop").about("Stop local pageserver")
|
||||||
.arg(stop_mode_arg.clone()))
|
.arg(stop_mode_arg.clone()))
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
@@ -1569,7 +1573,6 @@ fn cli() -> Command {
|
|||||||
.arg(pg_version_arg.clone())
|
.arg(pg_version_arg.clone())
|
||||||
.arg(hot_standby_arg.clone())
|
.arg(hot_standby_arg.clone())
|
||||||
.arg(update_catalog)
|
.arg(update_catalog)
|
||||||
.arg(allow_multiple.clone())
|
|
||||||
)
|
)
|
||||||
.subcommand(Command::new("start")
|
.subcommand(Command::new("start")
|
||||||
.about("Start postgres.\n If the endpoint doesn't exist yet, it is created.")
|
.about("Start postgres.\n If the endpoint doesn't exist yet, it is created.")
|
||||||
@@ -1578,7 +1581,6 @@ fn cli() -> Command {
|
|||||||
.arg(safekeepers_arg)
|
.arg(safekeepers_arg)
|
||||||
.arg(remote_ext_config_args)
|
.arg(remote_ext_config_args)
|
||||||
.arg(create_test_user)
|
.arg(create_test_user)
|
||||||
.arg(allow_multiple.clone())
|
|
||||||
)
|
)
|
||||||
.subcommand(Command::new("reconfigure")
|
.subcommand(Command::new("reconfigure")
|
||||||
.about("Reconfigure the endpoint")
|
.about("Reconfigure the endpoint")
|
||||||
@@ -1630,6 +1632,7 @@ fn cli() -> Command {
|
|||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("start")
|
Command::new("start")
|
||||||
.about("Start page server and safekeepers")
|
.about("Start page server and safekeepers")
|
||||||
|
.arg(pageserver_config_args)
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("stop")
|
Command::new("stop")
|
||||||
|
|||||||
@@ -554,7 +554,6 @@ impl Endpoint {
|
|||||||
format_version: 1.0,
|
format_version: 1.0,
|
||||||
operation_uuid: None,
|
operation_uuid: None,
|
||||||
features: self.features.clone(),
|
features: self.features.clone(),
|
||||||
swap_size_bytes: None,
|
|
||||||
cluster: Cluster {
|
cluster: Cluster {
|
||||||
cluster_id: None, // project ID: not used
|
cluster_id: None, // project ID: not used
|
||||||
name: None, // project name: not used
|
name: None, // project name: not used
|
||||||
|
|||||||
+176
-344
@@ -3,7 +3,7 @@
|
|||||||
//! Now it also provides init method which acts like a stub for proper installation
|
//! Now it also provides init method which acts like a stub for proper installation
|
||||||
//! script which will use local paths.
|
//! script which will use local paths.
|
||||||
|
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, ensure, Context};
|
||||||
|
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use postgres_backend::AuthType;
|
use postgres_backend::AuthType;
|
||||||
@@ -17,14 +17,11 @@ use std::net::Ipv4Addr;
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::time::Duration;
|
|
||||||
use utils::{
|
use utils::{
|
||||||
auth::{encode_from_key_file, Claims},
|
auth::{encode_from_key_file, Claims},
|
||||||
id::{NodeId, TenantId, TenantTimelineId, TimelineId},
|
id::{NodeId, TenantId, TenantTimelineId, TimelineId},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::pageserver::PageServerNode;
|
|
||||||
use crate::pageserver::PAGESERVER_REMOTE_STORAGE_DIR;
|
|
||||||
use crate::safekeeper::SafekeeperNode;
|
use crate::safekeeper::SafekeeperNode;
|
||||||
|
|
||||||
pub const DEFAULT_PG_VERSION: u32 = 15;
|
pub const DEFAULT_PG_VERSION: u32 = 15;
|
||||||
@@ -36,107 +33,63 @@ pub const DEFAULT_PG_VERSION: u32 = 15;
|
|||||||
// to 'neon_local init --config=<path>' option. See control_plane/simple.conf for
|
// to 'neon_local init --config=<path>' option. See control_plane/simple.conf for
|
||||||
// an example.
|
// an example.
|
||||||
//
|
//
|
||||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
||||||
pub struct LocalEnv {
|
pub struct LocalEnv {
|
||||||
// Base directory for all the nodes (the pageserver, safekeepers and
|
// Base directory for all the nodes (the pageserver, safekeepers and
|
||||||
// compute endpoints).
|
// compute endpoints).
|
||||||
//
|
//
|
||||||
// This is not stored in the config file. Rather, this is the path where the
|
// This is not stored in the config file. Rather, this is the path where the
|
||||||
// config file itself is. It is read from the NEON_REPO_DIR env variable which
|
// config file itself is. It is read from the NEON_REPO_DIR env variable or
|
||||||
// must be an absolute path. If the env var is not set, $PWD/.neon is used.
|
// '.neon' if not given.
|
||||||
|
#[serde(skip)]
|
||||||
pub base_data_dir: PathBuf,
|
pub base_data_dir: PathBuf,
|
||||||
|
|
||||||
// Path to postgres distribution. It's expected that "bin", "include",
|
// Path to postgres distribution. It's expected that "bin", "include",
|
||||||
// "lib", "share" from postgres distribution are there. If at some point
|
// "lib", "share" from postgres distribution are there. If at some point
|
||||||
// in time we will be able to run against vanilla postgres we may split that
|
// in time we will be able to run against vanilla postgres we may split that
|
||||||
// to four separate paths and match OS-specific installation layout.
|
// to four separate paths and match OS-specific installation layout.
|
||||||
|
#[serde(default)]
|
||||||
pub pg_distrib_dir: PathBuf,
|
pub pg_distrib_dir: PathBuf,
|
||||||
|
|
||||||
// Path to pageserver binary.
|
// Path to pageserver binary.
|
||||||
|
#[serde(default)]
|
||||||
pub neon_distrib_dir: PathBuf,
|
pub neon_distrib_dir: PathBuf,
|
||||||
|
|
||||||
// Default tenant ID to use with the 'neon_local' command line utility, when
|
// Default tenant ID to use with the 'neon_local' command line utility, when
|
||||||
// --tenant_id is not explicitly specified.
|
// --tenant_id is not explicitly specified.
|
||||||
|
#[serde(default)]
|
||||||
pub default_tenant_id: Option<TenantId>,
|
pub default_tenant_id: Option<TenantId>,
|
||||||
|
|
||||||
// used to issue tokens during e.g pg start
|
// used to issue tokens during e.g pg start
|
||||||
|
#[serde(default)]
|
||||||
pub private_key_path: PathBuf,
|
pub private_key_path: PathBuf,
|
||||||
|
|
||||||
pub broker: NeonBroker,
|
pub broker: NeonBroker,
|
||||||
|
|
||||||
// Configuration for the storage controller (1 per neon_local environment)
|
|
||||||
pub storage_controller: NeonStorageControllerConf,
|
|
||||||
|
|
||||||
/// This Vec must always contain at least one pageserver
|
/// This Vec must always contain at least one pageserver
|
||||||
/// Populdated by [`Self::load_config`] from the individual `pageserver.toml`s.
|
|
||||||
/// NB: not used anymore except for informing users that they need to change their `.neon/config`.
|
|
||||||
pub pageservers: Vec<PageServerConf>,
|
pub pageservers: Vec<PageServerConf>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
pub safekeepers: Vec<SafekeeperConf>,
|
pub safekeepers: Vec<SafekeeperConf>,
|
||||||
|
|
||||||
// Control plane upcall API for pageserver: if None, we will not run storage_controller If set, this will
|
// Control plane upcall API for pageserver: if None, we will not run storage_controller If set, this will
|
||||||
// be propagated into each pageserver's configuration.
|
// be propagated into each pageserver's configuration.
|
||||||
|
#[serde(default)]
|
||||||
pub control_plane_api: Option<Url>,
|
pub control_plane_api: Option<Url>,
|
||||||
|
|
||||||
// Control plane upcall API for storage controller. If set, this will be propagated into the
|
// Control plane upcall API for storage controller. If set, this will be propagated into the
|
||||||
// storage controller's configuration.
|
// storage controller's configuration.
|
||||||
|
#[serde(default)]
|
||||||
pub control_plane_compute_hook_api: Option<Url>,
|
pub control_plane_compute_hook_api: Option<Url>,
|
||||||
|
|
||||||
/// Keep human-readable aliases in memory (and persist them to config), to hide ZId hex strings from the user.
|
/// Keep human-readable aliases in memory (and persist them to config), to hide ZId hex strings from the user.
|
||||||
|
#[serde(default)]
|
||||||
// A `HashMap<String, HashMap<TenantId, TimelineId>>` would be more appropriate here,
|
// A `HashMap<String, HashMap<TenantId, TimelineId>>` would be more appropriate here,
|
||||||
// but deserialization into a generic toml object as `toml::Value::try_from` fails with an error.
|
// but deserialization into a generic toml object as `toml::Value::try_from` fails with an error.
|
||||||
// https://toml.io/en/v1.0.0 does not contain a concept of "a table inside another table".
|
// https://toml.io/en/v1.0.0 does not contain a concept of "a table inside another table".
|
||||||
pub branch_name_mappings: HashMap<String, Vec<(TenantId, TimelineId)>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// On-disk state stored in `.neon/config`.
|
|
||||||
#[derive(PartialEq, Eq, Clone, Debug, Default, Serialize, Deserialize)]
|
|
||||||
#[serde(default, deny_unknown_fields)]
|
|
||||||
pub struct OnDiskConfig {
|
|
||||||
pub pg_distrib_dir: PathBuf,
|
|
||||||
pub neon_distrib_dir: PathBuf,
|
|
||||||
pub default_tenant_id: Option<TenantId>,
|
|
||||||
pub private_key_path: PathBuf,
|
|
||||||
pub broker: NeonBroker,
|
|
||||||
pub storage_controller: NeonStorageControllerConf,
|
|
||||||
#[serde(
|
|
||||||
skip_serializing,
|
|
||||||
deserialize_with = "fail_if_pageservers_field_specified"
|
|
||||||
)]
|
|
||||||
pub pageservers: Vec<PageServerConf>,
|
|
||||||
pub safekeepers: Vec<SafekeeperConf>,
|
|
||||||
pub control_plane_api: Option<Url>,
|
|
||||||
pub control_plane_compute_hook_api: Option<Url>,
|
|
||||||
branch_name_mappings: HashMap<String, Vec<(TenantId, TimelineId)>>,
|
branch_name_mappings: HashMap<String, Vec<(TenantId, TimelineId)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fail_if_pageservers_field_specified<'de, D>(_: D) -> Result<Vec<PageServerConf>, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
Err(serde::de::Error::custom(
|
|
||||||
"The 'pageservers' field is no longer used; pageserver.toml is now authoritative; \
|
|
||||||
Please remove the `pageservers` from your .neon/config.",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The description of the neon_local env to be initialized by `neon_local init --config`.
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
pub struct NeonLocalInitConf {
|
|
||||||
// TODO: do we need this? Seems unused
|
|
||||||
pub pg_distrib_dir: Option<PathBuf>,
|
|
||||||
// TODO: do we need this? Seems unused
|
|
||||||
pub neon_distrib_dir: Option<PathBuf>,
|
|
||||||
pub default_tenant_id: TenantId,
|
|
||||||
pub broker: NeonBroker,
|
|
||||||
pub storage_controller: Option<NeonStorageControllerConf>,
|
|
||||||
pub pageservers: Vec<NeonLocalInitPageserverConf>,
|
|
||||||
pub safekeepers: Vec<SafekeeperConf>,
|
|
||||||
pub control_plane_api: Option<Option<Url>>,
|
|
||||||
pub control_plane_compute_hook_api: Option<Option<Url>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Broker config for cluster internal communication.
|
/// Broker config for cluster internal communication.
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -145,33 +98,6 @@ pub struct NeonBroker {
|
|||||||
pub listen_addr: SocketAddr,
|
pub listen_addr: SocketAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Broker config for cluster internal communication.
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
|
||||||
#[serde(default)]
|
|
||||||
pub struct NeonStorageControllerConf {
|
|
||||||
/// Heartbeat timeout before marking a node offline
|
|
||||||
#[serde(with = "humantime_serde")]
|
|
||||||
pub max_unavailable: Duration,
|
|
||||||
|
|
||||||
/// Threshold for auto-splitting a tenant into shards
|
|
||||||
pub split_threshold: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NeonStorageControllerConf {
|
|
||||||
// Use a shorter pageserver unavailability interval than the default to speed up tests.
|
|
||||||
const DEFAULT_MAX_UNAVAILABLE_INTERVAL: std::time::Duration =
|
|
||||||
std::time::Duration::from_secs(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for NeonStorageControllerConf {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
max_unavailable: Self::DEFAULT_MAX_UNAVAILABLE_INTERVAL,
|
|
||||||
split_threshold: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dummy Default impl to satisfy Deserialize derive.
|
// Dummy Default impl to satisfy Deserialize derive.
|
||||||
impl Default for NeonBroker {
|
impl Default for NeonBroker {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
@@ -187,18 +113,22 @@ impl NeonBroker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// neon_local needs to know this subset of pageserver configuration.
|
|
||||||
// For legacy reasons, this information is duplicated from `pageserver.toml` into `.neon/config`.
|
|
||||||
// It can get stale if `pageserver.toml` is changed.
|
|
||||||
// TODO(christian): don't store this at all in `.neon/config`, always load it from `pageserver.toml`
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct PageServerConf {
|
pub struct PageServerConf {
|
||||||
|
// node id
|
||||||
pub id: NodeId,
|
pub id: NodeId,
|
||||||
|
|
||||||
|
// Pageserver connection settings
|
||||||
pub listen_pg_addr: String,
|
pub listen_pg_addr: String,
|
||||||
pub listen_http_addr: String,
|
pub listen_http_addr: String,
|
||||||
|
|
||||||
|
// auth type used for the PG and HTTP ports
|
||||||
pub pg_auth_type: AuthType,
|
pub pg_auth_type: AuthType,
|
||||||
pub http_auth_type: AuthType,
|
pub http_auth_type: AuthType,
|
||||||
|
|
||||||
|
pub(crate) virtual_file_io_engine: Option<String>,
|
||||||
|
pub(crate) get_vectored_impl: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PageServerConf {
|
impl Default for PageServerConf {
|
||||||
@@ -209,40 +139,8 @@ impl Default for PageServerConf {
|
|||||||
listen_http_addr: String::new(),
|
listen_http_addr: String::new(),
|
||||||
pg_auth_type: AuthType::Trust,
|
pg_auth_type: AuthType::Trust,
|
||||||
http_auth_type: AuthType::Trust,
|
http_auth_type: AuthType::Trust,
|
||||||
}
|
virtual_file_io_engine: None,
|
||||||
}
|
get_vectored_impl: None,
|
||||||
}
|
|
||||||
|
|
||||||
/// The toml that can be passed to `neon_local init --config`.
|
|
||||||
/// This is a subset of the `pageserver.toml` configuration.
|
|
||||||
// TODO(christian): use pageserver_api::config::ConfigToml (PR #7656)
|
|
||||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct NeonLocalInitPageserverConf {
|
|
||||||
pub id: NodeId,
|
|
||||||
pub listen_pg_addr: String,
|
|
||||||
pub listen_http_addr: String,
|
|
||||||
pub pg_auth_type: AuthType,
|
|
||||||
pub http_auth_type: AuthType,
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub other: HashMap<String, toml::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&NeonLocalInitPageserverConf> for PageServerConf {
|
|
||||||
fn from(conf: &NeonLocalInitPageserverConf) -> Self {
|
|
||||||
let NeonLocalInitPageserverConf {
|
|
||||||
id,
|
|
||||||
listen_pg_addr,
|
|
||||||
listen_http_addr,
|
|
||||||
pg_auth_type,
|
|
||||||
http_auth_type,
|
|
||||||
other: _,
|
|
||||||
} = conf;
|
|
||||||
Self {
|
|
||||||
id: *id,
|
|
||||||
listen_pg_addr: listen_pg_addr.clone(),
|
|
||||||
listen_http_addr: listen_http_addr.clone(),
|
|
||||||
pg_auth_type: *pg_auth_type,
|
|
||||||
http_auth_type: *http_auth_type,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,7 +156,6 @@ pub struct SafekeeperConf {
|
|||||||
pub remote_storage: Option<String>,
|
pub remote_storage: Option<String>,
|
||||||
pub backup_threads: Option<u32>,
|
pub backup_threads: Option<u32>,
|
||||||
pub auth_enabled: bool,
|
pub auth_enabled: bool,
|
||||||
pub listen_addr: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SafekeeperConf {
|
impl Default for SafekeeperConf {
|
||||||
@@ -272,7 +169,6 @@ impl Default for SafekeeperConf {
|
|||||||
remote_storage: None,
|
remote_storage: None,
|
||||||
backup_threads: None,
|
backup_threads: None,
|
||||||
auth_enabled: false,
|
auth_enabled: false,
|
||||||
listen_addr: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,8 +326,44 @@ impl LocalEnv {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct `Self` from on-disk state.
|
/// Create a LocalEnv from a config file.
|
||||||
pub fn load_config(repopath: &Path) -> anyhow::Result<Self> {
|
///
|
||||||
|
/// Unlike 'load_config', this function fills in any defaults that are missing
|
||||||
|
/// from the config file.
|
||||||
|
pub fn parse_config(toml: &str) -> anyhow::Result<Self> {
|
||||||
|
let mut env: LocalEnv = toml::from_str(toml)?;
|
||||||
|
|
||||||
|
// Find postgres binaries.
|
||||||
|
// Follow POSTGRES_DISTRIB_DIR if set, otherwise look in "pg_install".
|
||||||
|
// Note that later in the code we assume, that distrib dirs follow the same pattern
|
||||||
|
// for all postgres versions.
|
||||||
|
if env.pg_distrib_dir == Path::new("") {
|
||||||
|
if let Some(postgres_bin) = env::var_os("POSTGRES_DISTRIB_DIR") {
|
||||||
|
env.pg_distrib_dir = postgres_bin.into();
|
||||||
|
} else {
|
||||||
|
let cwd = env::current_dir()?;
|
||||||
|
env.pg_distrib_dir = cwd.join("pg_install")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find neon binaries.
|
||||||
|
if env.neon_distrib_dir == Path::new("") {
|
||||||
|
env.neon_distrib_dir = env::current_exe()?.parent().unwrap().to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
if env.pageservers.is_empty() {
|
||||||
|
anyhow::bail!("Configuration must contain at least one pageserver");
|
||||||
|
}
|
||||||
|
|
||||||
|
env.base_data_dir = base_path();
|
||||||
|
|
||||||
|
Ok(env)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Locate and load config
|
||||||
|
pub fn load_config() -> anyhow::Result<Self> {
|
||||||
|
let repopath = base_path();
|
||||||
|
|
||||||
if !repopath.exists() {
|
if !repopath.exists() {
|
||||||
bail!(
|
bail!(
|
||||||
"Neon config is not found in {}. You need to run 'neon_local init' first",
|
"Neon config is not found in {}. You need to run 'neon_local init' first",
|
||||||
@@ -442,129 +374,38 @@ impl LocalEnv {
|
|||||||
// TODO: check that it looks like a neon repository
|
// TODO: check that it looks like a neon repository
|
||||||
|
|
||||||
// load and parse file
|
// load and parse file
|
||||||
let config_file_contents = fs::read_to_string(repopath.join("config"))?;
|
let config = fs::read_to_string(repopath.join("config"))?;
|
||||||
let on_disk_config: OnDiskConfig = toml::from_str(config_file_contents.as_str())?;
|
let mut env: LocalEnv = toml::from_str(config.as_str())?;
|
||||||
let mut env = {
|
|
||||||
let OnDiskConfig {
|
|
||||||
pg_distrib_dir,
|
|
||||||
neon_distrib_dir,
|
|
||||||
default_tenant_id,
|
|
||||||
private_key_path,
|
|
||||||
broker,
|
|
||||||
storage_controller,
|
|
||||||
pageservers,
|
|
||||||
safekeepers,
|
|
||||||
control_plane_api,
|
|
||||||
control_plane_compute_hook_api,
|
|
||||||
branch_name_mappings,
|
|
||||||
} = on_disk_config;
|
|
||||||
LocalEnv {
|
|
||||||
base_data_dir: repopath.to_owned(),
|
|
||||||
pg_distrib_dir,
|
|
||||||
neon_distrib_dir,
|
|
||||||
default_tenant_id,
|
|
||||||
private_key_path,
|
|
||||||
broker,
|
|
||||||
storage_controller,
|
|
||||||
pageservers,
|
|
||||||
safekeepers,
|
|
||||||
control_plane_api,
|
|
||||||
control_plane_compute_hook_api,
|
|
||||||
branch_name_mappings,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// The source of truth for pageserver configuration is the pageserver.toml.
|
env.base_data_dir = repopath;
|
||||||
assert!(
|
|
||||||
env.pageservers.is_empty(),
|
|
||||||
"we ensure this during deserialization"
|
|
||||||
);
|
|
||||||
env.pageservers = {
|
|
||||||
let iter = std::fs::read_dir(repopath).context("open dir")?;
|
|
||||||
let mut pageservers = Vec::new();
|
|
||||||
for res in iter {
|
|
||||||
let dentry = res?;
|
|
||||||
const PREFIX: &str = "pageserver_";
|
|
||||||
let dentry_name = dentry
|
|
||||||
.file_name()
|
|
||||||
.into_string()
|
|
||||||
.ok()
|
|
||||||
.with_context(|| format!("non-utf8 dentry: {:?}", dentry.path()))
|
|
||||||
.unwrap();
|
|
||||||
if !dentry_name.starts_with(PREFIX) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !dentry.file_type().context("determine file type")?.is_dir() {
|
|
||||||
anyhow::bail!("expected a directory, got {:?}", dentry.path());
|
|
||||||
}
|
|
||||||
let id = dentry_name[PREFIX.len()..]
|
|
||||||
.parse::<NodeId>()
|
|
||||||
.with_context(|| format!("parse id from {:?}", dentry.path()))?;
|
|
||||||
// TODO(christian): use pageserver_api::config::ConfigToml (PR #7656)
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
|
||||||
// (allow unknown fields, unlike PageServerConf)
|
|
||||||
struct PageserverConfigTomlSubset {
|
|
||||||
id: NodeId,
|
|
||||||
listen_pg_addr: String,
|
|
||||||
listen_http_addr: String,
|
|
||||||
pg_auth_type: AuthType,
|
|
||||||
http_auth_type: AuthType,
|
|
||||||
}
|
|
||||||
let config_toml_path = dentry.path().join("pageserver.toml");
|
|
||||||
let config_toml: PageserverConfigTomlSubset = toml_edit::de::from_str(
|
|
||||||
&std::fs::read_to_string(&config_toml_path)
|
|
||||||
.with_context(|| format!("read {:?}", config_toml_path))?,
|
|
||||||
)
|
|
||||||
.context("parse pageserver.toml")?;
|
|
||||||
let PageserverConfigTomlSubset {
|
|
||||||
id: config_toml_id,
|
|
||||||
listen_pg_addr,
|
|
||||||
listen_http_addr,
|
|
||||||
pg_auth_type,
|
|
||||||
http_auth_type,
|
|
||||||
} = config_toml;
|
|
||||||
let conf = PageServerConf {
|
|
||||||
id: {
|
|
||||||
anyhow::ensure!(
|
|
||||||
config_toml_id == id,
|
|
||||||
"id mismatch: config_toml.id={config_toml_id} id={id}",
|
|
||||||
);
|
|
||||||
id
|
|
||||||
},
|
|
||||||
listen_pg_addr,
|
|
||||||
listen_http_addr,
|
|
||||||
pg_auth_type,
|
|
||||||
http_auth_type,
|
|
||||||
};
|
|
||||||
pageservers.push(conf);
|
|
||||||
}
|
|
||||||
pageservers
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(env)
|
Ok(env)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn persist_config(&self) -> anyhow::Result<()> {
|
pub fn persist_config(&self, base_path: &Path) -> anyhow::Result<()> {
|
||||||
Self::persist_config_impl(
|
// Currently, the user first passes a config file with 'neon_local init --config=<path>'
|
||||||
&self.base_data_dir,
|
// We read that in, in `create_config`, and fill any missing defaults. Then it's saved
|
||||||
&OnDiskConfig {
|
// to .neon/config. TODO: We lose any formatting and comments along the way, which is
|
||||||
pg_distrib_dir: self.pg_distrib_dir.clone(),
|
// a bit sad.
|
||||||
neon_distrib_dir: self.neon_distrib_dir.clone(),
|
let mut conf_content = r#"# This file describes a local deployment of the page server
|
||||||
default_tenant_id: self.default_tenant_id,
|
# and safekeeeper node. It is read by the 'neon_local' command-line
|
||||||
private_key_path: self.private_key_path.clone(),
|
# utility.
|
||||||
broker: self.broker.clone(),
|
"#
|
||||||
storage_controller: self.storage_controller.clone(),
|
.to_string();
|
||||||
pageservers: vec![], // it's skip_serializing anyway
|
|
||||||
safekeepers: self.safekeepers.clone(),
|
// Convert the LocalEnv to a toml file.
|
||||||
control_plane_api: self.control_plane_api.clone(),
|
//
|
||||||
control_plane_compute_hook_api: self.control_plane_compute_hook_api.clone(),
|
// This could be as simple as this:
|
||||||
branch_name_mappings: self.branch_name_mappings.clone(),
|
//
|
||||||
},
|
// conf_content += &toml::to_string_pretty(env)?;
|
||||||
)
|
//
|
||||||
}
|
// But it results in a "values must be emitted before tables". I'm not sure
|
||||||
|
// why, AFAICS the table, i.e. 'safekeepers: Vec<SafekeeperConf>' is last.
|
||||||
|
// Maybe rust reorders the fields to squeeze avoid padding or something?
|
||||||
|
// In any case, converting to toml::Value first, and serializing that, works.
|
||||||
|
// See https://github.com/alexcrichton/toml-rs/issues/142
|
||||||
|
conf_content += &toml::to_string_pretty(&toml::Value::try_from(self)?)?;
|
||||||
|
|
||||||
pub fn persist_config_impl(base_path: &Path, config: &OnDiskConfig) -> anyhow::Result<()> {
|
|
||||||
let conf_content = &toml::to_string_pretty(config)?;
|
|
||||||
let target_config_path = base_path.join("config");
|
let target_config_path = base_path.join("config");
|
||||||
fs::write(&target_config_path, conf_content).with_context(|| {
|
fs::write(&target_config_path, conf_content).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
@@ -589,13 +430,17 @@ impl LocalEnv {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Materialize the [`NeonLocalInitConf`] to disk. Called during [`neon_local init`].
|
//
|
||||||
pub fn init(conf: NeonLocalInitConf, force: &InitForceMode) -> anyhow::Result<()> {
|
// Initialize a new Neon repository
|
||||||
let base_path = base_path();
|
//
|
||||||
assert_ne!(base_path, Path::new(""));
|
pub fn init(&mut self, pg_version: u32, force: &InitForceMode) -> anyhow::Result<()> {
|
||||||
let base_path = &base_path;
|
// check if config already exists
|
||||||
|
let base_path = &self.base_data_dir;
|
||||||
|
ensure!(
|
||||||
|
base_path != Path::new(""),
|
||||||
|
"repository base path is missing"
|
||||||
|
);
|
||||||
|
|
||||||
// create base_path dir
|
|
||||||
if base_path.exists() {
|
if base_path.exists() {
|
||||||
match force {
|
match force {
|
||||||
InitForceMode::MustNotExist => {
|
InitForceMode::MustNotExist => {
|
||||||
@@ -627,115 +472,74 @@ impl LocalEnv {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !self.pg_bin_dir(pg_version)?.join("postgres").exists() {
|
||||||
|
bail!(
|
||||||
|
"Can't find postgres binary at {}",
|
||||||
|
self.pg_bin_dir(pg_version)?.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for binary in ["pageserver", "safekeeper"] {
|
||||||
|
if !self.neon_distrib_dir.join(binary).exists() {
|
||||||
|
bail!(
|
||||||
|
"Can't find binary '{binary}' in neon distrib dir '{}'",
|
||||||
|
self.neon_distrib_dir.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !base_path.exists() {
|
if !base_path.exists() {
|
||||||
fs::create_dir(base_path)?;
|
fs::create_dir(base_path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let NeonLocalInitConf {
|
|
||||||
pg_distrib_dir,
|
|
||||||
neon_distrib_dir,
|
|
||||||
default_tenant_id,
|
|
||||||
broker,
|
|
||||||
storage_controller,
|
|
||||||
pageservers,
|
|
||||||
safekeepers,
|
|
||||||
control_plane_api,
|
|
||||||
control_plane_compute_hook_api,
|
|
||||||
} = conf;
|
|
||||||
|
|
||||||
// Find postgres binaries.
|
|
||||||
// Follow POSTGRES_DISTRIB_DIR if set, otherwise look in "pg_install".
|
|
||||||
// Note that later in the code we assume, that distrib dirs follow the same pattern
|
|
||||||
// for all postgres versions.
|
|
||||||
let pg_distrib_dir = pg_distrib_dir.unwrap_or_else(|| {
|
|
||||||
if let Some(postgres_bin) = env::var_os("POSTGRES_DISTRIB_DIR") {
|
|
||||||
postgres_bin.into()
|
|
||||||
} else {
|
|
||||||
let cwd = env::current_dir().unwrap();
|
|
||||||
cwd.join("pg_install")
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find neon binaries.
|
|
||||||
let neon_distrib_dir = neon_distrib_dir
|
|
||||||
.unwrap_or_else(|| env::current_exe().unwrap().parent().unwrap().to_owned());
|
|
||||||
|
|
||||||
// Generate keypair for JWT.
|
// Generate keypair for JWT.
|
||||||
//
|
//
|
||||||
// The keypair is only needed if authentication is enabled in any of the
|
// The keypair is only needed if authentication is enabled in any of the
|
||||||
// components. For convenience, we generate the keypair even if authentication
|
// components. For convenience, we generate the keypair even if authentication
|
||||||
// is not enabled, so that you can easily enable it after the initialization
|
// is not enabled, so that you can easily enable it after the initialization
|
||||||
// step.
|
// step. However, if the key generation fails, we treat it as non-fatal if
|
||||||
generate_auth_keys(
|
// authentication was not enabled.
|
||||||
base_path.join("auth_private_key.pem").as_path(),
|
if self.private_key_path == PathBuf::new() {
|
||||||
base_path.join("auth_public_key.pem").as_path(),
|
match generate_auth_keys(
|
||||||
)
|
base_path.join("auth_private_key.pem").as_path(),
|
||||||
.context("generate auth keys")?;
|
base_path.join("auth_public_key.pem").as_path(),
|
||||||
let private_key_path = PathBuf::from("auth_private_key.pem");
|
) {
|
||||||
|
Ok(()) => {
|
||||||
// create the runtime type because the remaining initialization code below needs
|
self.private_key_path = PathBuf::from("auth_private_key.pem");
|
||||||
// a LocalEnv instance op operation
|
}
|
||||||
// TODO: refactor to avoid this, LocalEnv should only be constructed from on-disk state
|
Err(e) => {
|
||||||
let env = LocalEnv {
|
if !self.auth_keys_needed() {
|
||||||
base_data_dir: base_path.clone(),
|
eprintln!("Could not generate keypair for JWT authentication: {e}");
|
||||||
pg_distrib_dir,
|
eprintln!("Continuing anyway because authentication was not enabled");
|
||||||
neon_distrib_dir,
|
self.private_key_path = PathBuf::from("auth_private_key.pem");
|
||||||
default_tenant_id: Some(default_tenant_id),
|
} else {
|
||||||
private_key_path,
|
return Err(e);
|
||||||
broker,
|
}
|
||||||
storage_controller: storage_controller.unwrap_or_default(),
|
}
|
||||||
pageservers: pageservers.iter().map(Into::into).collect(),
|
}
|
||||||
safekeepers,
|
|
||||||
control_plane_api: control_plane_api.unwrap_or_default(),
|
|
||||||
control_plane_compute_hook_api: control_plane_compute_hook_api.unwrap_or_default(),
|
|
||||||
branch_name_mappings: Default::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// create endpoints dir
|
|
||||||
fs::create_dir_all(env.endpoints_path())?;
|
|
||||||
|
|
||||||
// create safekeeper dirs
|
|
||||||
for safekeeper in &env.safekeepers {
|
|
||||||
fs::create_dir_all(SafekeeperNode::datadir_path_by_id(&env, safekeeper.id))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize pageserver state
|
fs::create_dir_all(self.endpoints_path())?;
|
||||||
for (i, ps) in pageservers.into_iter().enumerate() {
|
|
||||||
let runtime_ps = &env.pageservers[i];
|
for safekeeper in &self.safekeepers {
|
||||||
assert_eq!(&PageServerConf::from(&ps), runtime_ps);
|
fs::create_dir_all(SafekeeperNode::datadir_path_by_id(self, safekeeper.id))?;
|
||||||
fs::create_dir(env.pageserver_data_dir(ps.id))?;
|
|
||||||
PageServerNode::from_env(&env, runtime_ps)
|
|
||||||
.initialize(ps)
|
|
||||||
.context("pageserver init failed")?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup remote remote location for default LocalFs remote storage
|
self.persist_config(base_path)
|
||||||
std::fs::create_dir_all(env.base_data_dir.join(PAGESERVER_REMOTE_STORAGE_DIR))?;
|
}
|
||||||
|
|
||||||
env.persist_config()
|
fn auth_keys_needed(&self) -> bool {
|
||||||
|
self.pageservers.iter().any(|ps| {
|
||||||
|
ps.pg_auth_type == AuthType::NeonJWT || ps.http_auth_type == AuthType::NeonJWT
|
||||||
|
}) || self.safekeepers.iter().any(|sk| sk.auth_enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn base_path() -> PathBuf {
|
fn base_path() -> PathBuf {
|
||||||
let path = match std::env::var_os("NEON_REPO_DIR") {
|
match std::env::var_os("NEON_REPO_DIR") {
|
||||||
Some(val) => {
|
Some(val) => PathBuf::from(val),
|
||||||
let path = PathBuf::from(val);
|
None => PathBuf::from(".neon"),
|
||||||
if !path.is_absolute() {
|
}
|
||||||
// repeat the env var in the error because our default is always absolute
|
|
||||||
panic!("NEON_REPO_DIR must be an absolute path, got {path:?}");
|
|
||||||
}
|
|
||||||
path
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let pwd = std::env::current_dir()
|
|
||||||
// technically this can fail but it's quite unlikeley
|
|
||||||
.expect("determine current directory");
|
|
||||||
let pwd_abs = pwd.canonicalize().expect("canonicalize current directory");
|
|
||||||
pwd_abs.join(".neon")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
assert!(path.is_absolute());
|
|
||||||
path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a public/private key pair for JWT authentication
|
/// Generate a public/private key pair for JWT authentication
|
||||||
@@ -774,3 +578,31 @@ fn generate_auth_keys(private_key_path: &Path, public_key_path: &Path) -> anyhow
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_conf_parsing() {
|
||||||
|
let simple_conf_toml = include_str!("../simple.conf");
|
||||||
|
let simple_conf_parse_result = LocalEnv::parse_config(simple_conf_toml);
|
||||||
|
assert!(
|
||||||
|
simple_conf_parse_result.is_ok(),
|
||||||
|
"failed to parse simple config {simple_conf_toml}, reason: {simple_conf_parse_result:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let string_to_replace = "listen_addr = '127.0.0.1:50051'";
|
||||||
|
let spoiled_url_str = "listen_addr = '!@$XOXO%^&'";
|
||||||
|
let spoiled_url_toml = simple_conf_toml.replace(string_to_replace, spoiled_url_str);
|
||||||
|
assert!(
|
||||||
|
spoiled_url_toml.contains(spoiled_url_str),
|
||||||
|
"Failed to replace string {string_to_replace} in the toml file {simple_conf_toml}"
|
||||||
|
);
|
||||||
|
let spoiled_url_parse_result = LocalEnv::parse_config(&spoiled_url_toml);
|
||||||
|
assert!(
|
||||||
|
spoiled_url_parse_result.is_err(),
|
||||||
|
"expected toml with invalid Url {spoiled_url_toml} to fail the parsing, but got {spoiled_url_parse_result:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+116
-88
@@ -4,21 +4,21 @@
|
|||||||
//!
|
//!
|
||||||
//! .neon/
|
//! .neon/
|
||||||
//!
|
//!
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::num::NonZeroU64;
|
use std::num::NonZeroU64;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::process::Command;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use camino::Utf8PathBuf;
|
use camino::Utf8PathBuf;
|
||||||
use futures::SinkExt;
|
use futures::SinkExt;
|
||||||
use pageserver_api::models::{
|
use pageserver_api::models::{
|
||||||
self, AuxFilePolicy, LocationConfig, ShardParameters, TenantHistorySize, TenantInfo,
|
self, LocationConfig, ShardParameters, TenantHistorySize, TenantInfo, TimelineInfo,
|
||||||
TimelineInfo,
|
|
||||||
};
|
};
|
||||||
use pageserver_api::shard::TenantShardId;
|
use pageserver_api::shard::TenantShardId;
|
||||||
use pageserver_client::mgmt_api;
|
use pageserver_client::mgmt_api;
|
||||||
@@ -30,7 +30,7 @@ use utils::{
|
|||||||
lsn::Lsn,
|
lsn::Lsn,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::local_env::{NeonLocalInitPageserverConf, PageServerConf};
|
use crate::local_env::PageServerConf;
|
||||||
use crate::{background_process, local_env::LocalEnv};
|
use crate::{background_process, local_env::LocalEnv};
|
||||||
|
|
||||||
/// Directory within .neon which will be used by default for LocalFs remote storage.
|
/// Directory within .neon which will be used by default for LocalFs remote storage.
|
||||||
@@ -74,23 +74,57 @@ impl PageServerNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pageserver_init_make_toml(
|
/// Merge overrides provided by the user on the command line with our default overides derived from neon_local configuration.
|
||||||
&self,
|
///
|
||||||
conf: NeonLocalInitPageserverConf,
|
/// These all end up on the command line of the `pageserver` binary.
|
||||||
) -> anyhow::Result<toml_edit::Document> {
|
fn neon_local_overrides(&self, cli_overrides: &[&str]) -> Vec<String> {
|
||||||
assert_eq!(&PageServerConf::from(&conf), &self.conf, "during neon_local init, we derive the runtime state of ps conf (self.conf) from the --config flag fully");
|
|
||||||
|
|
||||||
// TODO(christian): instead of what we do here, create a pageserver_api::config::ConfigToml (PR #7656)
|
|
||||||
|
|
||||||
// FIXME: the paths should be shell-escaped to handle paths with spaces, quotas etc.
|
// FIXME: the paths should be shell-escaped to handle paths with spaces, quotas etc.
|
||||||
let pg_distrib_dir_param = format!(
|
let pg_distrib_dir_param = format!(
|
||||||
"pg_distrib_dir='{}'",
|
"pg_distrib_dir='{}'",
|
||||||
self.env.pg_distrib_dir_raw().display()
|
self.env.pg_distrib_dir_raw().display()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let PageServerConf {
|
||||||
|
id,
|
||||||
|
listen_pg_addr,
|
||||||
|
listen_http_addr,
|
||||||
|
pg_auth_type,
|
||||||
|
http_auth_type,
|
||||||
|
virtual_file_io_engine,
|
||||||
|
get_vectored_impl,
|
||||||
|
} = &self.conf;
|
||||||
|
|
||||||
|
let id = format!("id={}", id);
|
||||||
|
|
||||||
|
let http_auth_type_param = format!("http_auth_type='{}'", http_auth_type);
|
||||||
|
let listen_http_addr_param = format!("listen_http_addr='{}'", listen_http_addr);
|
||||||
|
|
||||||
|
let pg_auth_type_param = format!("pg_auth_type='{}'", pg_auth_type);
|
||||||
|
let listen_pg_addr_param = format!("listen_pg_addr='{}'", listen_pg_addr);
|
||||||
|
let virtual_file_io_engine = if let Some(virtual_file_io_engine) = virtual_file_io_engine {
|
||||||
|
format!("virtual_file_io_engine='{virtual_file_io_engine}'")
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let get_vectored_impl = if let Some(get_vectored_impl) = get_vectored_impl {
|
||||||
|
format!("get_vectored_impl='{get_vectored_impl}'")
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
let broker_endpoint_param = format!("broker_endpoint='{}'", self.env.broker.client_url());
|
let broker_endpoint_param = format!("broker_endpoint='{}'", self.env.broker.client_url());
|
||||||
|
|
||||||
let mut overrides = vec![pg_distrib_dir_param, broker_endpoint_param];
|
let mut overrides = vec![
|
||||||
|
id,
|
||||||
|
pg_distrib_dir_param,
|
||||||
|
http_auth_type_param,
|
||||||
|
pg_auth_type_param,
|
||||||
|
listen_http_addr_param,
|
||||||
|
listen_pg_addr_param,
|
||||||
|
broker_endpoint_param,
|
||||||
|
virtual_file_io_engine,
|
||||||
|
get_vectored_impl,
|
||||||
|
];
|
||||||
|
|
||||||
if let Some(control_plane_api) = &self.env.control_plane_api {
|
if let Some(control_plane_api) = &self.env.control_plane_api {
|
||||||
overrides.push(format!(
|
overrides.push(format!(
|
||||||
@@ -100,7 +134,7 @@ impl PageServerNode {
|
|||||||
|
|
||||||
// Storage controller uses the same auth as pageserver: if JWT is enabled
|
// Storage controller uses the same auth as pageserver: if JWT is enabled
|
||||||
// for us, we will also need it to talk to them.
|
// for us, we will also need it to talk to them.
|
||||||
if matches!(conf.http_auth_type, AuthType::NeonJWT) {
|
if matches!(http_auth_type, AuthType::NeonJWT) {
|
||||||
let jwt_token = self
|
let jwt_token = self
|
||||||
.env
|
.env
|
||||||
.generate_auth_token(&Claims::new(None, Scope::GenerationsApi))
|
.generate_auth_token(&Claims::new(None, Scope::GenerationsApi))
|
||||||
@@ -109,40 +143,31 @@ impl PageServerNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !conf.other.contains_key("remote_storage") {
|
if !cli_overrides
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.starts_with("remote_storage"))
|
||||||
|
{
|
||||||
overrides.push(format!(
|
overrides.push(format!(
|
||||||
"remote_storage={{local_path='../{PAGESERVER_REMOTE_STORAGE_DIR}'}}"
|
"remote_storage={{local_path='../{PAGESERVER_REMOTE_STORAGE_DIR}'}}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.http_auth_type != AuthType::Trust || conf.pg_auth_type != AuthType::Trust {
|
if *http_auth_type != AuthType::Trust || *pg_auth_type != AuthType::Trust {
|
||||||
// Keys are generated in the toplevel repo dir, pageservers' workdirs
|
// Keys are generated in the toplevel repo dir, pageservers' workdirs
|
||||||
// are one level below that, so refer to keys with ../
|
// are one level below that, so refer to keys with ../
|
||||||
overrides.push("auth_validation_public_key_path='../auth_public_key.pem'".to_owned());
|
overrides.push("auth_validation_public_key_path='../auth_public_key.pem'".to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the user-provided overrides
|
// Apply the user-provided overrides
|
||||||
overrides.push(
|
overrides.extend(cli_overrides.iter().map(|&c| c.to_owned()));
|
||||||
toml_edit::ser::to_string_pretty(&conf)
|
|
||||||
.expect("we deserialized this from toml earlier"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Turn `overrides` into a toml document.
|
overrides
|
||||||
// TODO: above code is legacy code, it should be refactored to use toml_edit directly.
|
|
||||||
let mut config_toml = toml_edit::Document::new();
|
|
||||||
for fragment_str in overrides {
|
|
||||||
let fragment = toml_edit::Document::from_str(&fragment_str)
|
|
||||||
.expect("all fragments in `overrides` are valid toml documents, this function controls that");
|
|
||||||
for (key, item) in fragment.iter() {
|
|
||||||
config_toml.insert(key, item.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(config_toml)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes a pageserver node by creating its config with the overrides provided.
|
/// Initializes a pageserver node by creating its config with the overrides provided.
|
||||||
pub fn initialize(&self, conf: NeonLocalInitPageserverConf) -> anyhow::Result<()> {
|
pub fn initialize(&self, config_overrides: &[&str]) -> anyhow::Result<()> {
|
||||||
self.pageserver_init(conf)
|
// First, run `pageserver --init` and wait for it to write a config into FS and exit.
|
||||||
|
self.pageserver_init(config_overrides)
|
||||||
.with_context(|| format!("Failed to run init for pageserver node {}", self.conf.id))
|
.with_context(|| format!("Failed to run init for pageserver node {}", self.conf.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,11 +183,11 @@ impl PageServerNode {
|
|||||||
.expect("non-Unicode path")
|
.expect("non-Unicode path")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(&self) -> anyhow::Result<()> {
|
pub async fn start(&self, config_overrides: &[&str]) -> anyhow::Result<()> {
|
||||||
self.start_node().await
|
self.start_node(config_overrides, false).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pageserver_init(&self, conf: NeonLocalInitPageserverConf) -> anyhow::Result<()> {
|
fn pageserver_init(&self, config_overrides: &[&str]) -> anyhow::Result<()> {
|
||||||
let datadir = self.repo_path();
|
let datadir = self.repo_path();
|
||||||
let node_id = self.conf.id;
|
let node_id = self.conf.id;
|
||||||
println!(
|
println!(
|
||||||
@@ -173,20 +198,29 @@ impl PageServerNode {
|
|||||||
);
|
);
|
||||||
io::stdout().flush()?;
|
io::stdout().flush()?;
|
||||||
|
|
||||||
let config = self
|
if !datadir.exists() {
|
||||||
.pageserver_init_make_toml(conf)
|
std::fs::create_dir(&datadir)?;
|
||||||
.context("make pageserver toml")?;
|
}
|
||||||
let config_file_path = datadir.join("pageserver.toml");
|
|
||||||
let mut config_file = std::fs::OpenOptions::new()
|
let datadir_path_str = datadir.to_str().with_context(|| {
|
||||||
.create_new(true)
|
format!("Cannot start pageserver node {node_id} in path that has no string representation: {datadir:?}")
|
||||||
.write(true)
|
})?;
|
||||||
.open(&config_file_path)
|
let mut args = self.pageserver_basic_args(config_overrides, datadir_path_str);
|
||||||
.with_context(|| format!("open pageserver toml for write: {config_file_path:?}"))?;
|
args.push(Cow::Borrowed("--init"));
|
||||||
config_file
|
|
||||||
.write_all(config.to_string().as_bytes())
|
let init_output = Command::new(self.env.pageserver_bin())
|
||||||
.context("write pageserver toml")?;
|
.args(args.iter().map(Cow::as_ref))
|
||||||
drop(config_file);
|
.envs(self.pageserver_env_variables()?)
|
||||||
// TODO: invoke a TBD config-check command to validate that pageserver will start with the written config
|
.output()
|
||||||
|
.with_context(|| format!("Failed to run pageserver init for node {node_id}"))?;
|
||||||
|
|
||||||
|
anyhow::ensure!(
|
||||||
|
init_output.status.success(),
|
||||||
|
"Pageserver init for node {} did not finish successfully, stdout: {}, stderr: {}",
|
||||||
|
node_id,
|
||||||
|
String::from_utf8_lossy(&init_output.stdout),
|
||||||
|
String::from_utf8_lossy(&init_output.stderr),
|
||||||
|
);
|
||||||
|
|
||||||
// Write metadata file, used by pageserver on startup to register itself with
|
// Write metadata file, used by pageserver on startup to register itself with
|
||||||
// the storage controller
|
// the storage controller
|
||||||
@@ -200,13 +234,12 @@ impl PageServerNode {
|
|||||||
// situation: the metadata is written by some other script.
|
// situation: the metadata is written by some other script.
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
metadata_path,
|
metadata_path,
|
||||||
serde_json::to_vec(&pageserver_api::config::NodeMetadata {
|
serde_json::to_vec(&serde_json::json!({
|
||||||
postgres_host: "localhost".to_string(),
|
"host": "localhost",
|
||||||
postgres_port: self.pg_connection_config.port(),
|
"port": self.pg_connection_config.port(),
|
||||||
http_host: "localhost".to_string(),
|
"http_host": "localhost",
|
||||||
http_port,
|
"http_port": http_port,
|
||||||
other: HashMap::new(),
|
}))
|
||||||
})
|
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
.expect("Failed to write metadata file");
|
.expect("Failed to write metadata file");
|
||||||
@@ -214,7 +247,11 @@ impl PageServerNode {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn start_node(&self) -> anyhow::Result<()> {
|
async fn start_node(
|
||||||
|
&self,
|
||||||
|
config_overrides: &[&str],
|
||||||
|
update_config: bool,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
// TODO: using a thread here because start_process() is not async but we need to call check_status()
|
// TODO: using a thread here because start_process() is not async but we need to call check_status()
|
||||||
let datadir = self.repo_path();
|
let datadir = self.repo_path();
|
||||||
print!(
|
print!(
|
||||||
@@ -231,12 +268,15 @@ impl PageServerNode {
|
|||||||
self.conf.id, datadir,
|
self.conf.id, datadir,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let args = vec!["-D", datadir_path_str];
|
let mut args = self.pageserver_basic_args(config_overrides, datadir_path_str);
|
||||||
|
if update_config {
|
||||||
|
args.push(Cow::Borrowed("--update-config"));
|
||||||
|
}
|
||||||
background_process::start_process(
|
background_process::start_process(
|
||||||
"pageserver",
|
"pageserver",
|
||||||
&datadir,
|
&datadir,
|
||||||
&self.env.pageserver_bin(),
|
&self.env.pageserver_bin(),
|
||||||
args,
|
args.iter().map(Cow::as_ref),
|
||||||
self.pageserver_env_variables()?,
|
self.pageserver_env_variables()?,
|
||||||
background_process::InitialPidFile::Expect(self.pid_file()),
|
background_process::InitialPidFile::Expect(self.pid_file()),
|
||||||
|| async {
|
|| async {
|
||||||
@@ -253,6 +293,22 @@ impl PageServerNode {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pageserver_basic_args<'a>(
|
||||||
|
&self,
|
||||||
|
config_overrides: &'a [&'a str],
|
||||||
|
datadir_path_str: &'a str,
|
||||||
|
) -> Vec<Cow<'a, str>> {
|
||||||
|
let mut args = vec![Cow::Borrowed("-D"), Cow::Borrowed(datadir_path_str)];
|
||||||
|
|
||||||
|
let overrides = self.neon_local_overrides(config_overrides);
|
||||||
|
for config_override in overrides {
|
||||||
|
args.push(Cow::Borrowed("-c"));
|
||||||
|
args.push(Cow::Owned(config_override));
|
||||||
|
}
|
||||||
|
|
||||||
|
args
|
||||||
|
}
|
||||||
|
|
||||||
fn pageserver_env_variables(&self) -> anyhow::Result<Vec<(String, String)>> {
|
fn pageserver_env_variables(&self) -> anyhow::Result<Vec<(String, String)>> {
|
||||||
// FIXME: why is this tied to pageserver's auth type? Whether or not the safekeeper
|
// FIXME: why is this tied to pageserver's auth type? Whether or not the safekeeper
|
||||||
// needs a token, and how to generate that token, seems independent to whether
|
// needs a token, and how to generate that token, seems independent to whether
|
||||||
@@ -333,10 +389,6 @@ impl PageServerNode {
|
|||||||
.remove("image_creation_threshold")
|
.remove("image_creation_threshold")
|
||||||
.map(|x| x.parse::<usize>())
|
.map(|x| x.parse::<usize>())
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
image_layer_creation_check_threshold: settings
|
|
||||||
.remove("image_layer_creation_check_threshold")
|
|
||||||
.map(|x| x.parse::<u8>())
|
|
||||||
.transpose()?,
|
|
||||||
pitr_interval: settings.remove("pitr_interval").map(|x| x.to_string()),
|
pitr_interval: settings.remove("pitr_interval").map(|x| x.to_string()),
|
||||||
walreceiver_connect_timeout: settings
|
walreceiver_connect_timeout: settings
|
||||||
.remove("walreceiver_connect_timeout")
|
.remove("walreceiver_connect_timeout")
|
||||||
@@ -378,15 +430,6 @@ impl PageServerNode {
|
|||||||
.map(serde_json::from_str)
|
.map(serde_json::from_str)
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("parse `timeline_get_throttle` from json")?,
|
.context("parse `timeline_get_throttle` from json")?,
|
||||||
switch_aux_file_policy: settings
|
|
||||||
.remove("switch_aux_file_policy")
|
|
||||||
.map(|x| x.parse::<AuxFilePolicy>())
|
|
||||||
.transpose()
|
|
||||||
.context("Failed to parse 'switch_aux_file_policy'")?,
|
|
||||||
lsn_lease_length: settings.remove("lsn_lease_length").map(|x| x.to_string()),
|
|
||||||
lsn_lease_length_for_ts: settings
|
|
||||||
.remove("lsn_lease_length_for_ts")
|
|
||||||
.map(|x| x.to_string()),
|
|
||||||
};
|
};
|
||||||
if !settings.is_empty() {
|
if !settings.is_empty() {
|
||||||
bail!("Unrecognized tenant settings: {settings:?}")
|
bail!("Unrecognized tenant settings: {settings:?}")
|
||||||
@@ -458,12 +501,6 @@ impl PageServerNode {
|
|||||||
.map(|x| x.parse::<usize>())
|
.map(|x| x.parse::<usize>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("Failed to parse 'image_creation_threshold' as non zero integer")?,
|
.context("Failed to parse 'image_creation_threshold' as non zero integer")?,
|
||||||
image_layer_creation_check_threshold: settings
|
|
||||||
.remove("image_layer_creation_check_threshold")
|
|
||||||
.map(|x| x.parse::<u8>())
|
|
||||||
.transpose()
|
|
||||||
.context("Failed to parse 'image_creation_check_threshold' as integer")?,
|
|
||||||
|
|
||||||
pitr_interval: settings.remove("pitr_interval").map(|x| x.to_string()),
|
pitr_interval: settings.remove("pitr_interval").map(|x| x.to_string()),
|
||||||
walreceiver_connect_timeout: settings
|
walreceiver_connect_timeout: settings
|
||||||
.remove("walreceiver_connect_timeout")
|
.remove("walreceiver_connect_timeout")
|
||||||
@@ -505,15 +542,6 @@ impl PageServerNode {
|
|||||||
.map(serde_json::from_str)
|
.map(serde_json::from_str)
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("parse `timeline_get_throttle` from json")?,
|
.context("parse `timeline_get_throttle` from json")?,
|
||||||
switch_aux_file_policy: settings
|
|
||||||
.remove("switch_aux_file_policy")
|
|
||||||
.map(|x| x.parse::<AuxFilePolicy>())
|
|
||||||
.transpose()
|
|
||||||
.context("Failed to parse 'switch_aux_file_policy'")?,
|
|
||||||
lsn_lease_length: settings.remove("lsn_lease_length").map(|x| x.to_string()),
|
|
||||||
lsn_lease_length_for_ts: settings
|
|
||||||
.remove("lsn_lease_length_for_ts")
|
|
||||||
.map(|x| x.to_string()),
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ use camino::Utf8PathBuf;
|
|||||||
use postgres_connection::PgConnectionConfig;
|
use postgres_connection::PgConnectionConfig;
|
||||||
use reqwest::{IntoUrl, Method};
|
use reqwest::{IntoUrl, Method};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use utils::auth::{Claims, Scope};
|
|
||||||
use utils::{http::error::HttpErrorBody, id::NodeId};
|
use utils::{http::error::HttpErrorBody, id::NodeId};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -71,31 +70,24 @@ pub struct SafekeeperNode {
|
|||||||
pub pg_connection_config: PgConnectionConfig,
|
pub pg_connection_config: PgConnectionConfig,
|
||||||
pub env: LocalEnv,
|
pub env: LocalEnv,
|
||||||
pub http_client: reqwest::Client,
|
pub http_client: reqwest::Client,
|
||||||
pub listen_addr: String,
|
|
||||||
pub http_base_url: String,
|
pub http_base_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SafekeeperNode {
|
impl SafekeeperNode {
|
||||||
pub fn from_env(env: &LocalEnv, conf: &SafekeeperConf) -> SafekeeperNode {
|
pub fn from_env(env: &LocalEnv, conf: &SafekeeperConf) -> SafekeeperNode {
|
||||||
let listen_addr = if let Some(ref listen_addr) = conf.listen_addr {
|
|
||||||
listen_addr.clone()
|
|
||||||
} else {
|
|
||||||
"127.0.0.1".to_string()
|
|
||||||
};
|
|
||||||
SafekeeperNode {
|
SafekeeperNode {
|
||||||
id: conf.id,
|
id: conf.id,
|
||||||
conf: conf.clone(),
|
conf: conf.clone(),
|
||||||
pg_connection_config: Self::safekeeper_connection_config(&listen_addr, conf.pg_port),
|
pg_connection_config: Self::safekeeper_connection_config(conf.pg_port),
|
||||||
env: env.clone(),
|
env: env.clone(),
|
||||||
http_client: reqwest::Client::new(),
|
http_client: reqwest::Client::new(),
|
||||||
http_base_url: format!("http://{}:{}/v1", listen_addr, conf.http_port),
|
http_base_url: format!("http://127.0.0.1:{}/v1", conf.http_port),
|
||||||
listen_addr,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct libpq connection string for connecting to this safekeeper.
|
/// Construct libpq connection string for connecting to this safekeeper.
|
||||||
fn safekeeper_connection_config(addr: &str, port: u16) -> PgConnectionConfig {
|
fn safekeeper_connection_config(port: u16) -> PgConnectionConfig {
|
||||||
PgConnectionConfig::new_host_port(url::Host::parse(addr).unwrap(), port)
|
PgConnectionConfig::new_host_port(url::Host::parse("127.0.0.1").unwrap(), port)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn datadir_path_by_id(env: &LocalEnv, sk_id: NodeId) -> PathBuf {
|
pub fn datadir_path_by_id(env: &LocalEnv, sk_id: NodeId) -> PathBuf {
|
||||||
@@ -119,8 +111,8 @@ impl SafekeeperNode {
|
|||||||
);
|
);
|
||||||
io::stdout().flush().unwrap();
|
io::stdout().flush().unwrap();
|
||||||
|
|
||||||
let listen_pg = format!("{}:{}", self.listen_addr, self.conf.pg_port);
|
let listen_pg = format!("127.0.0.1:{}", self.conf.pg_port);
|
||||||
let listen_http = format!("{}:{}", self.listen_addr, self.conf.http_port);
|
let listen_http = format!("127.0.0.1:{}", self.conf.http_port);
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let datadir = self.datadir_path();
|
let datadir = self.datadir_path();
|
||||||
|
|
||||||
@@ -147,7 +139,7 @@ impl SafekeeperNode {
|
|||||||
availability_zone,
|
availability_zone,
|
||||||
];
|
];
|
||||||
if let Some(pg_tenant_only_port) = self.conf.pg_tenant_only_port {
|
if let Some(pg_tenant_only_port) = self.conf.pg_tenant_only_port {
|
||||||
let listen_pg_tenant_only = format!("{}:{}", self.listen_addr, pg_tenant_only_port);
|
let listen_pg_tenant_only = format!("127.0.0.1:{}", pg_tenant_only_port);
|
||||||
args.extend(["--listen-pg-tenant-only".to_owned(), listen_pg_tenant_only]);
|
args.extend(["--listen-pg-tenant-only".to_owned(), listen_pg_tenant_only]);
|
||||||
}
|
}
|
||||||
if !self.conf.sync {
|
if !self.conf.sync {
|
||||||
@@ -198,7 +190,7 @@ impl SafekeeperNode {
|
|||||||
&datadir,
|
&datadir,
|
||||||
&self.env.safekeeper_bin(),
|
&self.env.safekeeper_bin(),
|
||||||
&args,
|
&args,
|
||||||
self.safekeeper_env_variables()?,
|
[],
|
||||||
background_process::InitialPidFile::Expect(self.pid_file()),
|
background_process::InitialPidFile::Expect(self.pid_file()),
|
||||||
|| async {
|
|| async {
|
||||||
match self.check_status().await {
|
match self.check_status().await {
|
||||||
@@ -211,18 +203,6 @@ impl SafekeeperNode {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn safekeeper_env_variables(&self) -> anyhow::Result<Vec<(String, String)>> {
|
|
||||||
// Generate a token to connect from safekeeper to peers
|
|
||||||
if self.conf.auth_enabled {
|
|
||||||
let token = self
|
|
||||||
.env
|
|
||||||
.generate_auth_token(&Claims::new(None, Scope::SafekeeperData))?;
|
|
||||||
Ok(vec![("SAFEKEEPER_AUTH_TOKEN".to_owned(), token)])
|
|
||||||
} else {
|
|
||||||
Ok(Vec::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Stop the server.
|
/// Stop the server.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use crate::{
|
use crate::{background_process, local_env::LocalEnv};
|
||||||
background_process,
|
|
||||||
local_env::{LocalEnv, NeonStorageControllerConf},
|
|
||||||
};
|
|
||||||
use camino::{Utf8Path, Utf8PathBuf};
|
use camino::{Utf8Path, Utf8PathBuf};
|
||||||
|
use hyper::Method;
|
||||||
use pageserver_api::{
|
use pageserver_api::{
|
||||||
controller_api::{
|
controller_api::{
|
||||||
NodeConfigureRequest, NodeRegisterRequest, TenantCreateResponse, TenantLocateResponse,
|
NodeConfigureRequest, NodeRegisterRequest, TenantCreateResponse, TenantLocateResponse,
|
||||||
@@ -16,7 +14,6 @@ use pageserver_api::{
|
|||||||
};
|
};
|
||||||
use pageserver_client::mgmt_api::ResponseErrorMessageExt;
|
use pageserver_client::mgmt_api::ResponseErrorMessageExt;
|
||||||
use postgres_backend::AuthType;
|
use postgres_backend::AuthType;
|
||||||
use reqwest::Method;
|
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use std::{fs, str::FromStr};
|
use std::{fs, str::FromStr};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
@@ -35,18 +32,19 @@ pub struct StorageController {
|
|||||||
public_key: Option<String>,
|
public_key: Option<String>,
|
||||||
postgres_port: u16,
|
postgres_port: u16,
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
config: NeonStorageControllerConf,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const COMMAND: &str = "storage_controller";
|
const COMMAND: &str = "storage_controller";
|
||||||
|
|
||||||
const STORAGE_CONTROLLER_POSTGRES_VERSION: u32 = 16;
|
const STORAGE_CONTROLLER_POSTGRES_VERSION: u32 = 16;
|
||||||
|
|
||||||
|
// Use a shorter pageserver unavailability interval than the default to speed up tests.
|
||||||
|
const NEON_LOCAL_MAX_UNAVAILABLE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(10);
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct AttachHookRequest {
|
pub struct AttachHookRequest {
|
||||||
pub tenant_shard_id: TenantShardId,
|
pub tenant_shard_id: TenantShardId,
|
||||||
pub node_id: Option<NodeId>,
|
pub node_id: Option<NodeId>,
|
||||||
pub generation_override: Option<i32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@@ -137,7 +135,6 @@ impl StorageController {
|
|||||||
client: reqwest::ClientBuilder::new()
|
client: reqwest::ClientBuilder::new()
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to construct http client"),
|
.expect("Failed to construct http client"),
|
||||||
config: env.storage_controller.clone(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,13 +241,9 @@ impl StorageController {
|
|||||||
anyhow::bail!("initdb failed with status {status}");
|
anyhow::bail!("initdb failed with status {status}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write a minimal config file:
|
|
||||||
// - Specify the port, since this is chosen dynamically
|
|
||||||
// - Switch off fsync, since we're running on lightweight test environments and when e.g. scale testing
|
|
||||||
// the storage controller we don't want a slow local disk to interfere with that.
|
|
||||||
tokio::fs::write(
|
tokio::fs::write(
|
||||||
&pg_data_path.join("postgresql.conf"),
|
&pg_data_path.join("postgresql.conf"),
|
||||||
format!("port = {}\nfsync=off\n", self.postgres_port),
|
format!("port = {}", self.postgres_port),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
};
|
};
|
||||||
@@ -279,6 +272,8 @@ impl StorageController {
|
|||||||
// Run migrations on every startup, in case something changed.
|
// Run migrations on every startup, in case something changed.
|
||||||
let database_url = self.setup_database().await?;
|
let database_url = self.setup_database().await?;
|
||||||
|
|
||||||
|
let max_unavailable: humantime::Duration = NEON_LOCAL_MAX_UNAVAILABLE_INTERVAL.into();
|
||||||
|
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
"-l",
|
"-l",
|
||||||
&self.listen,
|
&self.listen,
|
||||||
@@ -288,7 +283,7 @@ impl StorageController {
|
|||||||
"--database-url",
|
"--database-url",
|
||||||
&database_url,
|
&database_url,
|
||||||
"--max-unavailable-interval",
|
"--max-unavailable-interval",
|
||||||
&humantime::Duration::from(self.config.max_unavailable).to_string(),
|
&max_unavailable.to_string(),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
@@ -310,21 +305,15 @@ impl StorageController {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(split_threshold) = self.config.split_threshold.as_ref() {
|
|
||||||
args.push(format!("--split-threshold={split_threshold}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push(format!(
|
|
||||||
"--neon-local-repo-dir={}",
|
|
||||||
self.env.base_data_dir.display()
|
|
||||||
));
|
|
||||||
|
|
||||||
background_process::start_process(
|
background_process::start_process(
|
||||||
COMMAND,
|
COMMAND,
|
||||||
&self.env.base_data_dir,
|
&self.env.base_data_dir,
|
||||||
&self.env.storage_controller_bin(),
|
&self.env.storage_controller_bin(),
|
||||||
args,
|
args,
|
||||||
[],
|
[(
|
||||||
|
"NEON_REPO_DIR".to_string(),
|
||||||
|
self.env.base_data_dir.to_string_lossy().to_string(),
|
||||||
|
)],
|
||||||
background_process::InitialPidFile::Create(self.pid_file()),
|
background_process::InitialPidFile::Create(self.pid_file()),
|
||||||
|| async {
|
|| async {
|
||||||
match self.ready().await {
|
match self.ready().await {
|
||||||
@@ -390,7 +379,7 @@ impl StorageController {
|
|||||||
/// Simple HTTP request wrapper for calling into storage controller
|
/// Simple HTTP request wrapper for calling into storage controller
|
||||||
async fn dispatch<RQ, RS>(
|
async fn dispatch<RQ, RS>(
|
||||||
&self,
|
&self,
|
||||||
method: reqwest::Method,
|
method: hyper::Method,
|
||||||
path: String,
|
path: String,
|
||||||
body: Option<RQ>,
|
body: Option<RQ>,
|
||||||
) -> anyhow::Result<RS>
|
) -> anyhow::Result<RS>
|
||||||
@@ -443,7 +432,6 @@ impl StorageController {
|
|||||||
let request = AttachHookRequest {
|
let request = AttachHookRequest {
|
||||||
tenant_shard_id,
|
tenant_shard_id,
|
||||||
node_id: Some(pageserver_id),
|
node_id: Some(pageserver_id),
|
||||||
generation_override: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
@@ -484,16 +472,6 @@ impl StorageController {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
|
||||||
pub async fn tenant_import(&self, tenant_id: TenantId) -> anyhow::Result<TenantCreateResponse> {
|
|
||||||
self.dispatch::<(), TenantCreateResponse>(
|
|
||||||
Method::POST,
|
|
||||||
format!("debug/v1/tenant/{tenant_id}/import"),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub async fn tenant_locate(&self, tenant_id: TenantId) -> anyhow::Result<TenantLocateResponse> {
|
pub async fn tenant_locate(&self, tenant_id: TenantId) -> anyhow::Result<TenantLocateResponse> {
|
||||||
self.dispatch::<(), _>(
|
self.dispatch::<(), _>(
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "storcon_cli"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow.workspace = true
|
|
||||||
clap.workspace = true
|
|
||||||
comfy-table.workspace = true
|
|
||||||
futures.workspace = true
|
|
||||||
humantime.workspace = true
|
|
||||||
hyper.workspace = true
|
|
||||||
pageserver_api.workspace = true
|
|
||||||
pageserver_client.workspace = true
|
|
||||||
reqwest.workspace = true
|
|
||||||
serde.workspace = true
|
|
||||||
serde_json = { workspace = true, features = ["raw_value"] }
|
|
||||||
thiserror.workspace = true
|
|
||||||
tokio.workspace = true
|
|
||||||
tracing.workspace = true
|
|
||||||
utils.workspace = true
|
|
||||||
workspace_hack.workspace = true
|
|
||||||
|
|
||||||
@@ -1,948 +0,0 @@
|
|||||||
use futures::StreamExt;
|
|
||||||
use std::{collections::HashMap, str::FromStr, time::Duration};
|
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
|
||||||
use pageserver_api::{
|
|
||||||
controller_api::{
|
|
||||||
NodeAvailabilityWrapper, NodeDescribeResponse, ShardSchedulingPolicy,
|
|
||||||
TenantDescribeResponse, TenantPolicyRequest,
|
|
||||||
},
|
|
||||||
models::{
|
|
||||||
EvictionPolicy, EvictionPolicyLayerAccessThreshold, LocationConfigSecondary,
|
|
||||||
ShardParameters, TenantConfig, TenantConfigRequest, TenantCreateRequest,
|
|
||||||
TenantShardSplitRequest, TenantShardSplitResponse,
|
|
||||||
},
|
|
||||||
shard::{ShardStripeSize, TenantShardId},
|
|
||||||
};
|
|
||||||
use pageserver_client::mgmt_api::{self, ResponseErrorMessageExt};
|
|
||||||
use reqwest::{Method, StatusCode, Url};
|
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
|
||||||
use utils::id::{NodeId, TenantId};
|
|
||||||
|
|
||||||
use pageserver_api::controller_api::{
|
|
||||||
NodeConfigureRequest, NodeRegisterRequest, NodeSchedulingPolicy, PlacementPolicy,
|
|
||||||
TenantLocateResponse, TenantShardMigrateRequest, TenantShardMigrateResponse,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
|
||||||
enum Command {
|
|
||||||
/// Register a pageserver with the storage controller. This shouldn't usually be necessary,
|
|
||||||
/// since pageservers auto-register when they start up
|
|
||||||
NodeRegister {
|
|
||||||
#[arg(long)]
|
|
||||||
node_id: NodeId,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
listen_pg_addr: String,
|
|
||||||
#[arg(long)]
|
|
||||||
listen_pg_port: u16,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
listen_http_addr: String,
|
|
||||||
#[arg(long)]
|
|
||||||
listen_http_port: u16,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Modify a node's configuration in the storage controller
|
|
||||||
NodeConfigure {
|
|
||||||
#[arg(long)]
|
|
||||||
node_id: NodeId,
|
|
||||||
|
|
||||||
/// Availability is usually auto-detected based on heartbeats. Set 'offline' here to
|
|
||||||
/// manually mark a node offline
|
|
||||||
#[arg(long)]
|
|
||||||
availability: Option<NodeAvailabilityArg>,
|
|
||||||
/// Scheduling policy controls whether tenant shards may be scheduled onto this node.
|
|
||||||
#[arg(long)]
|
|
||||||
scheduling: Option<NodeSchedulingPolicy>,
|
|
||||||
},
|
|
||||||
/// Modify a tenant's policies in the storage controller
|
|
||||||
TenantPolicy {
|
|
||||||
#[arg(long)]
|
|
||||||
tenant_id: TenantId,
|
|
||||||
/// Placement policy controls whether a tenant is `detached`, has only a secondary location (`secondary`),
|
|
||||||
/// or is in the normal attached state with N secondary locations (`attached:N`)
|
|
||||||
#[arg(long)]
|
|
||||||
placement: Option<PlacementPolicyArg>,
|
|
||||||
/// Scheduling policy enables pausing the controller's scheduling activity involving this tenant. `active` is normal,
|
|
||||||
/// `essential` disables optimization scheduling changes, `pause` disables all scheduling changes, and `stop` prevents
|
|
||||||
/// all reconciliation activity including for scheduling changes already made. `pause` and `stop` can make a tenant
|
|
||||||
/// unavailable, and are only for use in emergencies.
|
|
||||||
#[arg(long)]
|
|
||||||
scheduling: Option<ShardSchedulingPolicyArg>,
|
|
||||||
},
|
|
||||||
/// List nodes known to the storage controller
|
|
||||||
Nodes {},
|
|
||||||
/// List tenants known to the storage controller
|
|
||||||
Tenants {},
|
|
||||||
/// Create a new tenant in the storage controller, and by extension on pageservers.
|
|
||||||
TenantCreate {
|
|
||||||
#[arg(long)]
|
|
||||||
tenant_id: TenantId,
|
|
||||||
},
|
|
||||||
/// Delete a tenant in the storage controller, and by extension on pageservers.
|
|
||||||
TenantDelete {
|
|
||||||
#[arg(long)]
|
|
||||||
tenant_id: TenantId,
|
|
||||||
},
|
|
||||||
/// Split an existing tenant into a higher number of shards than its current shard count.
|
|
||||||
TenantShardSplit {
|
|
||||||
#[arg(long)]
|
|
||||||
tenant_id: TenantId,
|
|
||||||
#[arg(long)]
|
|
||||||
shard_count: u8,
|
|
||||||
/// Optional, in 8kiB pages. e.g. set 2048 for 16MB stripes.
|
|
||||||
#[arg(long)]
|
|
||||||
stripe_size: Option<u32>,
|
|
||||||
},
|
|
||||||
/// Migrate the attached location for a tenant shard to a specific pageserver.
|
|
||||||
TenantShardMigrate {
|
|
||||||
#[arg(long)]
|
|
||||||
tenant_shard_id: TenantShardId,
|
|
||||||
#[arg(long)]
|
|
||||||
node: NodeId,
|
|
||||||
},
|
|
||||||
/// Modify the pageserver tenant configuration of a tenant: this is the configuration structure
|
|
||||||
/// that is passed through to pageservers, and does not affect storage controller behavior.
|
|
||||||
TenantConfig {
|
|
||||||
#[arg(long)]
|
|
||||||
tenant_id: TenantId,
|
|
||||||
#[arg(long)]
|
|
||||||
config: String,
|
|
||||||
},
|
|
||||||
/// Attempt to balance the locations for a tenant across pageservers. This is a client-side
|
|
||||||
/// alternative to the storage controller's scheduling optimization behavior.
|
|
||||||
TenantScatter {
|
|
||||||
#[arg(long)]
|
|
||||||
tenant_id: TenantId,
|
|
||||||
},
|
|
||||||
/// Print details about a particular tenant, including all its shards' states.
|
|
||||||
TenantDescribe {
|
|
||||||
#[arg(long)]
|
|
||||||
tenant_id: TenantId,
|
|
||||||
},
|
|
||||||
/// For a tenant which hasn't been onboarded to the storage controller yet, add it in secondary
|
|
||||||
/// mode so that it can warm up content on a pageserver.
|
|
||||||
TenantWarmup {
|
|
||||||
#[arg(long)]
|
|
||||||
tenant_id: TenantId,
|
|
||||||
},
|
|
||||||
/// Uncleanly drop a tenant from the storage controller: this doesn't delete anything from pageservers. Appropriate
|
|
||||||
/// if you e.g. used `tenant-warmup` by mistake on a tenant ID that doesn't really exist, or is in some other region.
|
|
||||||
TenantDrop {
|
|
||||||
#[arg(long)]
|
|
||||||
tenant_id: TenantId,
|
|
||||||
#[arg(long)]
|
|
||||||
unclean: bool,
|
|
||||||
},
|
|
||||||
NodeDrop {
|
|
||||||
#[arg(long)]
|
|
||||||
node_id: NodeId,
|
|
||||||
#[arg(long)]
|
|
||||||
unclean: bool,
|
|
||||||
},
|
|
||||||
TenantSetTimeBasedEviction {
|
|
||||||
#[arg(long)]
|
|
||||||
tenant_id: TenantId,
|
|
||||||
#[arg(long)]
|
|
||||||
period: humantime::Duration,
|
|
||||||
#[arg(long)]
|
|
||||||
threshold: humantime::Duration,
|
|
||||||
},
|
|
||||||
// Drain a set of specified pageservers by moving the primary attachments to pageservers
|
|
||||||
// outside of the specified set.
|
|
||||||
Drain {
|
|
||||||
// Set of pageserver node ids to drain.
|
|
||||||
#[arg(long)]
|
|
||||||
nodes: Vec<NodeId>,
|
|
||||||
// Optional: migration concurrency (default is 8)
|
|
||||||
#[arg(long)]
|
|
||||||
concurrency: Option<usize>,
|
|
||||||
// Optional: maximum number of shards to migrate
|
|
||||||
#[arg(long)]
|
|
||||||
max_shards: Option<usize>,
|
|
||||||
// Optional: when set to true, nothing is migrated, but the plan is printed to stdout
|
|
||||||
#[arg(long)]
|
|
||||||
dry_run: Option<bool>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(
|
|
||||||
author,
|
|
||||||
version,
|
|
||||||
about,
|
|
||||||
long_about = "CLI for Storage Controller Support/Debug"
|
|
||||||
)]
|
|
||||||
#[command(arg_required_else_help(true))]
|
|
||||||
struct Cli {
|
|
||||||
#[arg(long)]
|
|
||||||
/// URL to storage controller. e.g. http://127.0.0.1:1234 when using `neon_local`
|
|
||||||
api: Url,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
/// JWT token for authenticating with storage controller. Depending on the API used, this
|
|
||||||
/// should have either `pageserverapi` or `admin` scopes: for convenience, you should mint
|
|
||||||
/// a token with both scopes to use with this tool.
|
|
||||||
jwt: Option<String>,
|
|
||||||
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: Command,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct PlacementPolicyArg(PlacementPolicy);
|
|
||||||
|
|
||||||
impl FromStr for PlacementPolicyArg {
|
|
||||||
type Err = anyhow::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s {
|
|
||||||
"detached" => Ok(Self(PlacementPolicy::Detached)),
|
|
||||||
"secondary" => Ok(Self(PlacementPolicy::Secondary)),
|
|
||||||
_ if s.starts_with("attached:") => {
|
|
||||||
let mut splitter = s.split(':');
|
|
||||||
let _prefix = splitter.next().unwrap();
|
|
||||||
match splitter.next().and_then(|s| s.parse::<usize>().ok()) {
|
|
||||||
Some(n) => Ok(Self(PlacementPolicy::Attached(n))),
|
|
||||||
None => Err(anyhow::anyhow!(
|
|
||||||
"Invalid format '{s}', a valid example is 'attached:1'"
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(anyhow::anyhow!(
|
|
||||||
"Unknown placement policy '{s}', try detached,secondary,attached:<n>"
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct ShardSchedulingPolicyArg(ShardSchedulingPolicy);
|
|
||||||
|
|
||||||
impl FromStr for ShardSchedulingPolicyArg {
|
|
||||||
type Err = anyhow::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s {
|
|
||||||
"active" => Ok(Self(ShardSchedulingPolicy::Active)),
|
|
||||||
"essential" => Ok(Self(ShardSchedulingPolicy::Essential)),
|
|
||||||
"pause" => Ok(Self(ShardSchedulingPolicy::Pause)),
|
|
||||||
"stop" => Ok(Self(ShardSchedulingPolicy::Stop)),
|
|
||||||
_ => Err(anyhow::anyhow!(
|
|
||||||
"Unknown scheduling policy '{s}', try active,essential,pause,stop"
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct NodeAvailabilityArg(NodeAvailabilityWrapper);
|
|
||||||
|
|
||||||
impl FromStr for NodeAvailabilityArg {
|
|
||||||
type Err = anyhow::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s {
|
|
||||||
"active" => Ok(Self(NodeAvailabilityWrapper::Active)),
|
|
||||||
"offline" => Ok(Self(NodeAvailabilityWrapper::Offline)),
|
|
||||||
_ => Err(anyhow::anyhow!("Unknown availability state '{s}'")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Client {
|
|
||||||
base_url: Url,
|
|
||||||
jwt_token: Option<String>,
|
|
||||||
client: reqwest::Client,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
|
||||||
fn new(base_url: Url, jwt_token: Option<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
base_url,
|
|
||||||
jwt_token,
|
|
||||||
client: reqwest::ClientBuilder::new()
|
|
||||||
.build()
|
|
||||||
.expect("Failed to construct http client"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Simple HTTP request wrapper for calling into storage controller
|
|
||||||
async fn dispatch<RQ, RS>(
|
|
||||||
&self,
|
|
||||||
method: Method,
|
|
||||||
path: String,
|
|
||||||
body: Option<RQ>,
|
|
||||||
) -> mgmt_api::Result<RS>
|
|
||||||
where
|
|
||||||
RQ: Serialize + Sized,
|
|
||||||
RS: DeserializeOwned + Sized,
|
|
||||||
{
|
|
||||||
// The configured URL has the /upcall path prefix for pageservers to use: we will strip that out
|
|
||||||
// for general purpose API access.
|
|
||||||
let url = Url::from_str(&format!(
|
|
||||||
"http://{}:{}/{path}",
|
|
||||||
self.base_url.host_str().unwrap(),
|
|
||||||
self.base_url.port().unwrap()
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut builder = self.client.request(method, url);
|
|
||||||
if let Some(body) = body {
|
|
||||||
builder = builder.json(&body)
|
|
||||||
}
|
|
||||||
if let Some(jwt_token) = &self.jwt_token {
|
|
||||||
builder = builder.header(
|
|
||||||
reqwest::header::AUTHORIZATION,
|
|
||||||
format!("Bearer {jwt_token}"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = builder.send().await.map_err(mgmt_api::Error::ReceiveBody)?;
|
|
||||||
let response = response.error_from_body().await?;
|
|
||||||
|
|
||||||
response
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.map_err(pageserver_client::mgmt_api::Error::ReceiveBody)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> anyhow::Result<()> {
|
|
||||||
let cli = Cli::parse();
|
|
||||||
|
|
||||||
let storcon_client = Client::new(cli.api.clone(), cli.jwt.clone());
|
|
||||||
|
|
||||||
let mut trimmed = cli.api.to_string();
|
|
||||||
trimmed.pop();
|
|
||||||
let vps_client = mgmt_api::Client::new(trimmed, cli.jwt.as_deref());
|
|
||||||
|
|
||||||
match cli.command {
|
|
||||||
Command::NodeRegister {
|
|
||||||
node_id,
|
|
||||||
listen_pg_addr,
|
|
||||||
listen_pg_port,
|
|
||||||
listen_http_addr,
|
|
||||||
listen_http_port,
|
|
||||||
} => {
|
|
||||||
storcon_client
|
|
||||||
.dispatch::<_, ()>(
|
|
||||||
Method::POST,
|
|
||||||
"control/v1/node".to_string(),
|
|
||||||
Some(NodeRegisterRequest {
|
|
||||||
node_id,
|
|
||||||
listen_pg_addr,
|
|
||||||
listen_pg_port,
|
|
||||||
listen_http_addr,
|
|
||||||
listen_http_port,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Command::TenantCreate { tenant_id } => {
|
|
||||||
vps_client
|
|
||||||
.tenant_create(&TenantCreateRequest {
|
|
||||||
new_tenant_id: TenantShardId::unsharded(tenant_id),
|
|
||||||
generation: None,
|
|
||||||
shard_parameters: ShardParameters::default(),
|
|
||||||
placement_policy: Some(PlacementPolicy::Attached(1)),
|
|
||||||
config: TenantConfig::default(),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Command::TenantDelete { tenant_id } => {
|
|
||||||
let status = vps_client
|
|
||||||
.tenant_delete(TenantShardId::unsharded(tenant_id))
|
|
||||||
.await?;
|
|
||||||
tracing::info!("Delete status: {}", status);
|
|
||||||
}
|
|
||||||
Command::Nodes {} => {
|
|
||||||
let resp = storcon_client
|
|
||||||
.dispatch::<(), Vec<NodeDescribeResponse>>(
|
|
||||||
Method::GET,
|
|
||||||
"control/v1/node".to_string(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let mut table = comfy_table::Table::new();
|
|
||||||
table.set_header(["Id", "Hostname", "Scheduling", "Availability"]);
|
|
||||||
for node in resp {
|
|
||||||
table.add_row([
|
|
||||||
format!("{}", node.id),
|
|
||||||
node.listen_http_addr,
|
|
||||||
format!("{:?}", node.scheduling),
|
|
||||||
format!("{:?}", node.availability),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
println!("{table}");
|
|
||||||
}
|
|
||||||
Command::NodeConfigure {
|
|
||||||
node_id,
|
|
||||||
availability,
|
|
||||||
scheduling,
|
|
||||||
} => {
|
|
||||||
let req = NodeConfigureRequest {
|
|
||||||
node_id,
|
|
||||||
availability: availability.map(|a| a.0),
|
|
||||||
scheduling,
|
|
||||||
};
|
|
||||||
storcon_client
|
|
||||||
.dispatch::<_, ()>(
|
|
||||||
Method::PUT,
|
|
||||||
format!("control/v1/node/{node_id}/config"),
|
|
||||||
Some(req),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Command::Tenants {} => {
|
|
||||||
let resp = storcon_client
|
|
||||||
.dispatch::<(), Vec<TenantDescribeResponse>>(
|
|
||||||
Method::GET,
|
|
||||||
"control/v1/tenant".to_string(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let mut table = comfy_table::Table::new();
|
|
||||||
table.set_header([
|
|
||||||
"TenantId",
|
|
||||||
"ShardCount",
|
|
||||||
"StripeSize",
|
|
||||||
"Placement",
|
|
||||||
"Scheduling",
|
|
||||||
]);
|
|
||||||
for tenant in resp {
|
|
||||||
let shard_zero = tenant.shards.into_iter().next().unwrap();
|
|
||||||
table.add_row([
|
|
||||||
format!("{}", tenant.tenant_id),
|
|
||||||
format!("{}", shard_zero.tenant_shard_id.shard_count.literal()),
|
|
||||||
format!("{:?}", tenant.stripe_size),
|
|
||||||
format!("{:?}", tenant.policy),
|
|
||||||
format!("{:?}", shard_zero.scheduling_policy),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{table}");
|
|
||||||
}
|
|
||||||
Command::TenantPolicy {
|
|
||||||
tenant_id,
|
|
||||||
placement,
|
|
||||||
scheduling,
|
|
||||||
} => {
|
|
||||||
let req = TenantPolicyRequest {
|
|
||||||
scheduling: scheduling.map(|s| s.0),
|
|
||||||
placement: placement.map(|p| p.0),
|
|
||||||
};
|
|
||||||
storcon_client
|
|
||||||
.dispatch::<_, ()>(
|
|
||||||
Method::PUT,
|
|
||||||
format!("control/v1/tenant/{tenant_id}/policy"),
|
|
||||||
Some(req),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Command::TenantShardSplit {
|
|
||||||
tenant_id,
|
|
||||||
shard_count,
|
|
||||||
stripe_size,
|
|
||||||
} => {
|
|
||||||
let req = TenantShardSplitRequest {
|
|
||||||
new_shard_count: shard_count,
|
|
||||||
new_stripe_size: stripe_size.map(ShardStripeSize),
|
|
||||||
};
|
|
||||||
|
|
||||||
let response = storcon_client
|
|
||||||
.dispatch::<TenantShardSplitRequest, TenantShardSplitResponse>(
|
|
||||||
Method::PUT,
|
|
||||||
format!("control/v1/tenant/{tenant_id}/shard_split"),
|
|
||||||
Some(req),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
println!(
|
|
||||||
"Split tenant {} into {} shards: {}",
|
|
||||||
tenant_id,
|
|
||||||
shard_count,
|
|
||||||
response
|
|
||||||
.new_shards
|
|
||||||
.iter()
|
|
||||||
.map(|s| format!("{:?}", s))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(",")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Command::TenantShardMigrate {
|
|
||||||
tenant_shard_id,
|
|
||||||
node,
|
|
||||||
} => {
|
|
||||||
let req = TenantShardMigrateRequest {
|
|
||||||
tenant_shard_id,
|
|
||||||
node_id: node,
|
|
||||||
};
|
|
||||||
|
|
||||||
storcon_client
|
|
||||||
.dispatch::<TenantShardMigrateRequest, TenantShardMigrateResponse>(
|
|
||||||
Method::PUT,
|
|
||||||
format!("control/v1/tenant/{tenant_shard_id}/migrate"),
|
|
||||||
Some(req),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Command::TenantConfig { tenant_id, config } => {
|
|
||||||
let tenant_conf = serde_json::from_str(&config)?;
|
|
||||||
|
|
||||||
vps_client
|
|
||||||
.tenant_config(&TenantConfigRequest {
|
|
||||||
tenant_id,
|
|
||||||
config: tenant_conf,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Command::TenantScatter { tenant_id } => {
|
|
||||||
// Find the shards
|
|
||||||
let locate_response = storcon_client
|
|
||||||
.dispatch::<(), TenantLocateResponse>(
|
|
||||||
Method::GET,
|
|
||||||
format!("control/v1/tenant/{tenant_id}/locate"),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let shards = locate_response.shards;
|
|
||||||
|
|
||||||
let mut node_to_shards: HashMap<NodeId, Vec<TenantShardId>> = HashMap::new();
|
|
||||||
let shard_count = shards.len();
|
|
||||||
for s in shards {
|
|
||||||
let entry = node_to_shards.entry(s.node_id).or_default();
|
|
||||||
entry.push(s.shard_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load list of available nodes
|
|
||||||
let nodes_resp = storcon_client
|
|
||||||
.dispatch::<(), Vec<NodeDescribeResponse>>(
|
|
||||||
Method::GET,
|
|
||||||
"control/v1/node".to_string(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for node in nodes_resp {
|
|
||||||
if matches!(node.availability, NodeAvailabilityWrapper::Active) {
|
|
||||||
node_to_shards.entry(node.id).or_default();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let max_shard_per_node = shard_count / node_to_shards.len();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let mut migrate_shard = None;
|
|
||||||
for shards in node_to_shards.values_mut() {
|
|
||||||
if shards.len() > max_shard_per_node {
|
|
||||||
// Pick the emptiest
|
|
||||||
migrate_shard = Some(shards.pop().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let Some(migrate_shard) = migrate_shard else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pick the emptiest node to migrate to
|
|
||||||
let mut destinations = node_to_shards
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| (k, v.len()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
destinations.sort_by_key(|i| i.1);
|
|
||||||
let (destination_node, destination_count) = *destinations.first().unwrap();
|
|
||||||
if destination_count + 1 > max_shard_per_node {
|
|
||||||
// Even the emptiest destination doesn't have space: we're done
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let destination_node = *destination_node;
|
|
||||||
|
|
||||||
node_to_shards
|
|
||||||
.get_mut(&destination_node)
|
|
||||||
.unwrap()
|
|
||||||
.push(migrate_shard);
|
|
||||||
|
|
||||||
println!("Migrate {} -> {} ...", migrate_shard, destination_node);
|
|
||||||
|
|
||||||
storcon_client
|
|
||||||
.dispatch::<TenantShardMigrateRequest, TenantShardMigrateResponse>(
|
|
||||||
Method::PUT,
|
|
||||||
format!("control/v1/tenant/{migrate_shard}/migrate"),
|
|
||||||
Some(TenantShardMigrateRequest {
|
|
||||||
tenant_shard_id: migrate_shard,
|
|
||||||
node_id: destination_node,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
println!("Migrate {} -> {} OK", migrate_shard, destination_node);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spread the shards across the nodes
|
|
||||||
}
|
|
||||||
Command::TenantDescribe { tenant_id } => {
|
|
||||||
let describe_response = storcon_client
|
|
||||||
.dispatch::<(), TenantDescribeResponse>(
|
|
||||||
Method::GET,
|
|
||||||
format!("control/v1/tenant/{tenant_id}"),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let shards = describe_response.shards;
|
|
||||||
let mut table = comfy_table::Table::new();
|
|
||||||
table.set_header(["Shard", "Attached", "Secondary", "Last error", "status"]);
|
|
||||||
for shard in shards {
|
|
||||||
let secondary = shard
|
|
||||||
.node_secondary
|
|
||||||
.iter()
|
|
||||||
.map(|n| format!("{}", n))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(",");
|
|
||||||
|
|
||||||
let mut status_parts = Vec::new();
|
|
||||||
if shard.is_reconciling {
|
|
||||||
status_parts.push("reconciling");
|
|
||||||
}
|
|
||||||
|
|
||||||
if shard.is_pending_compute_notification {
|
|
||||||
status_parts.push("pending_compute");
|
|
||||||
}
|
|
||||||
|
|
||||||
if shard.is_splitting {
|
|
||||||
status_parts.push("splitting");
|
|
||||||
}
|
|
||||||
let status = status_parts.join(",");
|
|
||||||
|
|
||||||
table.add_row([
|
|
||||||
format!("{}", shard.tenant_shard_id),
|
|
||||||
shard
|
|
||||||
.node_attached
|
|
||||||
.map(|n| format!("{}", n))
|
|
||||||
.unwrap_or(String::new()),
|
|
||||||
secondary,
|
|
||||||
shard.last_error,
|
|
||||||
status,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
println!("{table}");
|
|
||||||
}
|
|
||||||
Command::TenantWarmup { tenant_id } => {
|
|
||||||
let describe_response = storcon_client
|
|
||||||
.dispatch::<(), TenantDescribeResponse>(
|
|
||||||
Method::GET,
|
|
||||||
format!("control/v1/tenant/{tenant_id}"),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
match describe_response {
|
|
||||||
Ok(describe) => {
|
|
||||||
if matches!(describe.policy, PlacementPolicy::Secondary) {
|
|
||||||
// Fine: it's already known to controller in secondary mode: calling
|
|
||||||
// again to put it into secondary mode won't cause problems.
|
|
||||||
} else {
|
|
||||||
anyhow::bail!("Tenant already present with policy {:?}", describe.policy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(mgmt_api::Error::ApiError(StatusCode::NOT_FOUND, _)) => {
|
|
||||||
// Fine: this tenant isn't know to the storage controller yet.
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
// Unexpected API error
|
|
||||||
return Err(e.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vps_client
|
|
||||||
.location_config(
|
|
||||||
TenantShardId::unsharded(tenant_id),
|
|
||||||
pageserver_api::models::LocationConfig {
|
|
||||||
mode: pageserver_api::models::LocationConfigMode::Secondary,
|
|
||||||
generation: None,
|
|
||||||
secondary_conf: Some(LocationConfigSecondary { warm: true }),
|
|
||||||
shard_number: 0,
|
|
||||||
shard_count: 0,
|
|
||||||
shard_stripe_size: ShardParameters::DEFAULT_STRIPE_SIZE.0,
|
|
||||||
tenant_conf: TenantConfig::default(),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let describe_response = storcon_client
|
|
||||||
.dispatch::<(), TenantDescribeResponse>(
|
|
||||||
Method::GET,
|
|
||||||
format!("control/v1/tenant/{tenant_id}"),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let secondary_ps_id = describe_response
|
|
||||||
.shards
|
|
||||||
.first()
|
|
||||||
.unwrap()
|
|
||||||
.node_secondary
|
|
||||||
.first()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
println!("Tenant {tenant_id} warming up on pageserver {secondary_ps_id}");
|
|
||||||
loop {
|
|
||||||
let (status, progress) = vps_client
|
|
||||||
.tenant_secondary_download(
|
|
||||||
TenantShardId::unsharded(tenant_id),
|
|
||||||
Some(Duration::from_secs(10)),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
println!(
|
|
||||||
"Progress: {}/{} layers, {}/{} bytes",
|
|
||||||
progress.layers_downloaded,
|
|
||||||
progress.layers_total,
|
|
||||||
progress.bytes_downloaded,
|
|
||||||
progress.bytes_total
|
|
||||||
);
|
|
||||||
match status {
|
|
||||||
StatusCode::OK => {
|
|
||||||
println!("Download complete");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
StatusCode::ACCEPTED => {
|
|
||||||
// Loop
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
anyhow::bail!("Unexpected download status: {status}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::TenantDrop { tenant_id, unclean } => {
|
|
||||||
if !unclean {
|
|
||||||
anyhow::bail!("This command is not a tenant deletion, and uncleanly drops all controller state for the tenant. If you know what you're doing, add `--unclean` to proceed.")
|
|
||||||
}
|
|
||||||
storcon_client
|
|
||||||
.dispatch::<(), ()>(
|
|
||||||
Method::POST,
|
|
||||||
format!("debug/v1/tenant/{tenant_id}/drop"),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Command::NodeDrop { node_id, unclean } => {
|
|
||||||
if !unclean {
|
|
||||||
anyhow::bail!("This command is not a clean node decommission, and uncleanly drops all controller state for the node, without checking if any tenants still refer to it. If you know what you're doing, add `--unclean` to proceed.")
|
|
||||||
}
|
|
||||||
storcon_client
|
|
||||||
.dispatch::<(), ()>(Method::POST, format!("debug/v1/node/{node_id}/drop"), None)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Command::TenantSetTimeBasedEviction {
|
|
||||||
tenant_id,
|
|
||||||
period,
|
|
||||||
threshold,
|
|
||||||
} => {
|
|
||||||
vps_client
|
|
||||||
.tenant_config(&TenantConfigRequest {
|
|
||||||
tenant_id,
|
|
||||||
config: TenantConfig {
|
|
||||||
eviction_policy: Some(EvictionPolicy::LayerAccessThreshold(
|
|
||||||
EvictionPolicyLayerAccessThreshold {
|
|
||||||
period: period.into(),
|
|
||||||
threshold: threshold.into(),
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Command::Drain {
|
|
||||||
nodes,
|
|
||||||
concurrency,
|
|
||||||
max_shards,
|
|
||||||
dry_run,
|
|
||||||
} => {
|
|
||||||
// Load the list of nodes, split them up into the drained and filled sets,
|
|
||||||
// and validate that draining is possible.
|
|
||||||
let node_descs = storcon_client
|
|
||||||
.dispatch::<(), Vec<NodeDescribeResponse>>(
|
|
||||||
Method::GET,
|
|
||||||
"control/v1/node".to_string(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut node_to_drain_descs = Vec::new();
|
|
||||||
let mut node_to_fill_descs = Vec::new();
|
|
||||||
|
|
||||||
for desc in node_descs {
|
|
||||||
let to_drain = nodes.iter().any(|id| *id == desc.id);
|
|
||||||
if to_drain {
|
|
||||||
node_to_drain_descs.push(desc);
|
|
||||||
} else {
|
|
||||||
node_to_fill_descs.push(desc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if nodes.len() != node_to_drain_descs.len() {
|
|
||||||
anyhow::bail!("Drain requested for node which doesn't exist.")
|
|
||||||
}
|
|
||||||
|
|
||||||
node_to_fill_descs.retain(|desc| {
|
|
||||||
matches!(desc.availability, NodeAvailabilityWrapper::Active)
|
|
||||||
&& matches!(
|
|
||||||
desc.scheduling,
|
|
||||||
NodeSchedulingPolicy::Active | NodeSchedulingPolicy::Filling
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
if node_to_fill_descs.is_empty() {
|
|
||||||
anyhow::bail!("There are no nodes to drain to")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the node scheduling policy to draining for the nodes which
|
|
||||||
// we plan to drain.
|
|
||||||
for node_desc in node_to_drain_descs.iter() {
|
|
||||||
let req = NodeConfigureRequest {
|
|
||||||
node_id: node_desc.id,
|
|
||||||
availability: None,
|
|
||||||
scheduling: Some(NodeSchedulingPolicy::Draining),
|
|
||||||
};
|
|
||||||
|
|
||||||
storcon_client
|
|
||||||
.dispatch::<_, ()>(
|
|
||||||
Method::PUT,
|
|
||||||
format!("control/v1/node/{}/config", node_desc.id),
|
|
||||||
Some(req),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the drain: move each tenant shard scheduled on a node to
|
|
||||||
// be drained to a node which is being filled. A simple round robin
|
|
||||||
// strategy is used to pick the new node.
|
|
||||||
let tenants = storcon_client
|
|
||||||
.dispatch::<(), Vec<TenantDescribeResponse>>(
|
|
||||||
Method::GET,
|
|
||||||
"control/v1/tenant".to_string(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut selected_node_idx = 0;
|
|
||||||
|
|
||||||
struct DrainMove {
|
|
||||||
tenant_shard_id: TenantShardId,
|
|
||||||
from: NodeId,
|
|
||||||
to: NodeId,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut moves: Vec<DrainMove> = Vec::new();
|
|
||||||
|
|
||||||
let shards = tenants
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|tenant| tenant.shards.into_iter());
|
|
||||||
for shard in shards {
|
|
||||||
if let Some(max_shards) = max_shards {
|
|
||||||
if moves.len() >= max_shards {
|
|
||||||
println!(
|
|
||||||
"Stop planning shard moves since the requested maximum was reached"
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let should_migrate = {
|
|
||||||
if let Some(attached_to) = shard.node_attached {
|
|
||||||
node_to_drain_descs
|
|
||||||
.iter()
|
|
||||||
.map(|desc| desc.id)
|
|
||||||
.any(|id| id == attached_to)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !should_migrate {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
moves.push(DrainMove {
|
|
||||||
tenant_shard_id: shard.tenant_shard_id,
|
|
||||||
from: shard
|
|
||||||
.node_attached
|
|
||||||
.expect("We only migrate attached tenant shards"),
|
|
||||||
to: node_to_fill_descs[selected_node_idx].id,
|
|
||||||
});
|
|
||||||
selected_node_idx = (selected_node_idx + 1) % node_to_fill_descs.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
let total_moves = moves.len();
|
|
||||||
|
|
||||||
if dry_run == Some(true) {
|
|
||||||
println!("Dryrun requested. Planned {total_moves} moves:");
|
|
||||||
for mv in &moves {
|
|
||||||
println!("{}: {} -> {}", mv.tenant_shard_id, mv.from, mv.to)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_MIGRATE_CONCURRENCY: usize = 8;
|
|
||||||
let mut stream = futures::stream::iter(moves)
|
|
||||||
.map(|mv| {
|
|
||||||
let client = Client::new(cli.api.clone(), cli.jwt.clone());
|
|
||||||
async move {
|
|
||||||
client
|
|
||||||
.dispatch::<TenantShardMigrateRequest, TenantShardMigrateResponse>(
|
|
||||||
Method::PUT,
|
|
||||||
format!("control/v1/tenant/{}/migrate", mv.tenant_shard_id),
|
|
||||||
Some(TenantShardMigrateRequest {
|
|
||||||
tenant_shard_id: mv.tenant_shard_id,
|
|
||||||
node_id: mv.to,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| (mv.tenant_shard_id, mv.from, mv.to, e))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.buffered(concurrency.unwrap_or(DEFAULT_MIGRATE_CONCURRENCY));
|
|
||||||
|
|
||||||
let mut success = 0;
|
|
||||||
let mut failure = 0;
|
|
||||||
|
|
||||||
while let Some(res) = stream.next().await {
|
|
||||||
match res {
|
|
||||||
Ok(_) => {
|
|
||||||
success += 1;
|
|
||||||
}
|
|
||||||
Err((tenant_shard_id, from, to, error)) => {
|
|
||||||
failure += 1;
|
|
||||||
println!(
|
|
||||||
"Failed to migrate {} from node {} to node {}: {}",
|
|
||||||
tenant_shard_id, from, to, error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (success + failure) % 20 == 0 {
|
|
||||||
println!(
|
|
||||||
"Processed {}/{} shards: {} succeeded, {} failed",
|
|
||||||
success + failure,
|
|
||||||
total_moves,
|
|
||||||
success,
|
|
||||||
failure
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"Processed {}/{} shards: {} succeeded, {} failed",
|
|
||||||
success + failure,
|
|
||||||
total_moves,
|
|
||||||
success,
|
|
||||||
failure
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -99,13 +99,6 @@ name = "async-executor"
|
|||||||
[[bans.deny]]
|
[[bans.deny]]
|
||||||
name = "smol"
|
name = "smol"
|
||||||
|
|
||||||
[[bans.deny]]
|
|
||||||
# We want to use rustls instead of the platform's native tls implementation.
|
|
||||||
name = "native-tls"
|
|
||||||
|
|
||||||
[[bans.deny]]
|
|
||||||
name = "openssl"
|
|
||||||
|
|
||||||
# This section is considered when running `cargo deny check sources`.
|
# This section is considered when running `cargo deny check sources`.
|
||||||
# More documentation about the 'sources' section can be found here:
|
# More documentation about the 'sources' section can be found here:
|
||||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||||
|
|||||||
+2
-2
@@ -2,8 +2,8 @@
|
|||||||
# see https://diesel.rs/guides/configuring-diesel-cli
|
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||||
|
|
||||||
[print_schema]
|
[print_schema]
|
||||||
file = "storage_controller/src/schema.rs"
|
file = "control_plane/attachment_service/src/schema.rs"
|
||||||
custom_type_derives = ["diesel::query_builder::QueryId"]
|
custom_type_derives = ["diesel::query_builder::QueryId"]
|
||||||
|
|
||||||
[migrations_directory]
|
[migrations_directory]
|
||||||
dir = "storage_controller/migrations"
|
dir = "control_plane/attachment_service/migrations"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
ARG REPOSITORY=neondatabase
|
ARG REPOSITORY=369495373322.dkr.ecr.eu-central-1.amazonaws.com
|
||||||
ARG COMPUTE_IMAGE=compute-node-v14
|
ARG COMPUTE_IMAGE=compute-node-v14
|
||||||
ARG TAG=latest
|
ARG TAG=latest
|
||||||
|
|
||||||
@@ -8,11 +8,6 @@ USER root
|
|||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y curl \
|
apt-get install -y curl \
|
||||||
jq \
|
jq \
|
||||||
python3-pip \
|
|
||||||
netcat
|
netcat
|
||||||
#Faker is required for the pg_anon test
|
|
||||||
RUN pip3 install Faker
|
|
||||||
#This is required for the pg_hintplan test
|
|
||||||
RUN mkdir -p /ext-src/pg_hint_plan-src && chown postgres /ext-src/pg_hint_plan-src
|
|
||||||
|
|
||||||
USER postgres
|
USER postgres
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "shared_preload_libraries",
|
"name": "shared_preload_libraries",
|
||||||
"value": "neon,pg_cron,timescaledb,pg_stat_statements",
|
"value": "neon",
|
||||||
"vartype": "string"
|
"vartype": "string"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -127,16 +127,6 @@
|
|||||||
"name": "max_replication_flush_lag",
|
"name": "max_replication_flush_lag",
|
||||||
"value": "10GB",
|
"value": "10GB",
|
||||||
"vartype": "string"
|
"vartype": "string"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "cron.database",
|
|
||||||
"value": "postgres",
|
|
||||||
"vartype": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "session_preload_libraries",
|
|
||||||
"value": "anon",
|
|
||||||
"vartype": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
minio:
|
minio:
|
||||||
restart: always
|
restart: always
|
||||||
@@ -159,12 +161,12 @@ services:
|
|||||||
context: ./compute_wrapper/
|
context: ./compute_wrapper/
|
||||||
args:
|
args:
|
||||||
- REPOSITORY=${REPOSITORY:-neondatabase}
|
- REPOSITORY=${REPOSITORY:-neondatabase}
|
||||||
- COMPUTE_IMAGE=compute-node-v${PG_VERSION:-16}
|
- COMPUTE_IMAGE=compute-node-v${PG_VERSION:-14}
|
||||||
- TAG=${TAG:-latest}
|
- TAG=${TAG:-latest}
|
||||||
- http_proxy=$http_proxy
|
- http_proxy=$http_proxy
|
||||||
- https_proxy=$https_proxy
|
- https_proxy=$https_proxy
|
||||||
environment:
|
environment:
|
||||||
- PG_VERSION=${PG_VERSION:-16}
|
- PG_VERSION=${PG_VERSION:-14}
|
||||||
#- RUST_BACKTRACE=1
|
#- RUST_BACKTRACE=1
|
||||||
# Mount the test files directly, for faster editing cycle.
|
# Mount the test files directly, for faster editing cycle.
|
||||||
volumes:
|
volumes:
|
||||||
@@ -192,14 +194,3 @@ services:
|
|||||||
done"
|
done"
|
||||||
depends_on:
|
depends_on:
|
||||||
- compute
|
- compute
|
||||||
|
|
||||||
neon-test-extensions:
|
|
||||||
profiles: ["test-extensions"]
|
|
||||||
image: ${REPOSITORY:-neondatabase}/neon-test-extensions-v${PG_TEST_VERSION:-16}:${TAG:-latest}
|
|
||||||
entrypoint:
|
|
||||||
- "/bin/bash"
|
|
||||||
- "-c"
|
|
||||||
command:
|
|
||||||
- sleep 1800
|
|
||||||
depends_on:
|
|
||||||
- compute
|
|
||||||
|
|||||||
@@ -7,94 +7,54 @@
|
|||||||
# Implicitly accepts `REPOSITORY` and `TAG` env vars that are passed into the compose file
|
# Implicitly accepts `REPOSITORY` and `TAG` env vars that are passed into the compose file
|
||||||
# Their defaults point at DockerHub `neondatabase/neon:latest` image.`,
|
# Their defaults point at DockerHub `neondatabase/neon:latest` image.`,
|
||||||
# to verify custom image builds (e.g pre-published ones).
|
# to verify custom image builds (e.g pre-published ones).
|
||||||
#
|
|
||||||
# A test script for postgres extensions
|
# XXX: Current does not work on M1 macs due to x86_64 Docker images compiled only, and no seccomp support in M1 Docker emulation layer.
|
||||||
# Currently supports only v16
|
|
||||||
#
|
|
||||||
set -eux -o pipefail
|
set -eux -o pipefail
|
||||||
|
|
||||||
COMPOSE_FILE='docker-compose.yml'
|
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
cd $(dirname $0)
|
COMPOSE_FILE=$SCRIPT_DIR/docker-compose.yml
|
||||||
|
|
||||||
COMPUTE_CONTAINER_NAME=docker-compose-compute-1
|
COMPUTE_CONTAINER_NAME=docker-compose-compute-1
|
||||||
TEST_CONTAINER_NAME=docker-compose-neon-test-extensions-1
|
SQL="CREATE TABLE t(key int primary key, value text); insert into t values(1,1); select * from t;"
|
||||||
PSQL_OPTION="-h localhost -U cloud_admin -p 55433 -d postgres"
|
PSQL_OPTION="-h localhost -U cloud_admin -p 55433 -c '$SQL' postgres"
|
||||||
: ${http_proxy:=}
|
|
||||||
: ${https_proxy:=}
|
|
||||||
export http_proxy https_proxy
|
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
echo "show container information"
|
echo "show container information"
|
||||||
docker ps
|
docker ps
|
||||||
docker compose --profile test-extensions -f $COMPOSE_FILE logs
|
docker compose -f $COMPOSE_FILE logs
|
||||||
echo "stop containers..."
|
echo "stop containers..."
|
||||||
docker compose --profile test-extensions -f $COMPOSE_FILE down
|
docker compose -f $COMPOSE_FILE down
|
||||||
}
|
}
|
||||||
|
|
||||||
|
echo "clean up containers if exists"
|
||||||
|
cleanup
|
||||||
|
|
||||||
for pg_version in 14 15 16; do
|
for pg_version in 14 15 16; do
|
||||||
echo "clean up containers if exists"
|
echo "start containers (pg_version=$pg_version)."
|
||||||
cleanup
|
PG_VERSION=$pg_version docker compose -f $COMPOSE_FILE up --build -d
|
||||||
PG_TEST_VERSION=$(($pg_version < 16 ? 16 : $pg_version))
|
|
||||||
PG_VERSION=$pg_version PG_TEST_VERSION=$PG_TEST_VERSION docker compose --profile test-extensions -f $COMPOSE_FILE up --build -d
|
|
||||||
|
|
||||||
echo "wait until the compute is ready. timeout after 60s. "
|
echo "wait until the compute is ready. timeout after 60s. "
|
||||||
cnt=0
|
cnt=0
|
||||||
while sleep 3; do
|
while sleep 1; do
|
||||||
# check timeout
|
# check timeout
|
||||||
cnt=`expr $cnt + 3`
|
cnt=`expr $cnt + 1`
|
||||||
if [ $cnt -gt 60 ]; then
|
if [ $cnt -gt 60 ]; then
|
||||||
echo "timeout before the compute is ready."
|
echo "timeout before the compute is ready."
|
||||||
cleanup
|
cleanup
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if docker compose --profile test-extensions -f $COMPOSE_FILE logs "compute_is_ready" | grep -q "accepting connections"; then
|
|
||||||
|
# check if the compute is ready
|
||||||
|
set +o pipefail
|
||||||
|
result=`docker compose -f $COMPOSE_FILE logs "compute_is_ready" | grep "accepting connections" | wc -l`
|
||||||
|
set -o pipefail
|
||||||
|
if [ $result -eq 1 ]; then
|
||||||
echo "OK. The compute is ready to connect."
|
echo "OK. The compute is ready to connect."
|
||||||
echo "execute simple queries."
|
echo "execute simple queries."
|
||||||
docker exec $COMPUTE_CONTAINER_NAME /bin/bash -c "psql $PSQL_OPTION"
|
docker exec $COMPUTE_CONTAINER_NAME /bin/bash -c "psql $PSQL_OPTION"
|
||||||
|
cleanup
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ $pg_version -ge 16 ]
|
|
||||||
then
|
|
||||||
echo Enabling trust connection
|
|
||||||
docker exec $COMPUTE_CONTAINER_NAME bash -c "sed -i '\$d' /var/db/postgres/compute/pg_hba.conf && echo -e 'host\t all\t all\t all\t trust' >> /var/db/postgres/compute/pg_hba.conf && psql $PSQL_OPTION -c 'select pg_reload_conf()' "
|
|
||||||
echo Adding postgres role
|
|
||||||
docker exec $COMPUTE_CONTAINER_NAME psql $PSQL_OPTION -c "CREATE ROLE postgres SUPERUSER LOGIN"
|
|
||||||
# This is required for the pg_hint_plan test, to prevent flaky log message causing the test to fail
|
|
||||||
# It cannot be moved to Dockerfile now because the database directory is created after the start of the container
|
|
||||||
echo Adding dummy config
|
|
||||||
docker exec $COMPUTE_CONTAINER_NAME touch /var/db/postgres/compute/compute_ctl_temp_override.conf
|
|
||||||
# This block is required for the pg_anon extension test.
|
|
||||||
# The test assumes that it is running on the same host with the postgres engine.
|
|
||||||
# In our case it's not true, that's why we are copying files to the compute node
|
|
||||||
TMPDIR=$(mktemp -d)
|
|
||||||
docker cp $TEST_CONTAINER_NAME:/ext-src/pg_anon-src/data $TMPDIR/data
|
|
||||||
echo -e '1\t too \t many \t tabs' > $TMPDIR/data/bad.csv
|
|
||||||
docker cp $TMPDIR/data $COMPUTE_CONTAINER_NAME:/tmp/tmp_anon_alternate_data
|
|
||||||
rm -rf $TMPDIR
|
|
||||||
TMPDIR=$(mktemp -d)
|
|
||||||
# The following block does the same for the pg_hintplan test
|
|
||||||
docker cp $TEST_CONTAINER_NAME:/ext-src/pg_hint_plan-src/data $TMPDIR/data
|
|
||||||
docker cp $TMPDIR/data $COMPUTE_CONTAINER_NAME:/ext-src/pg_hint_plan-src/
|
|
||||||
rm -rf $TMPDIR
|
|
||||||
# We are running tests now
|
|
||||||
if docker exec -e SKIP=rum-src,timescaledb-src,rdkit-src,postgis-src,pgx_ulid-src,pgtap-src,pg_tiktoken-src,pg_jsonschema-src,pg_graphql-src,kq_imcx-src,wal2json_2_5-src \
|
|
||||||
$TEST_CONTAINER_NAME /run-tests.sh | tee testout.txt
|
|
||||||
then
|
|
||||||
cleanup
|
|
||||||
else
|
|
||||||
FAILED=$(tail -1 testout.txt)
|
|
||||||
for d in $FAILED
|
|
||||||
do
|
|
||||||
mkdir $d
|
|
||||||
docker cp $TEST_CONTAINER_NAME:/ext-src/$d/regression.diffs $d || true
|
|
||||||
docker cp $TEST_CONTAINER_NAME:/ext-src/$d/regression.out $d || true
|
|
||||||
cat $d/regression.out $d/regression.diffs || true
|
|
||||||
done
|
|
||||||
rm -rf $FAILED
|
|
||||||
cleanup
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
cleanup
|
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -x
|
|
||||||
|
|
||||||
cd /ext-src
|
|
||||||
FAILED=
|
|
||||||
LIST=$((echo ${SKIP} | sed 's/,/\n/g'; ls -d *-src) | sort | uniq -u)
|
|
||||||
for d in ${LIST}
|
|
||||||
do
|
|
||||||
[ -d ${d} ] || continue
|
|
||||||
psql -c "select 1" >/dev/null || break
|
|
||||||
make -C ${d} installcheck || FAILED="${d} ${FAILED}"
|
|
||||||
done
|
|
||||||
[ -z "${FAILED}" ] && exit 0
|
|
||||||
echo ${FAILED}
|
|
||||||
exit 1
|
|
||||||
+173
-169
@@ -11,28 +11,15 @@ page server. We currently use the same binary for both, with --wal-redo runtime
|
|||||||
the WAL redo mode. Some PostgreSQL changes are needed in the compute node, while others are just for
|
the WAL redo mode. Some PostgreSQL changes are needed in the compute node, while others are just for
|
||||||
the WAL redo process.
|
the WAL redo process.
|
||||||
|
|
||||||
In addition to core PostgreSQL changes, there is a Neon extension in the pgxn/neon directory that
|
In addition to core PostgreSQL changes, there is a Neon extension in contrib/neon, to hook into the
|
||||||
hooks into the smgr interface, and rmgr extension in pgxn/neon_rmgr. The extensions are loaded into
|
smgr interface. Once all the core changes have been submitted to upstream or eliminated some other
|
||||||
the Postgres processes with shared_preload_libraries. Most of the Neon-specific code is in the
|
way, the extension could live outside the postgres repository and build against vanilla PostgreSQL.
|
||||||
extensions, and for any new features, that is preferred over modifying core PostgreSQL code.
|
|
||||||
|
|
||||||
Below is a list of all the PostgreSQL source code changes, categorized into changes needed for
|
Below is a list of all the PostgreSQL source code changes, categorized into changes needed for
|
||||||
compute, and changes needed for the WAL redo process:
|
compute, and changes needed for the WAL redo process:
|
||||||
|
|
||||||
# Changes for Compute node
|
# Changes for Compute node
|
||||||
|
|
||||||
## Prefetching
|
|
||||||
|
|
||||||
There are changes in many places to perform prefetching, for example for sequential scans. Neon
|
|
||||||
doesn't benefit from OS readahead, and the latency to pageservers is quite high compared to local
|
|
||||||
disk, so prefetching is critical for performance, also for sequential scans.
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
Upcoming "streaming read" work in v17 might simplify this. And async I/O work in v18 will hopefully
|
|
||||||
do more.
|
|
||||||
|
|
||||||
|
|
||||||
## Add t_cid to heap WAL records
|
## Add t_cid to heap WAL records
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -50,11 +37,54 @@ The problem is that the XLOG_HEAP_INSERT record does not include the command id
|
|||||||
|
|
||||||
Bite the bullet and submit the patch to PostgreSQL, to add the t_cid to the WAL records. It makes the WAL records larger, which could make this unpopular in the PostgreSQL community. However, it might simplify some logical decoding code; Andres Freund briefly mentioned in PGCon 2022 discussion on Heikki's Neon presentation that logical decoding currently needs to jump through some hoops to reconstruct the same information.
|
Bite the bullet and submit the patch to PostgreSQL, to add the t_cid to the WAL records. It makes the WAL records larger, which could make this unpopular in the PostgreSQL community. However, it might simplify some logical decoding code; Andres Freund briefly mentioned in PGCon 2022 discussion on Heikki's Neon presentation that logical decoding currently needs to jump through some hoops to reconstruct the same information.
|
||||||
|
|
||||||
Update from Heikki (2024-04-17): I tried to write an upstream patch for that, to use the t_cid field for logical decoding, but it was not as straightforward as it first sounded.
|
|
||||||
|
|
||||||
### Alternatives
|
### Alternatives
|
||||||
Perhaps we could write an extra WAL record with the t_cid information, when a page is evicted that contains rows that were touched a transaction that's still running. However, that seems very complicated.
|
Perhaps we could write an extra WAL record with the t_cid information, when a page is evicted that contains rows that were touched a transaction that's still running. However, that seems very complicated.
|
||||||
|
|
||||||
|
## ginfast.c
|
||||||
|
|
||||||
|
```
|
||||||
|
diff --git a/src/backend/access/gin/ginfast.c b/src/backend/access/gin/ginfast.c
|
||||||
|
index e0d9940946..2d964c02e9 100644
|
||||||
|
--- a/src/backend/access/gin/ginfast.c
|
||||||
|
+++ b/src/backend/access/gin/ginfast.c
|
||||||
|
@@ -285,6 +285,17 @@ ginHeapTupleFastInsert(GinState *ginstate, GinTupleCollector *collector)
|
||||||
|
memset(&sublist, 0, sizeof(GinMetaPageData));
|
||||||
|
makeSublist(index, collector->tuples, collector->ntuples, &sublist);
|
||||||
|
|
||||||
|
+ if (metadata->head != InvalidBlockNumber)
|
||||||
|
+ {
|
||||||
|
+ /*
|
||||||
|
+ * ZENITH: Get buffer before XLogBeginInsert() to avoid recursive call
|
||||||
|
+ * of XLogBeginInsert(). Reading a new buffer might evict a dirty page from
|
||||||
|
+ * the buffer cache, and if that page happens to be an FSM or VM page, zenith_write()
|
||||||
|
+ * will try to WAL-log an image of the page.
|
||||||
|
+ */
|
||||||
|
+ buffer = ReadBuffer(index, metadata->tail);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
if (needWal)
|
||||||
|
XLogBeginInsert();
|
||||||
|
|
||||||
|
@@ -316,7 +327,6 @@ ginHeapTupleFastInsert(GinState *ginstate, GinTupleCollector *collector)
|
||||||
|
data.prevTail = metadata->tail;
|
||||||
|
data.newRightlink = sublist.head;
|
||||||
|
|
||||||
|
- buffer = ReadBuffer(index, metadata->tail);
|
||||||
|
LockBuffer(buffer, GIN_EXCLUSIVE);
|
||||||
|
page = BufferGetPage(buffer);
|
||||||
|
```
|
||||||
|
|
||||||
|
The problem is explained in the comment above
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
Can we stop WAL-logging FSM or VM pages? Or delay the WAL logging until we're out of the critical
|
||||||
|
section or something.
|
||||||
|
|
||||||
|
Maybe some bigger rewrite of FSM and VM would help to avoid WAL-logging FSM and VM page images?
|
||||||
|
|
||||||
|
|
||||||
## Mark index builds that use buffer manager without logging explicitly
|
## Mark index builds that use buffer manager without logging explicitly
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -65,8 +95,6 @@ Perhaps we could write an extra WAL record with the t_cid information, when a pa
|
|||||||
also some changes in src/backend/storage/smgr/smgr.c
|
also some changes in src/backend/storage/smgr/smgr.c
|
||||||
```
|
```
|
||||||
|
|
||||||
pgvector 0.6.0 also needs a similar change, which would be very nice to get rid of too.
|
|
||||||
|
|
||||||
When a GIN index is built, for example, it is built by inserting the entries into the index more or
|
When a GIN index is built, for example, it is built by inserting the entries into the index more or
|
||||||
less normally, but without WAL-logging anything. After the index has been built, we iterate through
|
less normally, but without WAL-logging anything. After the index has been built, we iterate through
|
||||||
all pages and write them to the WAL. That doesn't work for Neon, because if a page is not WAL-logged
|
all pages and write them to the WAL. That doesn't work for Neon, because if a page is not WAL-logged
|
||||||
@@ -81,10 +109,6 @@ an operation: `smgr_start_unlogged_build`, `smgr_finish_unlogged_build_phase_1`
|
|||||||
I think it would make sense to be more explicit about that in PostgreSQL too. So extract these
|
I think it would make sense to be more explicit about that in PostgreSQL too. So extract these
|
||||||
changes to a patch and post to pgsql-hackers.
|
changes to a patch and post to pgsql-hackers.
|
||||||
|
|
||||||
Perhaps we could deduce that an unlogged index build has started when we see a page being evicted
|
|
||||||
with zero LSN. How to be sure it's an unlogged index build rather than a bug? Currently we have a
|
|
||||||
check for that and PANIC if we see page with zero LSN being evicted. And how do we detect when the
|
|
||||||
index build has finished? See https://github.com/neondatabase/neon/pull/7440 for an attempt at that.
|
|
||||||
|
|
||||||
## Track last-written page LSN
|
## Track last-written page LSN
|
||||||
|
|
||||||
@@ -116,6 +140,57 @@ The old method is still available, though.
|
|||||||
Wait until v15?
|
Wait until v15?
|
||||||
|
|
||||||
|
|
||||||
|
## Cache relation sizes
|
||||||
|
|
||||||
|
The Neon extension contains a little cache for smgrnblocks() and smgrexists() calls, to avoid going
|
||||||
|
to the page server every time. It might be useful to cache those in PostgreSQL, maybe in the
|
||||||
|
relcache? (I think we do cache nblocks in relcache already, check why that's not good enough for
|
||||||
|
Neon)
|
||||||
|
|
||||||
|
|
||||||
|
## Use buffer manager when extending VM or FSM
|
||||||
|
|
||||||
|
```
|
||||||
|
src/backend/storage/freespace/freespace.c | 14 +-
|
||||||
|
src/backend/access/heap/visibilitymap.c | 15 +-
|
||||||
|
|
||||||
|
diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c
|
||||||
|
index e198df65d8..addfe93eac 100644
|
||||||
|
--- a/src/backend/access/heap/visibilitymap.c
|
||||||
|
+++ b/src/backend/access/heap/visibilitymap.c
|
||||||
|
@@ -652,10 +652,19 @@ vm_extend(Relation rel, BlockNumber vm_nblocks)
|
||||||
|
/* Now extend the file */
|
||||||
|
while (vm_nblocks_now < vm_nblocks)
|
||||||
|
{
|
||||||
|
- PageSetChecksumInplace((Page) pg.data, vm_nblocks_now);
|
||||||
|
+ /*
|
||||||
|
+ * ZENITH: Initialize VM pages through buffer cache to prevent loading
|
||||||
|
+ * them from pageserver.
|
||||||
|
+ */
|
||||||
|
+ Buffer buffer = ReadBufferExtended(rel, VISIBILITYMAP_FORKNUM, P_NEW,
|
||||||
|
+ RBM_ZERO_AND_LOCK, NULL);
|
||||||
|
+ Page page = BufferGetPage(buffer);
|
||||||
|
+
|
||||||
|
+ PageInit((Page) page, BLCKSZ, 0);
|
||||||
|
+ PageSetChecksumInplace(page, vm_nblocks_now);
|
||||||
|
+ MarkBufferDirty(buffer);
|
||||||
|
+ UnlockReleaseBuffer(buffer);
|
||||||
|
|
||||||
|
- smgrextend(rel->rd_smgr, VISIBILITYMAP_FORKNUM, vm_nblocks_now,
|
||||||
|
- pg.data, false);
|
||||||
|
vm_nblocks_now++;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem we're trying to solve
|
||||||
|
|
||||||
|
???
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
Maybe this would be a reasonable change in PostgreSQL too?
|
||||||
|
|
||||||
|
|
||||||
## Allow startup without reading checkpoint record
|
## Allow startup without reading checkpoint record
|
||||||
|
|
||||||
In Neon, the compute node is stateless. So when we are launching compute node, we need to provide
|
In Neon, the compute node is stateless. So when we are launching compute node, we need to provide
|
||||||
@@ -156,7 +231,7 @@ index 0415df9ccb..9f9db3c8bc 100644
|
|||||||
* crash we can lose (skip over) as many values as we pre-logged.
|
* crash we can lose (skip over) as many values as we pre-logged.
|
||||||
*/
|
*/
|
||||||
-#define SEQ_LOG_VALS 32
|
-#define SEQ_LOG_VALS 32
|
||||||
+/* Neon XXX: to ensure sequence order of sequence in Zenith we need to WAL log each sequence update. */
|
+/* Zenith XXX: to ensure sequence order of sequence in Zenith we need to WAL log each sequence update. */
|
||||||
+/* #define SEQ_LOG_VALS 32 */
|
+/* #define SEQ_LOG_VALS 32 */
|
||||||
+#define SEQ_LOG_VALS 0
|
+#define SEQ_LOG_VALS 0
|
||||||
```
|
```
|
||||||
@@ -175,6 +250,66 @@ would be weird if the sequence moved backwards though, think of PITR.
|
|||||||
Or add a GUC for the amount to prefix to PostgreSQL, and force it to 1 in Neon.
|
Or add a GUC for the amount to prefix to PostgreSQL, and force it to 1 in Neon.
|
||||||
|
|
||||||
|
|
||||||
|
## Walproposer
|
||||||
|
|
||||||
|
```
|
||||||
|
src/Makefile | 1 +
|
||||||
|
src/backend/replication/libpqwalproposer/Makefile | 37 +
|
||||||
|
src/backend/replication/libpqwalproposer/libpqwalproposer.c | 416 ++++++++++++
|
||||||
|
src/backend/postmaster/bgworker.c | 4 +
|
||||||
|
src/backend/postmaster/postmaster.c | 6 +
|
||||||
|
src/backend/replication/Makefile | 4 +-
|
||||||
|
src/backend/replication/walproposer.c | 2350 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
src/backend/replication/walproposer_utils.c | 402 +++++++++++
|
||||||
|
src/backend/replication/walreceiver.c | 7 +
|
||||||
|
src/backend/replication/walsender.c | 320 ++++++---
|
||||||
|
src/backend/storage/ipc/ipci.c | 6 +
|
||||||
|
src/include/replication/walproposer.h | 565 ++++++++++++++++
|
||||||
|
```
|
||||||
|
|
||||||
|
WAL proposer is communicating with safekeeper and ensures WAL durability by quorum writes. It is
|
||||||
|
currently implemented as patch to standard WAL sender.
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
Refactor into an extension. Submit hooks or APIs into upstream if necessary.
|
||||||
|
|
||||||
|
@MMeent did some work on this already: https://github.com/neondatabase/postgres/pull/96
|
||||||
|
|
||||||
|
## Ignore unexpected data beyond EOF in bufmgr.c
|
||||||
|
|
||||||
|
```
|
||||||
|
@@ -922,11 +928,14 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
|
||||||
|
*/
|
||||||
|
bufBlock = isLocalBuf ? LocalBufHdrGetBlock(bufHdr) : BufHdrGetBlock(bufHdr);
|
||||||
|
if (!PageIsNew((Page) bufBlock))
|
||||||
|
- ereport(ERROR,
|
||||||
|
+ {
|
||||||
|
+ // XXX-ZENITH
|
||||||
|
+ MemSet((char *) bufBlock, 0, BLCKSZ);
|
||||||
|
+ ereport(DEBUG1,
|
||||||
|
(errmsg("unexpected data beyond EOF in block %u of relation %s",
|
||||||
|
blockNum, relpath(smgr->smgr_rnode, forkNum)),
|
||||||
|
errhint("This has been seen to occur with buggy kernels; consider updating your system.")));
|
||||||
|
-
|
||||||
|
+ }
|
||||||
|
/*
|
||||||
|
* We *must* do smgrextend before succeeding, else the page will not
|
||||||
|
* be reserved by the kernel, and the next P_NEW call will decide to
|
||||||
|
```
|
||||||
|
|
||||||
|
PostgreSQL is a bit sloppy with extending relations. Usually, the relation is extended with zeros
|
||||||
|
first, then the page is filled, and finally the new page WAL-logged. But if multiple backends extend
|
||||||
|
a relation at the same time, the pages can be WAL-logged in different order.
|
||||||
|
|
||||||
|
I'm not sure what scenario exactly required this change in Neon, though.
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
Submit patches to pgsql-hackers, to tighten up the WAL-logging around relation extension. It's a bit
|
||||||
|
confusing even in PostgreSQL. Maybe WAL log the intention to extend first, then extend the relation,
|
||||||
|
and finally WAL-log that the extension succeeded.
|
||||||
|
|
||||||
## Make smgr interface available to extensions
|
## Make smgr interface available to extensions
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -186,8 +321,6 @@ Or add a GUC for the amount to prefix to PostgreSQL, and force it to 1 in Neon.
|
|||||||
|
|
||||||
Submit to upstream. This could be useful for the Disk Encryption patches too, or for compression.
|
Submit to upstream. This could be useful for the Disk Encryption patches too, or for compression.
|
||||||
|
|
||||||
We have submitted this to upstream, but it's moving at glacial a speed.
|
|
||||||
https://commitfest.postgresql.org/47/4428/
|
|
||||||
|
|
||||||
## Added relpersistence argument to smgropen()
|
## Added relpersistence argument to smgropen()
|
||||||
|
|
||||||
@@ -311,148 +444,6 @@ Ignore it. This is only needed for disaster recovery, so once we've eliminated a
|
|||||||
patches, we can just keep it around as a patch or as separate branch in a repo.
|
patches, we can just keep it around as a patch or as separate branch in a repo.
|
||||||
|
|
||||||
|
|
||||||
## pg_waldump flags to ignore errors
|
|
||||||
|
|
||||||
After creating a new project or branch in Neon, the first timeline can begin in the middle of a WAL segment. pg_waldump chokes on that, so we added some flags to make it possible to ignore errors.
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
Like previous one, ignore it.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Backpressure if pageserver doesn't ingest WAL fast enough
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -3200,6 +3202,7 @@ ProcessInterrupts(void)
|
|
||||||
return;
|
|
||||||
InterruptPending = false;
|
|
||||||
|
|
||||||
+retry:
|
|
||||||
if (ProcDiePending)
|
|
||||||
{
|
|
||||||
ProcDiePending = false;
|
|
||||||
@@ -3447,6 +3450,13 @@ ProcessInterrupts(void)
|
|
||||||
|
|
||||||
if (ParallelApplyMessagePending)
|
|
||||||
HandleParallelApplyMessages();
|
|
||||||
+
|
|
||||||
+ /* Call registered callback if any */
|
|
||||||
+ if (ProcessInterruptsCallback)
|
|
||||||
+ {
|
|
||||||
+ if (ProcessInterruptsCallback())
|
|
||||||
+ goto retry;
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
Submit a patch to upstream, for a hook in ProcessInterrupts. Could be useful for other extensions
|
|
||||||
too.
|
|
||||||
|
|
||||||
|
|
||||||
## SLRU on-demand download
|
|
||||||
|
|
||||||
```
|
|
||||||
src/backend/access/transam/slru.c | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
|
|
||||||
1 file changed, 92 insertions(+), 13 deletions(-)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problem we're trying to solve
|
|
||||||
|
|
||||||
Previously, SLRU files were included in the basebackup, but the total size of them can be large,
|
|
||||||
several GB, and downloading them all made the startup time too long.
|
|
||||||
|
|
||||||
### Alternatives
|
|
||||||
|
|
||||||
FUSE hook or LD_PRELOAD trick to intercept the reads on SLRU files
|
|
||||||
|
|
||||||
|
|
||||||
## WAL-log an all-zeros page as one large hole
|
|
||||||
|
|
||||||
- In XLogRecordAssemble()
|
|
||||||
|
|
||||||
### Problem we're trying to solve
|
|
||||||
|
|
||||||
This change was made in v16. Starting with v16, when PostgreSQL extends a relation, it first extends
|
|
||||||
it with zeros, and it can extend the relation more than one block at a time. The all-zeros page is WAL-ogged, but it's very wasteful to include 8 kB of zeros in the WAL for that. This hack was made so that we WAL logged a compact record with a whole-page "hole". However, PostgreSQL has assertions that prevent that such WAL records from being replayed, so this breaks compatibility such that unmodified PostreSQL cannot process Neon-generated WAL.
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
Find another compact representation for a full-page image of an all-zeros page. A compressed image perhaps.
|
|
||||||
|
|
||||||
|
|
||||||
## Shut down walproposer after checkpointer
|
|
||||||
|
|
||||||
```
|
|
||||||
+ /* Neon: Also allow walproposer background worker to be treated like a WAL sender, so that it's shut down last */
|
|
||||||
+ if ((bp->bkend_type == BACKEND_TYPE_NORMAL || bp->bkend_type == BACKEND_TYPE_BGWORKER) &&
|
|
||||||
```
|
|
||||||
|
|
||||||
This changes was needed so that postmaster shuts down the walproposer process only after the shutdown checkpoint record is written. Otherwise, the shutdown record will never make it to the safekeepers.
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
Do a bigger refactoring of the postmaster state machine, such that a background worker can specify
|
|
||||||
the shutdown ordering by itself. The postmaster state machine has grown pretty complicated, and
|
|
||||||
would benefit from a refactoring for the sake of readability anyway.
|
|
||||||
|
|
||||||
|
|
||||||
## EXPLAIN changes for prefetch and LFC
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
Konstantin submitted a patch to -hackers already: https://commitfest.postgresql.org/47/4643/. Get that into a committable state.
|
|
||||||
|
|
||||||
|
|
||||||
## On-demand download of extensions
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
FUSE or LD_PRELOAD trickery to intercept reads?
|
|
||||||
|
|
||||||
|
|
||||||
## Publication superuser checks
|
|
||||||
|
|
||||||
We have hacked CreatePublication so that also neon_superuser can create them.
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
Create an upstream patch with more fine-grained privileges for publications CREATE/DROP that can be GRANTed to users.
|
|
||||||
|
|
||||||
|
|
||||||
## WAL log replication slots
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
Utilize the upcoming v17 "slot sync worker", or a similar neon-specific background worker process, to periodically WAL-log the slots, or to export them somewhere else.
|
|
||||||
|
|
||||||
|
|
||||||
## WAL-log replication snapshots
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
WAL-log them periodically, from a backgound worker.
|
|
||||||
|
|
||||||
|
|
||||||
## WAL-log relmapper files
|
|
||||||
|
|
||||||
Similarly to replications snapshot files, the CID mapping files generated during VACUUM FULL of a catalog table are WAL-logged
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
WAL-log them periodically, from a backgound worker.
|
|
||||||
|
|
||||||
|
|
||||||
## XLogWaitForReplayOf()
|
|
||||||
|
|
||||||
??
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Not currently committed but proposed
|
# Not currently committed but proposed
|
||||||
|
|
||||||
## Disable ring buffer buffer manager strategies
|
## Disable ring buffer buffer manager strategies
|
||||||
@@ -481,10 +472,23 @@ hint bits are set. Wal logging hint bits updates requires FPI which significantl
|
|||||||
|
|
||||||
Add special WAL record for setting page hints.
|
Add special WAL record for setting page hints.
|
||||||
|
|
||||||
|
## Prefetching
|
||||||
|
|
||||||
|
### Why?
|
||||||
|
|
||||||
|
As far as pages in Neon are loaded on demand, to reduce node startup time
|
||||||
|
and also speedup some massive queries we need some mechanism for bulk loading to
|
||||||
|
reduce page request round-trip overhead.
|
||||||
|
|
||||||
|
Currently Postgres is supporting prefetching only for bitmap scan.
|
||||||
|
In Neon we should also use prefetch for sequential and index scans, because the OS is not doing it for us.
|
||||||
|
For sequential scan we could prefetch some number of following pages. For index scan we could prefetch pages
|
||||||
|
of heap relation addressed by TIDs.
|
||||||
|
|
||||||
## Prewarming
|
## Prewarming
|
||||||
|
|
||||||
### Why?
|
### Why?
|
||||||
|
|
||||||
Short downtime (or, in other words, fast compute node restart time) is one of the key feature of Neon.
|
Short downtime (or, in other words, fast compute node restart time) is one of the key feature of Zenith.
|
||||||
But overhead of request-response round-trip for loading pages on demand can make started node warm-up quite slow.
|
But overhead of request-response round-trip for loading pages on demand can make started node warm-up quite slow.
|
||||||
We can capture state of compute node buffer cache and send bulk request for this pages at startup.
|
We can capture state of compute node buffer cache and send bulk request for this pages at startup.
|
||||||
|
|||||||
+15
-17
@@ -4,18 +4,18 @@
|
|||||||
|
|
||||||
Currently we build two main images:
|
Currently we build two main images:
|
||||||
|
|
||||||
- [neondatabase/neon](https://hub.docker.com/repository/docker/neondatabase/neon) — image with pre-built `pageserver`, `safekeeper` and `proxy` binaries and all the required runtime dependencies. Built from [/Dockerfile](/Dockerfile).
|
- [neondatabase/neon](https://hub.docker.com/repository/docker/zenithdb/zenith) — image with pre-built `pageserver`, `safekeeper` and `proxy` binaries and all the required runtime dependencies. Built from [/Dockerfile](/Dockerfile).
|
||||||
- [neondatabase/compute-node-v16](https://hub.docker.com/repository/docker/neondatabase/compute-node-v16) — compute node image with pre-built Postgres binaries from [neondatabase/postgres](https://github.com/neondatabase/postgres). Similar images exist for v15 and v14.
|
- [neondatabase/compute-node](https://hub.docker.com/repository/docker/zenithdb/compute-node) — compute node image with pre-built Postgres binaries from [neondatabase/postgres](https://github.com/neondatabase/postgres).
|
||||||
|
|
||||||
And additional intermediate image:
|
And additional intermediate image:
|
||||||
|
|
||||||
- [neondatabase/compute-tools](https://hub.docker.com/repository/docker/neondatabase/compute-tools) — compute node configuration management tools.
|
- [neondatabase/compute-tools](https://hub.docker.com/repository/docker/neondatabase/compute-tools) — compute node configuration management tools.
|
||||||
|
|
||||||
## Build pipeline
|
## Building pipeline
|
||||||
|
|
||||||
We build all images after a successful `release` tests run and push automatically to Docker Hub with two parallel CI jobs
|
We build all images after a successful `release` tests run and push automatically to Docker Hub with two parallel CI jobs
|
||||||
|
|
||||||
1. `neondatabase/compute-tools` and `neondatabase/compute-node-v16` (and -v15 and -v14)
|
1. `neondatabase/compute-tools` and `neondatabase/compute-node`
|
||||||
|
|
||||||
2. `neondatabase/neon`
|
2. `neondatabase/neon`
|
||||||
|
|
||||||
@@ -34,12 +34,12 @@ You can see a [docker compose](https://docs.docker.com/compose/) example to crea
|
|||||||
1. create containers
|
1. create containers
|
||||||
|
|
||||||
You can specify version of neon cluster using following environment values.
|
You can specify version of neon cluster using following environment values.
|
||||||
- PG_VERSION: postgres version for compute (default is 16 as of this writing)
|
- PG_VERSION: postgres version for compute (default is 14)
|
||||||
- TAG: the tag version of [docker image](https://registry.hub.docker.com/r/neondatabase/neon/tags), which is tagged in [CI test](/.github/workflows/build_and_test.yml). Default is 'latest'
|
- TAG: the tag version of [docker image](https://registry.hub.docker.com/r/neondatabase/neon/tags) (default is latest), which is tagged in [CI test](/.github/workflows/build_and_test.yml)
|
||||||
```
|
```
|
||||||
$ cd docker-compose/
|
$ cd docker-compose/
|
||||||
$ docker-compose down # remove the containers if exists
|
$ docker-compose down # remove the containers if exists
|
||||||
$ PG_VERSION=16 TAG=latest docker-compose up --build -d # You can specify the postgres and image version
|
$ PG_VERSION=15 TAG=2937 docker-compose up --build -d # You can specify the postgres and image version
|
||||||
Creating network "dockercompose_default" with the default driver
|
Creating network "dockercompose_default" with the default driver
|
||||||
Creating docker-compose_storage_broker_1 ... done
|
Creating docker-compose_storage_broker_1 ... done
|
||||||
(...omit...)
|
(...omit...)
|
||||||
@@ -47,31 +47,29 @@ Creating docker-compose_storage_broker_1 ... done
|
|||||||
|
|
||||||
2. connect compute node
|
2. connect compute node
|
||||||
```
|
```
|
||||||
$ psql postgresql://cloud_admin:cloud_admin@localhost:55433/postgres
|
$ echo "localhost:55433:postgres:cloud_admin:cloud_admin" >> ~/.pgpass
|
||||||
psql (16.3)
|
$ chmod 600 ~/.pgpass
|
||||||
Type "help" for help.
|
$ psql -h localhost -p 55433 -U cloud_admin
|
||||||
|
|
||||||
postgres=# CREATE TABLE t(key int primary key, value text);
|
postgres=# CREATE TABLE t(key int primary key, value text);
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
postgres=# insert into t values(1, 1);
|
postgres=# insert into t values(1,1);
|
||||||
INSERT 0 1
|
INSERT 0 1
|
||||||
postgres=# select * from t;
|
postgres=# select * from t;
|
||||||
key | value
|
key | value
|
||||||
-----+-------
|
-----+-------
|
||||||
1 | 1
|
1 | 1
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. If you want to see the log, you can use `docker-compose logs` command.
|
3. If you want to see the log, you can use `docker-compose logs` command.
|
||||||
```
|
```
|
||||||
# check the container name you want to see
|
# check the container name you want to see
|
||||||
$ docker ps
|
$ docker ps
|
||||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||||
3582f6d76227 docker-compose_compute "/shell/compute.sh" 2 minutes ago Up 2 minutes 0.0.0.0:3080->3080/tcp, :::3080->3080/tcp, 0.0.0.0:55433->55433/tcp, :::55433->55433/tcp docker-compose_compute_1
|
d6968a5ae912 dockercompose_compute "/shell/compute.sh" 5 minutes ago Up 5 minutes 0.0.0.0:3080->3080/tcp, 0.0.0.0:55433->55433/tcp dockercompose_compute_1
|
||||||
(...omit...)
|
(...omit...)
|
||||||
|
|
||||||
$ docker logs -f docker-compose_compute_1
|
$ docker logs -f dockercompose_compute_1
|
||||||
2022-10-21 06:15:48.757 GMT [56] LOG: connection authorized: user=cloud_admin database=postgres application_name=psql
|
2022-10-21 06:15:48.757 GMT [56] LOG: connection authorized: user=cloud_admin database=postgres application_name=psql
|
||||||
2022-10-21 06:17:00.307 GMT [56] LOG: [NEON_SMGR] libpagestore: connected to 'host=pageserver port=6400'
|
2022-10-21 06:17:00.307 GMT [56] LOG: [NEON_SMGR] libpagestore: connected to 'host=pageserver port=6400'
|
||||||
(...omit...)
|
(...omit...)
|
||||||
|
|||||||
@@ -101,12 +101,11 @@ or
|
|||||||
```toml
|
```toml
|
||||||
[remote_storage]
|
[remote_storage]
|
||||||
container_name = 'some-container-name'
|
container_name = 'some-container-name'
|
||||||
storage_account = 'somestorageaccnt'
|
|
||||||
container_region = 'us-east'
|
container_region = 'us-east'
|
||||||
prefix_in_container = '/test-prefix/'
|
prefix_in_container = '/test-prefix/'
|
||||||
```
|
```
|
||||||
|
|
||||||
The `AZURE_STORAGE_ACCESS_KEY` env variable can be used to specify the azure credentials if needed.
|
`AZURE_STORAGE_ACCOUNT` and `AZURE_STORAGE_ACCESS_KEY` env variables can be used to specify the azure credentials if needed.
|
||||||
|
|
||||||
## Repository background tasks
|
## Repository background tasks
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user