diff --git a/.circleci/ansible/.gitignore b/.circleci/ansible/.gitignore new file mode 100644 index 0000000000..14a1c155ae --- /dev/null +++ b/.circleci/ansible/.gitignore @@ -0,0 +1,2 @@ +zenith_install.tar.gz +.zenith_current_version diff --git a/.circleci/ansible/deploy.yaml b/.circleci/ansible/deploy.yaml index 2dd109f99a..2112102aa7 100644 --- a/.circleci/ansible/deploy.yaml +++ b/.circleci/ansible/deploy.yaml @@ -1,14 +1,11 @@ - name: Upload Zenith binaries - hosts: pageservers:safekeepers + hosts: storage gather_facts: False remote_user: admin - vars: - force_deploy: false tasks: - name: get latest version of Zenith binaries - ignore_errors: true register: current_version_file set_fact: current_version: "{{ lookup('file', '.zenith_current_version') | trim }}" @@ -16,48 +13,13 @@ - pageserver - safekeeper - - name: set zero value for current_version - when: current_version_file is failed - set_fact: - current_version: "0" - tags: - - pageserver - - safekeeper - - - name: get deployed version from content of remote file - ignore_errors: true - ansible.builtin.slurp: - src: /usr/local/.zenith_current_version - register: remote_version_file - tags: - - pageserver - - safekeeper - - - name: decode remote file content - when: remote_version_file is succeeded - set_fact: - remote_version: "{{ remote_version_file['content'] | b64decode | trim }}" - tags: - - pageserver - - safekeeper - - - name: set zero value for remote_version - when: remote_version_file is failed - set_fact: - remote_version: "0" - tags: - - pageserver - - safekeeper - - name: inform about versions - debug: msg="Version to deploy - {{ current_version }}, version on storage node - {{ remote_version }}" + debug: msg="Version to deploy - {{ current_version }}" tags: - pageserver - safekeeper - - name: upload and extract Zenith binaries to /usr/local - when: current_version > remote_version or force_deploy ansible.builtin.unarchive: owner: root group: root @@ -74,14 +36,24 @@ hosts: pageservers gather_facts: False remote_user: admin - vars: - force_deploy: false tasks: + + - name: upload init script + when: console_mgmt_base_url is defined + ansible.builtin.template: + src: scripts/init_pageserver.sh + dest: /tmp/init_pageserver.sh + owner: root + group: root + mode: '0755' + become: true + tags: + - pageserver + - name: init pageserver - when: current_version > remote_version or force_deploy shell: - cmd: sudo -u pageserver /usr/local/bin/pageserver -c "pg_distrib_dir='/usr/local'" --init -D /storage/pageserver/data + cmd: /tmp/init_pageserver.sh args: creates: "/storage/pageserver/data/tenants" environment: @@ -91,8 +63,23 @@ tags: - pageserver + # It seems that currently S3 integration does not play well + # even with fresh pageserver without a burden of old data. + # TODO: turn this back on once the issue is solved. + # - name: update remote storage (s3) config + # lineinfile: + # path: /storage/pageserver/data/pageserver.toml + # line: "{{ item }}" + # loop: + # - "[remote_storage]" + # - "bucket_name = '{{ bucket_name }}'" + # - "bucket_region = '{{ bucket_region }}'" + # - "prefix_in_bucket = '{{ inventory_hostname }}'" + # become: true + # tags: + # - pageserver + - name: upload systemd service definition - when: current_version > remote_version or force_deploy ansible.builtin.template: src: systemd/pageserver.service dest: /etc/systemd/system/pageserver.service @@ -104,7 +91,6 @@ - pageserver - name: start systemd service - when: current_version > remote_version or force_deploy ansible.builtin.systemd: daemon_reload: yes name: pageserver @@ -115,7 +101,7 @@ - pageserver - name: post version to console - when: (current_version > remote_version or force_deploy) and console_mgmt_base_url is defined + when: console_mgmt_base_url is defined shell: cmd: | INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id) @@ -127,22 +113,42 @@ hosts: safekeepers gather_facts: False remote_user: admin - vars: - force_deploy: false tasks: + - name: upload init script + when: console_mgmt_base_url is defined + ansible.builtin.template: + src: scripts/init_safekeeper.sh + dest: /tmp/init_safekeeper.sh + owner: root + group: root + mode: '0755' + become: true + tags: + - safekeeper + + - name: init safekeeper + shell: + cmd: /tmp/init_safekeeper.sh + args: + creates: "/storage/safekeeper/data/safekeeper.id" + environment: + ZENITH_REPO_DIR: "/storage/safekeeper/data" + LD_LIBRARY_PATH: "/usr/local/lib" + become: true + tags: + - safekeeper + # in the future safekeepers should discover pageservers byself # but currently use first pageserver that was discovered - name: set first pageserver var for safekeepers - when: current_version > remote_version or force_deploy set_fact: first_pageserver: "{{ hostvars[groups['pageservers'][0]]['inventory_hostname'] }}" tags: - safekeeper - name: upload systemd service definition - when: current_version > remote_version or force_deploy ansible.builtin.template: src: systemd/safekeeper.service dest: /etc/systemd/system/safekeeper.service @@ -154,7 +160,6 @@ - safekeeper - name: start systemd service - when: current_version > remote_version or force_deploy ansible.builtin.systemd: daemon_reload: yes name: safekeeper @@ -165,7 +170,7 @@ - safekeeper - name: post version to console - when: (current_version > remote_version or force_deploy) and console_mgmt_base_url is defined + when: console_mgmt_base_url is defined shell: cmd: | INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id) diff --git a/.circleci/ansible/production.hosts b/.circleci/ansible/production.hosts index c5b4f664a6..13224b7cf5 100644 --- a/.circleci/ansible/production.hosts +++ b/.circleci/ansible/production.hosts @@ -1,7 +1,16 @@ [pageservers] -zenith-1-ps-1 +zenith-1-ps-1 console_region_id=1 [safekeepers] -zenith-1-sk-1 -zenith-1-sk-2 -zenith-1-sk-3 +zenith-1-sk-1 console_region_id=1 +zenith-1-sk-2 console_region_id=1 +zenith-1-sk-3 console_region_id=1 + +[storage:children] +pageservers +safekeepers + +[storage:vars] +console_mgmt_base_url = http://console-release.local +bucket_name = zenith-storage-oregon +bucket_region = us-west-2 diff --git a/.circleci/ansible/scripts/init_pageserver.sh b/.circleci/ansible/scripts/init_pageserver.sh new file mode 100644 index 0000000000..1cbdd0db94 --- /dev/null +++ b/.circleci/ansible/scripts/init_pageserver.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# get instance id from meta-data service +INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id) + +# store fqdn hostname in var +HOST=$(hostname -f) + + +cat < /tmp/cache-key-postgres + command: | + git rev-parse HEAD:vendor/postgres > /tmp/cache-key-postgres + cat Makefile >> /tmp/cache-key-postgres - restore_cache: name: Restore postgres cache @@ -78,11 +81,14 @@ jobs: - checkout # Grab the postgres git revision to build a cache key. + # Append makefile as it could change the way postgres is built. # Note this works even though the submodule hasn't been checkout out yet. - run: name: Get postgres cache key command: | git rev-parse HEAD:vendor/postgres > /tmp/cache-key-postgres + cat Makefile >> /tmp/cache-key-postgres + - restore_cache: name: Restore postgres cache @@ -111,7 +117,12 @@ jobs: fi export CARGO_INCREMENTAL=0 + export CACHEPOT_BUCKET=zenith-rust-cachepot + export RUSTC_WRAPPER=cachepot + export AWS_ACCESS_KEY_ID="${CACHEPOT_AWS_ACCESS_KEY_ID}" + export AWS_SECRET_ACCESS_KEY="${CACHEPOT_AWS_SECRET_ACCESS_KEY}" "${cov_prefix[@]}" mold -run cargo build $CARGO_FLAGS --bins --tests + cachepot -s - save_cache: name: Save rust cache @@ -141,11 +152,13 @@ jobs: command: | if [[ $BUILD_TYPE == "debug" ]]; then cov_prefix=(scripts/coverage "--profraw-prefix=$CIRCLE_JOB" --dir=/tmp/zenith/coverage run) + CARGO_FLAGS= elif [[ $BUILD_TYPE == "release" ]]; then cov_prefix=() + CARGO_FLAGS=--release fi - "${cov_prefix[@]}" cargo test + "${cov_prefix[@]}" cargo test $CARGO_FLAGS # Install the rust binaries, for use by test jobs - run: @@ -215,12 +228,12 @@ jobs: - checkout - restore_cache: keys: - - v1-python-deps-{{ checksum "poetry.lock" }} + - v2-python-deps-{{ checksum "poetry.lock" }} - run: name: Install deps command: ./scripts/pysync - save_cache: - key: v1-python-deps-{{ checksum "poetry.lock" }} + key: v2-python-deps-{{ checksum "poetry.lock" }} paths: - /home/circleci/.cache/pypoetry/virtualenvs - run: @@ -274,12 +287,12 @@ jobs: - run: git submodule update --init --depth 1 - restore_cache: keys: - - v1-python-deps-{{ checksum "poetry.lock" }} + - v2-python-deps-{{ checksum "poetry.lock" }} - run: name: Install deps command: ./scripts/pysync - save_cache: - key: v1-python-deps-{{ checksum "poetry.lock" }} + key: v2-python-deps-{{ checksum "poetry.lock" }} paths: - /home/circleci/.cache/pypoetry/virtualenvs - run: @@ -464,7 +477,10 @@ jobs: name: Build and push compute-tools Docker image command: | echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin - docker build -t zenithdb/compute-tools:latest -f Dockerfile.compute-tools . + docker build \ + --build-arg AWS_ACCESS_KEY_ID="${CACHEPOT_AWS_ACCESS_KEY_ID}" \ + --build-arg AWS_SECRET_ACCESS_KEY="${CACHEPOT_AWS_SECRET_ACCESS_KEY}" \ + --tag zenithdb/compute-tools:latest -f Dockerfile.compute-tools . docker push zenithdb/compute-tools:latest - run: name: Init postgres submodule @@ -518,7 +534,10 @@ jobs: name: Build and push compute-tools Docker image command: | echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin - docker build -t zenithdb/compute-tools:release -f Dockerfile.compute-tools . + docker build \ + --build-arg AWS_ACCESS_KEY_ID="${CACHEPOT_AWS_ACCESS_KEY_ID}" \ + --build-arg AWS_SECRET_ACCESS_KEY="${CACHEPOT_AWS_SECRET_ACCESS_KEY}" \ + --tag zenithdb/compute-tools:release -f Dockerfile.compute-tools . docker push zenithdb/compute-tools:release - run: name: Init postgres submodule @@ -605,7 +624,7 @@ jobs: ssh-add ssh-key rm -f ssh-key ssh-key-cert.pub - ansible-playbook deploy.yaml -i production.hosts -e console_mgmt_base_url=http://console-release.local + ansible-playbook deploy.yaml -i production.hosts rm -f zenith_install.tar.gz .zenith_current_version deploy-release-proxy: diff --git a/.config/hakari.toml b/.config/hakari.toml new file mode 100644 index 0000000000..7bccc6c4a3 --- /dev/null +++ b/.config/hakari.toml @@ -0,0 +1,24 @@ +# This file contains settings for `cargo hakari`. +# See https://docs.rs/cargo-hakari/latest/cargo_hakari/config for a full list of options. + +hakari-package = "workspace_hack" + +# Format for `workspace-hack = ...` lines in other Cargo.tomls. Requires cargo-hakari 0.9.8 or above. +dep-format-version = "2" + +# Setting workspace.resolver = "2" in the root Cargo.toml is HIGHLY recommended. +# Hakari works much better with the new feature resolver. +# For more about the new feature resolver, see: +# https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#cargos-new-feature-resolver +resolver = "2" + +# Add triples corresponding to platforms commonly used by developers here. +# https://doc.rust-lang.org/rustc/platform-support.html +platforms = [ + # "x86_64-unknown-linux-gnu", + # "x86_64-apple-darwin", + # "x86_64-pc-windows-msvc", +] + +# Write out exact versions rather than a semver range. (Defaults to false.) +# exact-versions = true diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 218783387b..83e46ce6be 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -1,10 +1,6 @@ name: Build and Test -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] +on: push jobs: regression-check: @@ -13,7 +9,7 @@ jobs: # If we want to duplicate this job for different # Rust toolchains (e.g. nightly or 1.37.0), add them here. rust_toolchain: [stable] - os: [ubuntu-latest] + os: [ubuntu-latest, macos-latest] timeout-minutes: 30 name: run regression test suite runs-on: ${{ matrix.os }} @@ -32,11 +28,17 @@ jobs: toolchain: ${{ matrix.rust_toolchain }} override: true - - name: Install postgres dependencies + - name: Install Ubuntu postgres dependencies + if: matrix.os == 'ubuntu-latest' run: | sudo apt update sudo apt install build-essential libreadline-dev zlib1g-dev flex bison libseccomp-dev + - name: Install macOs postgres dependencies + if: matrix.os == 'macos-latest' + run: | + brew install flex bison + - name: Set pg revision for caching id: pg_ver run: echo ::set-output name=pg_rev::$(git rev-parse HEAD:vendor/postgres) diff --git a/Cargo.lock b/Cargo.lock index cac5996ef7..1a9e261281 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,12 +17,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "ahash" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" - [[package]] name = "ahash" version = "0.7.6" @@ -61,58 +55,6 @@ dependencies = [ "backtrace", ] -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "askama" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d298738b6e47e1034e560e5afe63aa488fea34e25ec11b855a76f0d7b8e73134" -dependencies = [ - "askama_derive", - "askama_escape", - "askama_shared", -] - -[[package]] -name = "askama_derive" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2925c4c290382f9d2fa3d1c1b6a63fa1427099721ecca4749b154cc9c25522" -dependencies = [ - "askama_shared", - "proc-macro2", - "syn", -] - -[[package]] -name = "askama_escape" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" - -[[package]] -name = "askama_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d6083ccb191711e9c2b80b22ee24a8381a18524444914c746d4239e21d1afaf" -dependencies = [ - "askama_escape", - "humansize", - "nom 6.1.2", - "num-traits", - "percent-encoding", - "proc-macro2", - "quote", - "serde", - "syn", - "toml", -] - [[package]] name = "async-compression" version = "0.3.12" @@ -127,6 +69,27 @@ dependencies = [ "zstd-safe", ] +[[package]] +name = "async-stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.52" @@ -138,23 +101,6 @@ dependencies = [ "syn", ] -[[package]] -name = "attohttpc" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e69e13a99a7e6e070bb114f7ff381e58c7ccc188630121fc4c2fe4bcf24cd072" -dependencies = [ - "http", - "log", - "rustls 0.20.2", - "serde", - "serde_json", - "url", - "webpki 0.22.0", - "webpki-roots", - "wildmatch", -] - [[package]] name = "atty" version = "0.2.14" @@ -172,55 +118,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "aversion" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41992ab8cfcc3026ef9abceffe0c2b0479c043183fc23825e30d22baab6df334" -dependencies = [ - "aversion-macros", - "byteorder", - "serde", - "serde_cbor", - "thiserror", -] - -[[package]] -name = "aversion-macros" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ba5785f953985aa0caca927ba4005880f3b4f53de87f134e810ae3549f744d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "aws-creds" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460a75eac8f3cb7683e0a9a588a83c3ff039331ea7bfbfbfcecf1dacab276e11" -dependencies = [ - "anyhow", - "attohttpc", - "dirs", - "rust-ini", - "serde", - "serde-xml-rs", - "serde_derive", - "url", -] - -[[package]] -name = "aws-region" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e37c2dc2c9047311911ef175e0ffbb3853f17c32b72cf3d562f455e5ff77267" -dependencies = [ - "anyhow", -] - [[package]] name = "backtrace" version = "0.3.64" @@ -286,18 +183,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitvec" -version = "0.19.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.9.0" @@ -307,17 +192,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bookfile" -version = "0.3.0" -source = "git+https://github.com/zenithdb/bookfile.git?branch=generic-readext#d51a99c7a0be48c3d9cc7cb85c9b7fb05ce1100c" -dependencies = [ - "aversion", - "byteorder", - "serde", - "thiserror", -] - [[package]] name = "boxfnonce" version = "0.1.1" @@ -381,7 +255,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom 7.1.0", + "nom", ] [[package]] @@ -399,6 +273,7 @@ dependencies = [ "libc", "num-integer", "num-traits", + "serde", "time", "winapi", ] @@ -471,6 +346,7 @@ dependencies = [ "serde_json", "tar", "tokio", + "workspace_hack", ] [[package]] @@ -505,6 +381,7 @@ dependencies = [ "regex", "reqwest", "serde", + "serde_with", "tar", "thiserror", "toml", @@ -514,6 +391,22 @@ dependencies = [ "zenith_utils", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "cpufeatures" version = "0.2.1" @@ -532,6 +425,15 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "criterion" version = "0.3.5" @@ -570,9 +472,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" dependencies = [ "cfg-if", "crossbeam-utils", @@ -591,10 +493,11 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9" +checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" dependencies = [ + "autocfg", "cfg-if", "crossbeam-utils", "lazy_static", @@ -664,6 +567,41 @@ dependencies = [ "libc", ] +[[package]] +name = "darling" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d720b8683f8dd83c65155f0530560cba68cd2bf395f6513a483caee57ff7f4" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a340f241d2ceed1deb47ae36c4144b2707ec7dd0b649f894cb39bb595986324" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c41b3b7352feb3211a0d743dc5700a4e3b60f51bd2b368892d1e0f9a95f44b" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.9.0" @@ -674,34 +612,26 @@ dependencies = [ ] [[package]] -name = "dirs" -version = "4.0.0" +name = "dirs-next" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "dirs-sys", + "cfg-if", + "dirs-sys-next", ] [[package]] -name = "dirs-sys" -version = "0.3.6" +name = "dirs-sys-next" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", "winapi", ] -[[package]] -name = "dlv-list" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b" -dependencies = [ - "rand", -] - [[package]] name = "either" version = "1.6.1" @@ -730,6 +660,21 @@ dependencies = [ "termcolor", ] +[[package]] +name = "etcd-client" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585de5039d1ecce74773db49ba4e8107e42be7c2cd0b1a9e7fce27181db7b118" +dependencies = [ + "http", + "prost", + "tokio", + "tokio-stream", + "tonic", + "tonic-build", + "tower-service", +] + [[package]] name = "fail" version = "0.5.0" @@ -768,12 +713,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "fixedbitset" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -794,12 +760,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "funty" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" - [[package]] name = "futures" version = "0.3.21" @@ -907,7 +867,7 @@ checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.10.0+wasi-snapshot-preview1", ] [[package]] @@ -959,7 +919,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util", + "tokio-util 0.6.9", "tracing", ] @@ -969,22 +929,22 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" -[[package]] -name = "hashbrown" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" -dependencies = [ - "ahash 0.4.7", -] - [[package]] name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash 0.7.6", + "ahash", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", ] [[package]] @@ -1065,12 +1025,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" -[[package]] -name = "humansize" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" - [[package]] name = "humantime" version = "2.1.0" @@ -1079,9 +1033,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.16" +version = "0.14.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" +checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd" dependencies = [ "bytes", "futures-channel", @@ -1092,7 +1046,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 0.4.8", + "itoa 1.0.1", "pin-project-lite", "socket2", "tokio", @@ -1114,6 +1068,37 @@ dependencies = [ "tokio-rustls 0.23.2", ] +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -1132,7 +1117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ "autocfg", - "hashbrown 0.11.2", + "hashbrown", ] [[package]] @@ -1224,19 +1209,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "lexical-core" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" -dependencies = [ - "arrayvec", - "bitflags", - "cfg-if", - "ryu", - "static_assertions", -] - [[package]] name = "libc" version = "0.2.117" @@ -1253,12 +1225,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "libm" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" - [[package]] name = "lock_api" version = "0.4.6" @@ -1293,17 +1259,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" -[[package]] -name = "maybe-async" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6007f9dad048e0a224f27ca599d669fca8cfa0dac804725aab542b2eb032bce6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "md-5" version = "0.9.1" @@ -1360,14 +1315,15 @@ dependencies = [ [[package]] name = "mio" -version = "0.7.14" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" dependencies = [ "libc", "log", "miow", "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", "winapi", ] @@ -1380,6 +1336,30 @@ dependencies = [ "winapi", ] +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "native-tls" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nix" version = "0.23.1" @@ -1393,19 +1373,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "nom" -version = "6.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" -dependencies = [ - "bitvec", - "funty", - "lexical-core", - "memchr", - "version_check", -] - [[package]] name = "nom" version = "7.1.0" @@ -1419,9 +1386,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" dependencies = [ "winapi", ] @@ -1454,7 +1421,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -1495,13 +1461,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] -name = "ordered-multimap" -version = "0.3.1" +name = "openssl" +version = "0.10.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" dependencies = [ - "dlv-list", - "hashbrown 0.9.1", + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", ] [[package]] @@ -1520,7 +1509,6 @@ dependencies = [ "anyhow", "async-compression", "async-trait", - "bookfile", "byteorder", "bytes", "chrono", @@ -1540,17 +1528,18 @@ dependencies = [ "log", "nix", "once_cell", - "plotly", "postgres 0.19.1 (git+https://github.com/zenithdb/rust-postgres.git?rev=2949d98df52587d562986aad155dd4e889e408b7)", "postgres-protocol 0.6.1 (git+https://github.com/zenithdb/rust-postgres.git?rev=2949d98df52587d562986aad155dd4e889e408b7)", "postgres-types 0.2.1 (git+https://github.com/zenithdb/rust-postgres.git?rev=2949d98df52587d562986aad155dd4e889e408b7)", "postgres_ffi", "rand", "regex", - "rust-s3", + "rusoto_core", + "rusoto_s3", "scopeguard", "serde", "serde_json", + "serde_with", "signal-hook", "tar", "tempfile", @@ -1558,6 +1547,7 @@ dependencies = [ "tokio", "tokio-postgres 0.7.1 (git+https://github.com/zenithdb/rust-postgres.git?rev=2949d98df52587d562986aad155dd4e889e408b7)", "tokio-stream", + "tokio-util 0.7.0", "toml_edit", "tracing", "tracing-futures", @@ -1624,6 +1614,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "petgraph" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "phf" version = "0.8.0" @@ -1675,17 +1675,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "plotly" -version = "0.7.0" +name = "pkg-config" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f011c8acfc15cfa062397000d894fba55bf3d1eca4301b07d6e82237b06e55" -dependencies = [ - "askama", - "rand", - "rand_distr", - "serde", - "serde_json", -] +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" [[package]] name = "plotters" @@ -1856,6 +1849,59 @@ dependencies = [ "thiserror", ] +[[package]] +name = "prost" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" +dependencies = [ + "bytes", + "heck", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prost", + "prost-types", + "regex", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" +dependencies = [ + "bytes", + "prost", +] + [[package]] name = "proxy" version = "0.1.0" @@ -1865,7 +1911,7 @@ dependencies = [ "clap 3.0.14", "fail", "futures", - "hashbrown 0.11.2", + "hashbrown", "hex", "hyper", "lazy_static", @@ -1879,11 +1925,13 @@ dependencies = [ "scopeguard", "serde", "serde_json", + "socket2", "thiserror", "tokio", "tokio-postgres 0.7.1 (git+https://github.com/zenithdb/rust-postgres.git?rev=2949d98df52587d562986aad155dd4e889e408b7)", "tokio-postgres-rustls", "tokio-rustls 0.22.0", + "workspace_hack", "zenith_metrics", "zenith_utils", ] @@ -1897,12 +1945,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" - [[package]] name = "rand" version = "0.8.4" @@ -1934,16 +1976,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand", -] - [[package]] name = "rand_hc" version = "0.3.1" @@ -2074,7 +2106,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-rustls 0.23.2", - "tokio-util", + "tokio-util 0.6.9", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -2112,43 +2144,85 @@ dependencies = [ ] [[package]] -name = "rust-ini" -version = "0.17.0" +name = "rusoto_core" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22" +checksum = "5b4f000e8934c1b4f70adde180056812e7ea6b1a247952db8ee98c94cd3116cc" dependencies = [ - "cfg-if", - "ordered-multimap", + "async-trait", + "base64 0.13.0", + "bytes", + "crc32fast", + "futures", + "http", + "hyper", + "hyper-tls", + "lazy_static", + "log", + "rusoto_credential", + "rusoto_signature", + "rustc_version", + "serde", + "serde_json", + "tokio", + "xml-rs", ] [[package]] -name = "rust-s3" -version = "0.28.1" +name = "rusoto_credential" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc0e521d1084d6950e050d4e2595f0fbdaa2b96bb795bab3d90a282288c5e49" +checksum = "6a46b67db7bb66f5541e44db22b0a02fed59c9603e146db3a9e633272d3bac2f" dependencies = [ - "anyhow", "async-trait", - "aws-creds", - "aws-region", - "base64 0.13.0", - "cfg-if", "chrono", + "dirs-next", + "futures", + "hyper", + "serde", + "serde_json", + "shlex", + "tokio", + "zeroize", +] + +[[package]] +name = "rusoto_s3" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048c2fe811a823ad5a9acc976e8bf4f1d910df719dcf44b15c3e96c5b7a51027" +dependencies = [ + "async-trait", + "bytes", + "futures", + "rusoto_core", + "xml-rs", +] + +[[package]] +name = "rusoto_signature" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6264e93384b90a747758bcc82079711eacf2e755c3a8b5091687b5349d870bcc" +dependencies = [ + "base64 0.13.0", + "bytes", + "chrono", + "digest", + "futures", "hex", "hmac 0.11.0", "http", + "hyper", "log", - "maybe-async", - "md5", + "md-5", "percent-encoding", - "reqwest", + "pin-project-lite", + "rusoto_credential", + "rustc_version", "serde", - "serde-xml-rs", - "serde_derive", "sha2", "tokio", - "tokio-stream", - "url", ] [[package]] @@ -2215,6 +2289,12 @@ dependencies = [ "rustls 0.19.1", ] +[[package]] +name = "rustversion" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" + [[package]] name = "ryu" version = "1.0.9" @@ -2230,6 +2310,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -2256,6 +2346,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.5" @@ -2271,18 +2384,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-xml-rs" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65162e9059be2f6a3421ebbb4fef3e74b7d9e7c60c50a0e292c6239f19f1edfa" -dependencies = [ - "log", - "serde", - "thiserror", - "xml-rs", -] - [[package]] name = "serde_cbor" version = "0.11.2" @@ -2327,6 +2428,29 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1e6ec4d8950e5b1e894eac0d360742f3b1407a6078a604a731c4b3f49cefbc" +dependencies = [ + "rustversion", + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12e47be9471c72889ebafb5e14d5ff930d89ae7a67bbdb5f8abb564f845a927e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha2" version = "0.9.9" @@ -2419,12 +2543,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "stringprep" version = "0.1.2" @@ -2464,12 +2582,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "tar" version = "0.4.38" @@ -2555,7 +2667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", - "wasi", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] @@ -2586,9 +2698,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.16.1" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" dependencies = [ "bytes", "libc", @@ -2598,10 +2710,21 @@ dependencies = [ "once_cell", "pin-project-lite", "signal-hook-registry", + "socket2", "tokio-macros", "winapi", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-macros" version = "1.7.0" @@ -2613,6 +2736,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-postgres" version = "0.7.1" @@ -2632,7 +2765,7 @@ dependencies = [ "postgres-types 0.2.1 (git+https://github.com/zenithdb/rust-postgres.git?rev=2949d98df52587d562986aad155dd4e889e408b7)", "socket2", "tokio", - "tokio-util", + "tokio-util 0.6.9", ] [[package]] @@ -2654,7 +2787,7 @@ dependencies = [ "postgres-types 0.2.1 (git+https://github.com/zenithdb/rust-postgres.git?rev=9eb0dbfbeb6a6c1b79099b9f7ae4a8c021877858)", "socket2", "tokio", - "tokio-util", + "tokio-util 0.6.9", ] [[package]] @@ -2719,6 +2852,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.5.8" @@ -2741,6 +2888,75 @@ dependencies = [ "serde", ] +[[package]] +name = "tonic" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff08f4649d10a70ffa3522ca559031285d8e421d727ac85c60825761818f5d0a" +dependencies = [ + "async-stream", + "async-trait", + "base64 0.13.0", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "prost-derive", + "tokio", + "tokio-stream", + "tokio-util 0.6.9", + "tower", + "tower-layer", + "tower-service", + "tracing", + "tracing-futures", +] + +[[package]] +name = "tonic-build" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9403f1bafde247186684b230dc6f38b5cd514584e8bec1dd32514be4745fa757" +dependencies = [ + "proc-macro2", + "prost-build", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util 0.7.0", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + [[package]] name = "tower-service" version = "0.3.1" @@ -2754,6 +2970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d8d93354fe2a8e50d5953f5ae2e47a3fc2ef03292e7ea46e3cc38f549525fb9" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2846,6 +3063,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + [[package]] name = "unicode-width" version = "0.1.9" @@ -2882,6 +3105,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" @@ -2916,6 +3145,7 @@ dependencies = [ "const_format", "crc32c", "daemonize", + "etcd-client", "fs2", "hex", "humantime", @@ -2925,14 +3155,18 @@ dependencies = [ "postgres-protocol 0.6.1 (git+https://github.com/zenithdb/rust-postgres.git?rev=2949d98df52587d562986aad155dd4e889e408b7)", "postgres_ffi", "regex", - "rust-s3", + "rusoto_core", + "rusoto_s3", "serde", "serde_json", + "serde_with", "signal-hook", "tempfile", "tokio", "tokio-postgres 0.7.1 (git+https://github.com/zenithdb/rust-postgres.git?rev=2949d98df52587d562986aad155dd4e889e408b7)", + "tokio-util 0.7.0", "tracing", + "url", "walkdir", "workspace_hack", "zenith_metrics", @@ -2955,6 +3189,12 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.79" @@ -3071,12 +3311,6 @@ dependencies = [ "libc", ] -[[package]] -name = "wildmatch" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c48bd20df7e4ced539c12f570f937c6b4884928a87fee70a479d72f031d4e0" - [[package]] name = "winapi" version = "0.3.9" @@ -3121,7 +3355,14 @@ dependencies = [ name = "workspace_hack" version = "0.1.0" dependencies = [ + "anyhow", + "bytes", + "cc", + "clap 2.34.0", + "either", + "hashbrown", "libc", + "log", "memchr", "num-integer", "num-traits", @@ -3129,16 +3370,15 @@ dependencies = [ "quote", "regex", "regex-syntax", + "reqwest", + "scopeguard", "serde", "syn", + "tokio", + "tracing", + "tracing-core", ] -[[package]] -name = "wyz" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" - [[package]] name = "xattr" version = "0.2.2" @@ -3187,6 +3427,7 @@ dependencies = [ "libc", "once_cell", "prometheus", + "workspace_hack", ] [[package]] @@ -3214,6 +3455,7 @@ dependencies = [ "rustls-split", "serde", "serde_json", + "serde_with", "signal-hook", "tempfile", "thiserror", @@ -3225,6 +3467,12 @@ dependencies = [ "zenith_metrics", ] +[[package]] +name = "zeroize" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c88870063c39ee00ec285a2f8d6a966e5b6fb2becc4e8dac77ed0d370ed6006" + [[package]] name = "zstd" version = "0.10.0+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index b20e64a06f..f3ac36dcb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "zenith_metrics", "zenith_utils", ] +resolver = "2" [profile.release] # This is useful for profiling and, to some extent, debug. diff --git a/Dockerfile b/Dockerfile index 9ee6abaa8a..babc3b8e1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,13 +24,14 @@ ARG GIT_VERSION=local ARG CACHEPOT_BUCKET=zenith-rust-cachepot ARG AWS_ACCESS_KEY_ID ARG AWS_SECRET_ACCESS_KEY -#ENV RUSTC_WRAPPER cachepot ENV RUSTC_WRAPPER /usr/local/cargo/bin/cachepot COPY --from=pg-build /pg/tmp_install/include/postgresql/server tmp_install/include/postgresql/server COPY . . -RUN cargo build --release +# Show build caching stats to check if it was used in the end. +# Has to be the part of the same RUN since cachepot daemon is killed in the end of this RUN, loosing the compilation stats. +RUN cargo build --release && /usr/local/cargo/bin/cachepot -s # Build final image # diff --git a/Dockerfile.compute-tools b/Dockerfile.compute-tools index a1f7582ee4..f7672251e6 100644 --- a/Dockerfile.compute-tools +++ b/Dockerfile.compute-tools @@ -1,12 +1,17 @@ # First transient image to build compute_tools binaries # NB: keep in sync with rust image version in .circle/config.yml -FROM rust:1.56.1-slim-buster AS rust-build +FROM zenithdb/build:buster-20220309 AS rust-build WORKDIR /zenith +ARG CACHEPOT_BUCKET=zenith-rust-cachepot +ARG AWS_ACCESS_KEY_ID +ARG AWS_SECRET_ACCESS_KEY +ENV RUSTC_WRAPPER /usr/local/cargo/bin/cachepot + COPY . . -RUN cargo build -p compute_tools --release +RUN cargo build -p compute_tools --release && /usr/local/cargo/bin/cachepot -s # Final image that only has one binary FROM debian:buster-slim diff --git a/Makefile b/Makefile index ef26ceee2d..d2a79661f2 100644 --- a/Makefile +++ b/Makefile @@ -78,6 +78,11 @@ postgres: postgres-configure \ $(MAKE) -C tmp_install/build/contrib/zenith install +@echo "Compiling contrib/zenith_test_utils" $(MAKE) -C tmp_install/build/contrib/zenith_test_utils install + +@echo "Compiling pg_buffercache" + $(MAKE) -C tmp_install/build/contrib/pg_buffercache install + +@echo "Compiling pageinspect" + $(MAKE) -C tmp_install/build/contrib/pageinspect install + .PHONY: postgres-clean postgres-clean: diff --git a/compute_tools/Cargo.toml b/compute_tools/Cargo.toml index 3adf762dcb..56047093f1 100644 --- a/compute_tools/Cargo.toml +++ b/compute_tools/Cargo.toml @@ -16,4 +16,5 @@ regex = "1" serde = { version = "1.0", features = ["derive"] } serde_json = "1" tar = "0.4" -tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } +tokio = { version = "1.17", features = ["macros", "rt", "rt-multi-thread"] } +workspace_hack = { version = "0.1", path = "../workspace_hack" } diff --git a/control_plane/Cargo.toml b/control_plane/Cargo.toml index eff6b3ef2d..e118ea4793 100644 --- a/control_plane/Cargo.toml +++ b/control_plane/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" tar = "0.4.33" postgres = { git = "https://github.com/zenithdb/rust-postgres.git", rev="2949d98df52587d562986aad155dd4e889e408b7" } serde = { version = "1.0", features = ["derive"] } +serde_with = "1.12.0" toml = "0.5" lazy_static = "1.4" regex = "1" @@ -19,4 +20,4 @@ reqwest = { version = "0.11", default-features = false, features = ["blocking", pageserver = { path = "../pageserver" } walkeeper = { path = "../walkeeper" } zenith_utils = { path = "../zenith_utils" } -workspace_hack = { path = "../workspace_hack" } +workspace_hack = { version = "0.1", path = "../workspace_hack" } diff --git a/control_plane/src/local_env.rs b/control_plane/src/local_env.rs index 2a1d51fe08..2bdc76e876 100644 --- a/control_plane/src/local_env.rs +++ b/control_plane/src/local_env.rs @@ -5,6 +5,7 @@ use anyhow::{bail, ensure, Context}; use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; use std::collections::HashMap; use std::env; use std::fs; @@ -12,9 +13,7 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use zenith_utils::auth::{encode_from_key_file, Claims, Scope}; use zenith_utils::postgres_backend::AuthType; -use zenith_utils::zid::{ - HexZTenantId, HexZTimelineId, ZNodeId, ZTenantId, ZTenantTimelineId, ZTimelineId, -}; +use zenith_utils::zid::{ZNodeId, ZTenantId, ZTenantTimelineId, ZTimelineId}; use crate::safekeeper::SafekeeperNode; @@ -25,6 +24,7 @@ use crate::safekeeper::SafekeeperNode; // to 'zenith init --config=' option. See control_plane/simple.conf for // an example. // +#[serde_as] #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] pub struct LocalEnv { // Base directory for all the nodes (the pageserver, safekeepers and @@ -50,12 +50,17 @@ pub struct LocalEnv { // Default tenant ID to use with the 'zenith' command line utility, when // --tenantid is not explicitly specified. #[serde(default)] - pub default_tenant_id: Option, + #[serde_as(as = "Option")] + pub default_tenant_id: Option, // used to issue tokens during e.g pg start #[serde(default)] pub private_key_path: PathBuf, + // A comma separated broker (etcd) endpoints for storage nodes coordination, e.g. 'http://127.0.0.1:2379'. + #[serde(default)] + pub broker_endpoints: Option, + pub pageserver: PageServerConf, #[serde(default)] @@ -66,7 +71,8 @@ pub struct LocalEnv { // A `HashMap>` would be more appropriate here, // 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". - branch_name_mappings: HashMap>, + #[serde_as(as = "HashMap<_, Vec<(DisplayFromStr, DisplayFromStr)>>")] + branch_name_mappings: HashMap>, } #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] @@ -164,9 +170,6 @@ impl LocalEnv { .entry(branch_name.clone()) .or_default(); - let tenant_id = HexZTenantId::from(tenant_id); - let timeline_id = HexZTimelineId::from(timeline_id); - let existing_ids = existing_values .iter() .find(|(existing_tenant_id, _)| existing_tenant_id == &tenant_id); @@ -193,7 +196,6 @@ impl LocalEnv { branch_name: &str, tenant_id: ZTenantId, ) -> Option { - let tenant_id = HexZTenantId::from(tenant_id); self.branch_name_mappings .get(branch_name)? .iter() @@ -207,13 +209,7 @@ impl LocalEnv { .iter() .flat_map(|(name, tenant_timelines)| { tenant_timelines.iter().map(|&(tenant_id, timeline_id)| { - ( - ZTenantTimelineId::new( - ZTenantId::from(tenant_id), - ZTimelineId::from(timeline_id), - ), - name.clone(), - ) + (ZTenantTimelineId::new(tenant_id, timeline_id), name.clone()) }) }) .collect() @@ -259,7 +255,7 @@ impl LocalEnv { // If no initial tenant ID was given, generate it. if env.default_tenant_id.is_none() { - env.default_tenant_id = Some(HexZTenantId::from(ZTenantId::generate())); + env.default_tenant_id = Some(ZTenantId::generate()); } env.base_data_dir = base_path(); diff --git a/control_plane/src/safekeeper.rs b/control_plane/src/safekeeper.rs index 969e2cd531..89ab0a31ee 100644 --- a/control_plane/src/safekeeper.rs +++ b/control_plane/src/safekeeper.rs @@ -73,6 +73,8 @@ pub struct SafekeeperNode { pub http_base_url: String, pub pageserver: Arc, + + broker_endpoints: Option, } impl SafekeeperNode { @@ -89,6 +91,7 @@ impl SafekeeperNode { http_client: Client::new(), http_base_url: format!("http://127.0.0.1:{}/v1", conf.http_port), pageserver, + broker_endpoints: env.broker_endpoints.clone(), } } @@ -135,6 +138,9 @@ impl SafekeeperNode { if !self.conf.sync { cmd.arg("--no-sync"); } + if let Some(ref ep) = self.broker_endpoints { + cmd.args(&["--broker-endpoints", ep]); + } if !cmd.status()?.success() { bail!( diff --git a/control_plane/src/storage.rs b/control_plane/src/storage.rs index f6b7173067..c49d5743a9 100644 --- a/control_plane/src/storage.rs +++ b/control_plane/src/storage.rs @@ -1,4 +1,3 @@ -use std::convert::TryFrom; use std::io::Write; use std::net::TcpStream; use std::path::PathBuf; @@ -10,7 +9,7 @@ use anyhow::{bail, Context}; use nix::errno::Errno; use nix::sys::signal::{kill, Signal}; use nix::unistd::Pid; -use pageserver::http::models::{TenantCreateRequest, TimelineCreateRequest, TimelineInfoResponse}; +use pageserver::http::models::{TenantCreateRequest, TimelineCreateRequest}; use pageserver::timelines::TimelineInfo; use postgres::{Config, NoTls}; use reqwest::blocking::{Client, RequestBuilder, Response}; @@ -19,7 +18,7 @@ use thiserror::Error; use zenith_utils::http::error::HttpErrorBody; use zenith_utils::lsn::Lsn; use zenith_utils::postgres_backend::AuthType; -use zenith_utils::zid::{HexZTenantId, HexZTimelineId, ZTenantId, ZTimelineId}; +use zenith_utils::zid::{ZTenantId, ZTimelineId}; use crate::local_env::LocalEnv; use crate::{fill_rust_env_vars, read_pidfile}; @@ -149,12 +148,20 @@ impl PageServerNode { let initial_timeline_id_string = initial_timeline_id.to_string(); args.extend(["--initial-timeline-id", &initial_timeline_id_string]); - let init_output = fill_rust_env_vars(cmd.args(args)) + let cmd_with_args = cmd.args(args); + let init_output = fill_rust_env_vars(cmd_with_args) .output() - .context("pageserver init failed")?; + .with_context(|| { + format!("failed to init pageserver with command {:?}", cmd_with_args) + })?; if !init_output.status.success() { - bail!("pageserver init failed"); + bail!( + "init invocation failed, {}\nStdout: {}\nStderr: {}", + init_output.status, + String::from_utf8_lossy(&init_output.stdout), + String::from_utf8_lossy(&init_output.stderr) + ); } Ok(initial_timeline_id) @@ -338,9 +345,7 @@ impl PageServerNode { ) -> anyhow::Result> { let tenant_id_string = self .http_request(Method::POST, format!("{}/tenant", self.http_base_url)) - .json(&TenantCreateRequest { - new_tenant_id: new_tenant_id.map(HexZTenantId::from), - }) + .json(&TenantCreateRequest { new_tenant_id }) .send()? .error_from_body()? .json::>()?; @@ -358,7 +363,7 @@ impl PageServerNode { } pub fn timeline_list(&self, tenant_id: &ZTenantId) -> anyhow::Result> { - let timeline_infos: Vec = self + let timeline_infos: Vec = self .http_request( Method::GET, format!("{}/tenant/{}/timeline", self.http_base_url, tenant_id), @@ -367,10 +372,7 @@ impl PageServerNode { .error_from_body()? .json()?; - timeline_infos - .into_iter() - .map(TimelineInfo::try_from) - .collect() + Ok(timeline_infos) } pub fn timeline_create( @@ -386,16 +388,14 @@ impl PageServerNode { format!("{}/tenant/{}/timeline", self.http_base_url, tenant_id), ) .json(&TimelineCreateRequest { - new_timeline_id: new_timeline_id.map(HexZTimelineId::from), + new_timeline_id, ancestor_start_lsn, - ancestor_timeline_id: ancestor_timeline_id.map(HexZTimelineId::from), + ancestor_timeline_id, }) .send()? .error_from_body()? - .json::>()?; + .json::>()?; - timeline_info_response - .map(TimelineInfo::try_from) - .transpose() + Ok(timeline_info_response) } } diff --git a/docs/glossary.md b/docs/glossary.md index 159a078e30..0f82f2d666 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -21,7 +21,7 @@ NOTE:It has nothing to do with PostgreSQL pg_basebackup. ### Branch -We can create branch at certain LSN using `zenith branch` command. +We can create branch at certain LSN using `zenith timeline branch` command. Each Branch lives in a corresponding timeline[] and has an ancestor[]. @@ -36,17 +36,25 @@ A checkpoint record in the WAL marks a point in the WAL sequence at which it is NOTE: This is an overloaded term. Whenever enough WAL has been accumulated in memory, the page server [] -writes out the changes from in-memory layers into new layer files[]. This process -is called "checkpointing". The page server only creates layer files for -relations that have been modified since the last checkpoint. +writes out the changes from the in-memory layer into a new delta layer file. This process +is called "checkpointing". Configuration parameter `checkpoint_distance` defines the distance from current LSN to perform checkpoint of in-memory layers. Default is `DEFAULT_CHECKPOINT_DISTANCE`. -Set this parameter to `0` to force checkpoint of every layer. -Configuration parameter `checkpoint_period` defines the interval between checkpoint iterations. -Default is `DEFAULT_CHECKPOINT_PERIOD`. +### Compaction + +A background operation on layer files. Compaction takes a number of L0 +layer files, each of which covers the whole key space and a range of +LSN, and reshuffles the data in them into L1 files so that each file +covers the whole LSN range, but only part of the key space. + +Compaction should also opportunistically leave obsolete page versions +from the L1 files, and materialize other page versions for faster +access. That hasn't been implemented as of this writing, though. + + ### Compute node Stateless Postgres node that stores data in pageserver. @@ -54,10 +62,10 @@ Stateless Postgres node that stores data in pageserver. ### Garbage collection The process of removing old on-disk layers that are not needed by any timeline anymore. + ### Fork Each of the separate segmented file sets in which a relation is stored. The main fork is where the actual data resides. There also exist two secondary forks for metadata: the free space map and the visibility map. -Each PostgreSQL fork is considered a separate relish. ### Layer @@ -72,15 +80,15 @@ are immutable. See pageserver/src/layered_repository/README.md for more. ### Layer file (on-disk layer) Layered repository on-disk format is based on immutable files. The -files are called "layer files". Each file corresponds to one RELISH_SEG_SIZE -segment of a PostgreSQL relation fork. There are two kinds of layer -files: image files and delta files. An image file contains a -"snapshot" of the segment at a particular LSN, and a delta file -contains WAL records applicable to the segment, in a range of LSNs. +files are called "layer files". There are two kinds of layer files: +image files and delta files. An image file contains a "snapshot" of a +range of keys at a particular LSN, and a delta file contains WAL +records applicable to a range of keys, in a range of LSNs. ### Layer map -The layer map tracks what layers exist for all the relishes in a timeline. +The layer map tracks what layers exist in a timeline. + ### Layered repository Zenith repository implementation that keeps data in layers. @@ -149,14 +157,6 @@ and create new databases and accounts (control plane API in our case). The generic term in PostgreSQL for all objects in a database that have a name and a list of attributes defined in a specific order. -### Relish - -We call each relation and other file that is stored in the -repository a "relish". It comes from "rel"-ish, as in "kind of a -rel", because it covers relations as well as other things that are -not relations, but are treated similarly for the purposes of the -storage layer. - ### Replication slot @@ -173,27 +173,18 @@ One repository corresponds to one Tenant. How much history do we need to keep around for PITR and read-only nodes? -### Segment (PostgreSQL) - -NOTE: This is an overloaded term. +### Segment A physical file that stores data for a given relation. File segments are limited in size by a compile-time setting (1 gigabyte by default), so if a relation exceeds that size, it is split into multiple segments. -### Segment (Layered Repository) - -NOTE: This is an overloaded term. - -Segment is a RELISH_SEG_SIZE slice of relish (identified by a SegmentTag). - ### SLRU SLRUs include pg_clog, pg_multixact/members, and pg_multixact/offsets. There are other SLRUs in PostgreSQL, but they don't need to be stored permanently (e.g. pg_subtrans), or we do not support them in zenith yet (pg_commit_ts). -Each SLRU segment is considered a separate relish[]. ### Tenant (Multitenancy) Tenant represents a single customer, interacting with Zenith. diff --git a/docs/rfcs/014-safekeepers-gossip.md b/docs/rfcs/014-safekeepers-gossip.md new file mode 100644 index 0000000000..3d6cc04b94 --- /dev/null +++ b/docs/rfcs/014-safekeepers-gossip.md @@ -0,0 +1,69 @@ +# Safekeeper gossip + +Extracted from this [PR](https://github.com/zenithdb/rfcs/pull/13) + +## Motivation + +In some situations, safekeeper (SK) needs coordination with other SK's that serve the same tenant: + +1. WAL deletion. SK needs to know what WAL was already safely replicated to delete it. Now we keep WAL indefinitely. +2. Deciding on who is sending WAL to the pageserver. Now sending SK crash may lead to a livelock where nobody sends WAL to the pageserver. +3. To enable SK to SK direct recovery without involving the compute + +## Summary + +Compute node has connection strings to each safekeeper. During each compute->safekeeper connection establishment, the compute node should pass down all that connection strings to each safekeeper. With that info, safekeepers may establish Postgres connections to each other and periodically send ping messages with LSN payload. + +## Components + +safekeeper, compute, compute<->safekeeper protocol, possibly console (group SK addresses) + +## Proposed implementation + +Each safekeeper can periodically ping all its peers and share connectivity and liveness info. If the ping was not receiver for, let's say, four ping periods, we may consider sending safekeeper as dead. That would mean some of the alive safekeepers should connect to the pageserver. One way to decide which one exactly: `make_connection = my_node_id == min(alive_nodes)` + +Since safekeepers are multi-tenant, we may establish either per-tenant physical connections or per-safekeeper ones. So it makes sense to group "logical" connections between corresponding tenants on different nodes into a single physical connection. That means that we should implement an interconnect thread that maintains physical connections and periodically broadcasts info about all tenants. + +Right now console may assign any 3 SK addresses to a given compute node. That may lead to a high number of gossip connections between SK's. Instead, we can assign safekeeper triples to the compute node. But if we want to "break"/" change" group by an ad-hoc action, we can do it. + +### Corner cases + +- Current safekeeper may be alive but may not have connectivity to the pageserver + + To address that, we need to gossip visibility info. Based on that info, we may define SK as alive only when it can connect to the pageserver. + +- Current safekeeper may be alive but may not have connectivity with the compute node. + + We may broadcast last_received_lsn and presence of compute connection and decide who is alive based on that. + +- It is tricky to decide when to shut down gossip connections because we need to be sure that pageserver got all the committed (in the distributed sense, so local SK info is not enough) records, and it may never lose them. It is not a strict requirement since `--sync-safekeepers` that happen before the compute start will allow the pageserver to consume missing WAL, but it is better to do that in the background. So the condition may look like that: `majority_max(flush_lsn) == pageserver_s3_lsn` Here we rely on the two facts: + - that `--sync-safekeepers` happened after the compute shutdown, and it advanced local commit_lsn's allowing pageserver to consume that WAL. + + - we wait for the `pageserver_s3_lsn` advancement to avoid pageserver's last_received_lsn/disk_consistent_lsn going backward due to the disk/hardware failure and subsequent S3 recovery + + If those conditions are not met, we will have some gossip activity (but that may be okay). + +## Pros/cons + +Pros: + +- distributed, does not introduce new services (like etcd), does not add console as a storage dependency +- lays the foundation for gossip-based recovery + +Cons: + +- Only compute knows a set of safekeepers, but they should communicate even without compute node. In case of safekeepers restart, we will lose that info and can't gossip anymore. Hence we can't trim some WAL tail until the compute node start. Also, it is ugly. + +- If the console assigns a random set of safekeepers to each Postgres, we may end up in a situation where each safekeeper needs to have a connection with all other safekeepers. We can group safekeepers into isolated triples in the console to avoid that. Then "mixing" would happen only if we do rebalancing. + +## Alternative implementation + +We can have a selected node (e.g., console) with everybody reporting to it. + +## Security implications + +We don't increase the attack surface here. Communication can happen in a private network that is not exposed to users. + +## Scalability implications + +The only thing that may grow as we grow the number of computes is the number of gossip connections. But if we group safekeepers and assign a compute node to the random SK triple, the number of connections would be constant. diff --git a/docs/rfcs/014-storage-lsm.md b/docs/rfcs/014-storage-lsm.md index c6f8eb5914..fed9ec38cb 100644 --- a/docs/rfcs/014-storage-lsm.md +++ b/docs/rfcs/014-storage-lsm.md @@ -73,8 +73,11 @@ is accessed through a buffer cache. If the page server crashes, the Memtable is lost. It is rebuilt by processing again the WAL that's newer than the latest layer in L0. -The size of the Memtable is equal to the "checkpoint distance", or the -amount of WAL that we need to keep in the safekeeper. +The size of the Memtable is configured by the "checkpoint distance" +setting. Because anything that hasn't been flushed to disk and +uploaded to S3 yet needs to be kept in the safekeeper, the "checkpoint +distance" also determines the amount of WAL that needs to kept in the +safekeeper. # L0 @@ -129,7 +132,7 @@ collecting layers that are older than the GC horizon. # Partitioning scheme When compaction happens and creates a new set of files in L1, how do -we partition the data into the files? +we partition the data into the files? - Goal is that each file is ~ 1 GB in size - Try to match partition boundaries at relation boundaries. (See [1] diff --git a/docs/rfcs/015-storage-messaging.md b/docs/rfcs/015-storage-messaging.md new file mode 100644 index 0000000000..47bc9eb89c --- /dev/null +++ b/docs/rfcs/015-storage-messaging.md @@ -0,0 +1,295 @@ +# Storage messaging + +Created on 19.01.22 + +Initially created [here](https://github.com/zenithdb/rfcs/pull/16) by @kelvich. + +That it is an alternative to (014-safekeeper-gossip)[] + +## Motivation + +As in 014-safekeeper-gossip we need to solve the following problems: + +* Trim WAL on safekeepers +* Decide on which SK should push WAL to the S3 +* Decide on which SK should forward WAL to the pageserver +* Decide on when to shut down SK<->pageserver connection + +This RFC suggests a more generic and hopefully more manageable way to address those problems. However, unlike 014-safekeeper-gossip, it does not bring us any closer to safekeeper-to-safekeeper recovery but rather unties two sets of different issues we previously wanted to solve with gossip. + +Also, with this approach, we would not need "call me maybe" anymore, and the pageserver will have all the data required to understand that it needs to reconnect to another safekeeper. + +## Summary + +Instead of p2p gossip, let's have a centralized broker where all the storage nodes report per-timeline state. Each storage node should have a `--broker-url=1.2.3.4` CLI param. + +Here I propose two ways to do that. After a lot of arguing with myself, I'm leaning towards the etcd approach. My arguments for it are in the pros/cons section. Both options require adding a Grpc client in our codebase either directly or as an etcd dependency. + +## Non-goals + +That RFC does *not* suggest moving the compute to pageserver and compute to safekeeper mappings out of the console. The console is still the only place in the cluster responsible for the persistency of that info. So I'm implying that each pageserver and safekeeper exactly knows what timelines he serves, as it currently is. We need some mechanism for a new pageserver to discover mapping info, but that is out of the scope of this RFC. + +## Impacted components + +pageserver, safekeeper +adds either etcd or console as a storage dependency + +## Possible implementation: custom message broker in the console + +We've decided to go with an etcd approach instead of the message broker. + +
+Original suggestion +
+We can add a Grpc service in the console that acts as a message broker since the console knows the addresses of all the components. The broker can ignore the payload and only redirect messages. So, for example, each safekeeper may send a message to the peering safekeepers or to the pageserver responsible for a given timeline. + +Message format could be `{sender, destination, payload}`. + +The destination is either: +1. `sk_#{tenant}_#{timeline}` -- to be broadcasted on all safekeepers, responsible for that timeline, or +2. `pserver_#{tenant}_#{timeline}` -- to be broadcasted on all pageservers, responsible for that timeline + +Sender is either: +1. `sk_#{sk_id}`, or +2. `pserver_#{pserver_id}` + +I can think of the following behavior to address our original problems: + +* WAL trimming + Each safekeeper periodically broadcasts `(write_lsn, commit_lsn)` to all peering (peering == responsible for that timeline) safekeepers + +* Decide on which SK should push WAL to the S3 + + Each safekeeper periodically broadcasts `i_am_alive_#{current_timestamp}` message to all peering safekeepers. That way, safekeepers may maintain the vector of alive peers (loose one, with false negatives). Alive safekeeper with the minimal id pushes data to S3. + +* Decide on which SK should forward WAL to the pageserver + + Each safekeeper periodically sends (write_lsn, commit_lsn, compute_connected) to the relevant pageservers. With that info, pageserver can maintain a view of the safekeepers state, connect to a random one, and detect the moments (e.g., one the safekeepers is not making progress or down) when it needs to reconnect to another safekeeper. Pageserver should resolve exact IP addresses through the console, e.g., exchange `#sk_#{sk_id}` to `4.5.6.7:6400`. + + Pageserver connection to the safekeeper triggered by the state change `compute_connected: false -> true`. With that, we don't need "call me maybe" anymore. + + Also, we don't have a "peer address amnesia" problem as in the gossip approach (with gossip, after a simultaneous reboot, safekeepers wouldn't know each other addresses until the next compute connection). + +* Decide on when to shutdown sk<->pageserver connection + + Again, pageserver would have all the info to understand when to shut down the safekeeper connection. + +### Scalability + +One node is enough (c) No, seriously, it is enough. + +### High Availability + +Broker lives in the console, so we can rely on k8s maintaining the console app alive. + +If the console is down, we won't trim WAL and reconnect the pageserver to another safekeeper. But, at the same, if the console is down, we already can't accept new compute connections and start stopped computes, so we are making things a bit worse, but not dramatically. + +### Interactions + +``` + .________________. +sk_1 <-> | | <-> pserver_1 +... | Console broker | ... +sk_n <-> |________________| <-> pserver_m +``` +
+ + +## Implementation: etcd state store + +Alternatively, we can set up `etcd` and maintain the following data structure in it: + +```ruby +"compute_#{tenant}_#{timeline}" => { + safekeepers => { + "sk_#{sk_id}" => { + write_lsn: "0/AEDF130", + commit_lsn: "0/AEDF100", + compute_connected: true, + last_updated: 1642621138, + }, + } +} +``` + +As etcd doesn't support field updates in the nested objects that translates to the following set of keys: + +```ruby +"compute_#{tenant}_#{timeline}/safekeepers/sk_#{sk_id}/write_lsn", +"compute_#{tenant}_#{timeline}/safekeepers/sk_#{sk_id}/commit_lsn", +... +``` + +Each storage node can subscribe to the relevant sets of keys and maintain a local view of that structure. So in terms of the data flow, everything is the same as in the previous approach. Still, we can avoid implementing the message broker and prevent runtime storage dependency on a console. + +### Safekeeper address discovery + +During the startup safekeeper should publish the address he is listening on as the part of `{"sk_#{sk_id}" => ip_address}`. Then the pageserver can resolve `sk_#{sk_id}` to the actual address. This way it would work both locally and in the cloud setup. Safekeeper should have `--advertised-address` CLI option so that we can listen on e.g. 0.0.0.0 but advertize something more useful. + +### Safekeeper behavior + +For each timeline safekeeper periodically broadcasts `compute_#{tenant}_#{timeline}/safekeepers/sk_#{sk_id}/*` fields. It subscribes to changes of `compute_#{tenant}_#{timeline}` -- that way safekeeper will have an information about peering safekeepers. +That amount of information is enough to properly trim WAL. To decide on who is pushing the data to S3 safekeeper may use etcd leases or broadcast a timestamp and hence track who is alive. + +### Pageserver behavior + +Pageserver subscribes to `compute_#{tenant}_#{timeline}` for each tenant it owns. With that info, pageserver can maintain a view of the safekeepers state, connect to a random one, and detect the moments (e.g., one the safekeepers is not making progress or down) when it needs to reconnect to another safekeeper. Pageserver should resolve exact IP addresses through the console, e.g., exchange `#sk_#{sk_id}` to `4.5.6.7:6400`. + +Pageserver connection to the safekeeper can be triggered by the state change `compute_connected: false -> true`. With that, we don't need "call me maybe" anymore. + +As an alternative to compute_connected, we can track timestamp of the latest message arrived to safekeeper from compute. Usually compute broadcasts KeepAlive to all safekeepers every second, so it'll be updated every second when connection is ok. Then the connection can be considered down when this timestamp isn't updated for a several seconds. + +This will help to faster detect issues with safekeeper (and switch to another) in the following cases: + + when compute failed but TCP connection stays alive until timeout (usually about a minute) + when safekeeper failed and didn't set compute_connected to false + +Another way to deal with [2] is to process (write_lsn, commit_lsn, compute_connected) as a KeepAlive on the pageserver side and detect issues when sk_id don't send anything for some time. This way is fully compliant to this RFC. + +Also, we don't have a "peer address amnesia" problem as in the gossip approach (with gossip, after a simultaneous reboot, safekeepers wouldn't know each other addresses until the next compute connection). + +### Interactions + +``` + .________________. +sk_1 <-> | | <-> pserver_1 +... | etcd | ... +sk_n <-> |________________| <-> pserver_m +``` + +### Sequence diagrams for different workflows + +#### Cluster startup + +```mermaid +sequenceDiagram + autonumber + participant C as Compute + participant SK1 + participant SK2 + participant SK3 + participant PS1 + participant PS2 + participant O as Orchestrator + participant M as Metadata Service + + PS1->>M: subscribe to updates to state of timeline N + C->>+SK1: WAL push + loop constantly update current lsns + SK1->>-M: I'm at lsn A + end + C->>+SK2: WAL push + loop constantly update current lsns + SK2->>-M: I'm at lsn B + end + C->>+SK3: WAL push + loop constantly update current lsns + SK3->>-M: I'm at lsn C + end + loop request pages + C->>+PS1: get_page@lsn + PS1->>-C: page image + end + M->>PS1: New compute appeared for timeline N. SK1 at A, SK2 at B, SK3 at C + note over PS1: Say SK1 at A=200, SK2 at B=150 SK3 at C=100
so connect to SK1 because it is the most up to date one + PS1->>SK1: start replication +``` + +#### Behavour of services during typical operations + +```mermaid +sequenceDiagram + autonumber + participant C as Compute + participant SK1 + participant SK2 + participant SK3 + participant PS1 + participant PS2 + participant O as Orchestrator + participant M as Metadata Service + + note over C,M: Scenario 1: Pageserver checkpoint + note over PS1: Upload data to S3 + PS1->>M: Update remote consistent lsn + M->>SK1: propagate remote consistent lsn update + note over SK1: truncate WAL up to remote consistent lsn + M->>SK2: propagate remote consistent lsn update + note over SK2: truncate WAL up to remote consistent lsn + M->>SK3: propagate remote consistent lsn update + note over SK3: truncate WAL up to remote consistent lsn + note over C,M: Scenario 2: SK1 finds itself lagging behind MAX(150 (SK2), 200 (SK2)) - 100 (SK1) > THRESHOLD + SK1->>SK2: Fetch WAL delta between 100 (SK1) and 200 (SK2) + note over C,M: Scenario 3: PS1 detects that SK1 is lagging behind: Connection from SK1 is broken or there is no messages from it in 30 seconds. + note over PS1: e.g. SK2 is at 150, SK3 is at 100, chose SK2 as a new replication source + PS1->>SK2: start replication +``` + +#### Behaviour during timeline relocation + +```mermaid +sequenceDiagram + autonumber + participant C as Compute + participant SK1 + participant SK2 + participant SK3 + participant PS1 + participant PS2 + participant O as Orchestrator + participant M as Metadata Service + + note over C,M: Timeline is being relocated from PS1 to PS2 + O->>+PS2: Attach timeline + PS2->>-O: 202 Accepted if timeline exists in S3 + note over PS2: Download timeline from S3 + note over O: Poll for timeline download (or subscribe to metadata service) + loop wait for attach to complete + O->>PS2: timeline detail should answer that timeline is ready + end + PS2->>M: Register downloaded timeline + PS2->>M: Get safekeepers for timeline, subscribe to changes + PS2->>SK1: Start replication to catch up + note over O: PS2 catched up, time to switch compute + O->>C: Restart compute with new pageserver url in config + note over C: Wal push is restarted + loop request pages + C->>+PS2: get_page@lsn + PS2->>-C: page image + end + O->>PS1: detach timeline + note over C,M: Scenario 1: Attach call failed + O--xPS2: Attach timeline + note over O: The operation can be safely retried,
if we hit some threshold we can try another pageserver + note over C,M: Scenario 2: Attach succeeded but pageserver failed to download the data or start replication + loop wait for attach to complete + O--xPS2: timeline detail should answer that timeline is ready + end + note over O: Can wait for a timeout, and then try another pageserver
there should be a limit on number of different pageservers to try + note over C,M: Scenario 3: Detach fails + O--xPS1: Detach timeline + note over O: can be retried, if continues to fail might lead to data duplication in s3 +``` + +# Pros/cons + +## Console broker/etcd vs gossip: + +Gossip pros: +* gossip allows running storage without the console or etcd + +Console broker/etcd pros: +* simpler +* solves "call me maybe" as well +* avoid possible N-to-N connection issues with gossip without grouping safekeepers in pre-defined triples + +## Console broker vs. etcd: + +Initially, I wanted to avoid etcd as a dependency mostly because I've seen how painful for Clickhouse was their ZooKeeper dependency: in each chat, at each conference, people were complaining about configuration and maintenance barriers with ZooKeeper. It was that bad that ClickHouse re-implemented ZooKeeper to embed it: https://clickhouse.com/docs/en/operations/clickhouse-keeper/. + +But with an etcd we are in a bit different situation: + +1. We don't need persistency and strong consistency guarantees for the data we store in the etcd +2. etcd uses Grpc as a protocol, and messages are pretty simple + +So it looks like implementing in-mem store with etcd interface is straightforward thing _if we will want that in future_. At the same time, we can avoid implementing it right now, and we will be able to run local zenith installation with etcd running somewhere in the background (as opposed to building and running console, which in turn requires Postgres). diff --git a/docs/settings.md b/docs/settings.md index 571cfba8df..69aadc602f 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -68,11 +68,11 @@ S3. The unit is # of bytes. -#### checkpoint_period +#### compaction_period -The pageserver checks whether `checkpoint_distance` has been reached -every `checkpoint_period` seconds. Default is 1 s, which should be -fine. +Every `compaction_period` seconds, the page server checks if +maintenance operations, like compaction, are needed on the layer +files. Default is 1 s, which should be fine. #### gc_horizon diff --git a/docs/sourcetree.md b/docs/sourcetree.md index 8d35d35f2f..89b07de8d2 100644 --- a/docs/sourcetree.md +++ b/docs/sourcetree.md @@ -67,6 +67,8 @@ For more detailed info, see `/walkeeper/README` `/workspace_hack`: The workspace_hack crate exists only to pin down some dependencies. +We use [cargo-hakari](https://crates.io/crates/cargo-hakari) for automation. + `/zenith` Main entry point for the 'zenith' CLI utility. diff --git a/pageserver/Cargo.toml b/pageserver/Cargo.toml index 09fa9fd8c9..dccdca291c 100644 --- a/pageserver/Cargo.toml +++ b/pageserver/Cargo.toml @@ -4,20 +4,21 @@ version = "0.1.0" edition = "2021" [dependencies] -bookfile = { git = "https://github.com/zenithdb/bookfile.git", branch="generic-readext" } chrono = "0.4.19" rand = "0.8.3" regex = "1.4.5" bytes = { version = "1.0.1", features = ['serde'] } byteorder = "1.4.3" futures = "0.3.13" +hex = "0.4.3" hyper = "0.14" itertools = "0.10.3" lazy_static = "1.4.0" log = "0.4.14" clap = "3.0" daemonize = "0.4.1" -tokio = { version = "1.11", features = ["process", "sync", "macros", "fs", "rt", "io-util", "time"] } +tokio = { version = "1.17", features = ["process", "sync", "macros", "fs", "rt", "io-util", "time"] } +tokio-util = { version = "0.7", features = ["io"] } postgres-types = { git = "https://github.com/zenithdb/rust-postgres.git", rev="2949d98df52587d562986aad155dd4e889e408b7" } postgres-protocol = { git = "https://github.com/zenithdb/rust-postgres.git", rev="2949d98df52587d562986aad155dd4e889e408b7" } postgres = { git = "https://github.com/zenithdb/rust-postgres.git", rev="2949d98df52587d562986aad155dd4e889e408b7" } @@ -26,14 +27,14 @@ tokio-stream = "0.1.8" anyhow = { version = "1.0", features = ["backtrace"] } crc32c = "0.6.0" thiserror = "1.0" -hex = { version = "0.4.3", features = ["serde"] } tar = "0.4.33" humantime = "2.1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1" +serde_with = "1.12.0" + toml_edit = { version = "0.13", features = ["easy"] } scopeguard = "1.1.0" -async-trait = "0.1" const_format = "0.2.21" tracing = "0.1.27" tracing-futures = "0.2" @@ -44,14 +45,15 @@ once_cell = "1.8.0" crossbeam-utils = "0.8.5" fail = "0.5.0" -rust-s3 = { version = "0.28", default-features = false, features = ["no-verify-ssl", "tokio-rustls-tls"] } +rusoto_core = "0.47" +rusoto_s3 = "0.47" +async-trait = "0.1" async-compression = {version = "0.3", features = ["zstd", "tokio"]} postgres_ffi = { path = "../postgres_ffi" } zenith_metrics = { path = "../zenith_metrics" } zenith_utils = { path = "../zenith_utils" } -workspace_hack = { path = "../workspace_hack" } -plotly = "0.7.0" +workspace_hack = { version = "0.1", path = "../workspace_hack" } [dev-dependencies] hex-literal = "0.3" diff --git a/pageserver/README.md b/pageserver/README.md index 69080a16cc..1fd627785c 100644 --- a/pageserver/README.md +++ b/pageserver/README.md @@ -13,7 +13,7 @@ keeps track of WAL records which are not synced to S3 yet. The Page Server consists of multiple threads that operate on a shared repository of page versions: - +``` | WAL V +--------------+ @@ -46,7 +46,7 @@ Legend: ---> Data flow <--- - +``` Page Service ------------ diff --git a/pageserver/src/basebackup.rs b/pageserver/src/basebackup.rs index 0c4ca83272..3caf27b9b3 100644 --- a/pageserver/src/basebackup.rs +++ b/pageserver/src/basebackup.rs @@ -10,7 +10,7 @@ //! This module is responsible for creation of such tarball //! from data stored in object storage. //! -use anyhow::{Context, Result}; +use anyhow::{ensure, Context, Result}; use bytes::{BufMut, BytesMut}; use log::*; use std::fmt::Write as FmtWrite; @@ -20,7 +20,7 @@ use std::sync::Arc; use std::time::SystemTime; use tar::{Builder, EntryType, Header}; -use crate::relish::*; +use crate::reltag::SlruKind; use crate::repository::Timeline; use crate::DatadirTimelineImpl; use postgres_ffi::xlog_utils::*; @@ -65,6 +65,7 @@ impl<'a> Basebackup<'a> { // prev_lsn to Lsn(0) if we cannot provide the correct value. let (backup_prev, backup_lsn) = if let Some(req_lsn) = req_lsn { // Backup was requested at a particular LSN. Wait for it to arrive. + info!("waiting for {}", req_lsn); timeline.tline.wait_lsn(req_lsn)?; // If the requested point is the end of the timeline, we can @@ -153,7 +154,7 @@ impl<'a> Basebackup<'a> { let img = self .timeline .get_slru_page_at_lsn(slru, segno, blknum, self.lsn)?; - assert!(img.len() == pg_constants::BLCKSZ as usize); + ensure!(img.len() == pg_constants::BLCKSZ as usize); slru_buf.extend_from_slice(&img); } @@ -180,7 +181,7 @@ impl<'a> Basebackup<'a> { ) -> anyhow::Result<()> { let relmap_img = if has_relmap_file { let img = self.timeline.get_relmap_file(spcnode, dbnode, self.lsn)?; - assert!(img.len() == 512); + ensure!(img.len() == 512); Some(img) } else { None @@ -220,7 +221,8 @@ impl<'a> Basebackup<'a> { { return Ok(()); } - assert!(spcnode == pg_constants::DEFAULTTABLESPACE_OID); + // User defined tablespaces are not supported + ensure!(spcnode == pg_constants::DEFAULTTABLESPACE_OID); // Append dir path for each database let path = format!("base/{}", dbnode); @@ -314,7 +316,7 @@ impl<'a> Basebackup<'a> { let wal_file_path = format!("pg_wal/{}", wal_file_name); let header = new_tar_header(&wal_file_path, pg_constants::WAL_SEGMENT_SIZE as u64)?; let wal_seg = generate_wal_segment(segno, pg_control.system_identifier); - assert!(wal_seg.len() == pg_constants::WAL_SEGMENT_SIZE); + ensure!(wal_seg.len() == pg_constants::WAL_SEGMENT_SIZE); self.ar.append(&header, &wal_seg[..])?; Ok(()) } diff --git a/pageserver/src/bin/dump_layerfile.rs b/pageserver/src/bin/dump_layerfile.rs index b954ad5a15..7cf39566ac 100644 --- a/pageserver/src/bin/dump_layerfile.rs +++ b/pageserver/src/bin/dump_layerfile.rs @@ -4,6 +4,7 @@ use anyhow::Result; use clap::{App, Arg}; use pageserver::layered_repository::dump_layerfile_from_path; +use pageserver::page_cache; use pageserver::virtual_file; use std::path::PathBuf; use zenith_utils::GIT_VERSION; @@ -24,8 +25,9 @@ fn main() -> Result<()> { // Basic initialization of things that don't change after startup virtual_file::init(10); + page_cache::init(100); - dump_layerfile_from_path(&path)?; + dump_layerfile_from_path(&path, true)?; Ok(()) } diff --git a/pageserver/src/bin/pageserver.rs b/pageserver/src/bin/pageserver.rs index ed5d041e08..0523726e4a 100644 --- a/pageserver/src/bin/pageserver.rs +++ b/pageserver/src/bin/pageserver.rs @@ -18,16 +18,18 @@ use daemonize::Daemonize; use pageserver::{ config::{defaults::*, PageServerConf}, - http, page_cache, page_service, remote_storage, tenant_mgr, thread_mgr, + http, page_cache, page_service, + remote_storage::{self, SyncStartupData}, + repository::{Repository, TimelineSyncStatusUpdate}, + tenant_mgr, thread_mgr, thread_mgr::ThreadKind, timelines, virtual_file, LOG_FILE_NAME, }; use zenith_utils::http::endpoint; -use zenith_utils::postgres_backend; use zenith_utils::shutdown::exit_now; use zenith_utils::signals::{self, Signal}; -fn main() -> Result<()> { +fn main() -> anyhow::Result<()> { zenith_metrics::set_common_metrics_prefix("pageserver"); let arg_matches = App::new("Zenith page server") .about("Materializes WAL stream to pages and serves them to the postgres") @@ -113,7 +115,7 @@ fn main() -> Result<()> { // We're initializing the repo, so there's no config file yet DEFAULT_CONFIG_FILE .parse::() - .expect("could not parse built-in config file") + .context("could not parse built-in config file")? } else { // Supplement the CLI arguments with the config file let cfg_file_contents = std::fs::read_to_string(&cfg_file_path) @@ -160,8 +162,7 @@ fn main() -> Result<()> { // Basic initialization of things that don't change after startup virtual_file::init(conf.max_file_descriptors); - - page_cache::init(conf); + page_cache::init(conf.page_cache_size); // Create repo and exit if init was requested if init { @@ -210,7 +211,9 @@ fn start_pageserver(conf: &'static PageServerConf, daemonize: bool) -> Result<() // There shouldn't be any logging to stdin/stdout. Redirect it to the main log so // that we will see any accidental manual fprintf's or backtraces. - let stdout = log_file.try_clone().unwrap(); + let stdout = log_file + .try_clone() + .with_context(|| format!("Failed to clone log file '{:?}'", log_file))?; let stderr = log_file; let daemonize = Daemonize::new() @@ -230,11 +233,47 @@ fn start_pageserver(conf: &'static PageServerConf, daemonize: bool) -> Result<() } let signals = signals::install_shutdown_handlers()?; - let sync_startup = remote_storage::start_local_timeline_sync(conf) + + // Initialize repositories with locally available timelines. + // Timelines that are only partially available locally (remote storage has more data than this pageserver) + // are scheduled for download and added to the repository once download is completed. + let SyncStartupData { + remote_index, + local_timeline_init_statuses, + } = remote_storage::start_local_timeline_sync(conf) .context("Failed to set up local files sync with external storage")?; - // Initialize tenant manager. - tenant_mgr::set_timeline_states(conf, sync_startup.initial_timeline_states); + for (tenant_id, local_timeline_init_statuses) in local_timeline_init_statuses { + // initialize local tenant + let repo = tenant_mgr::load_local_repo(conf, tenant_id, &remote_index); + for (timeline_id, init_status) in local_timeline_init_statuses { + match init_status { + remote_storage::LocalTimelineInitStatus::LocallyComplete => { + debug!("timeline {} for tenant {} is locally complete, registering it in repository", tenant_id, timeline_id); + // Lets fail here loudly to be on the safe side. + // XXX: It may be a better api to actually distinguish between repository startup + // and processing of newly downloaded timelines. + repo.apply_timeline_remote_sync_status_update( + timeline_id, + TimelineSyncStatusUpdate::Downloaded, + ) + .with_context(|| { + format!( + "Failed to bootstrap timeline {} for tenant {}", + timeline_id, tenant_id + ) + })? + } + remote_storage::LocalTimelineInitStatus::NeedsSync => { + debug!( + "timeline {} for tenant {} needs sync, \ + so skipped for adding into repository until sync is finished", + tenant_id, timeline_id + ); + } + } + } + } // initialize authentication for incoming connections let auth = match &conf.auth_type { @@ -255,8 +294,9 @@ fn start_pageserver(conf: &'static PageServerConf, daemonize: bool) -> Result<() None, None, "http_endpoint_thread", + false, move || { - let router = http::make_router(conf, auth_cloned); + let router = http::make_router(conf, auth_cloned, remote_index); endpoint::serve_thread_main(router, http_listener, thread_mgr::shutdown_watcher()) }, )?; @@ -268,6 +308,7 @@ fn start_pageserver(conf: &'static PageServerConf, daemonize: bool) -> Result<() None, None, "libpq endpoint thread", + false, move || page_service::thread_main(conf, auth, pageserver_listener, conf.auth_type), )?; @@ -285,38 +326,8 @@ fn start_pageserver(conf: &'static PageServerConf, daemonize: bool) -> Result<() "Got {}. Terminating gracefully in fast shutdown mode", signal.name() ); - shutdown_pageserver(); + pageserver::shutdown_pageserver(); unreachable!() } }) } - -fn shutdown_pageserver() { - // Shut down the libpq endpoint thread. This prevents new connections from - // being accepted. - thread_mgr::shutdown_threads(Some(ThreadKind::LibpqEndpointListener), None, None); - - // Shut down any page service threads. - postgres_backend::set_pgbackend_shutdown_requested(); - thread_mgr::shutdown_threads(Some(ThreadKind::PageRequestHandler), None, None); - - // Shut down all the tenants. This flushes everything to disk and kills - // the checkpoint and GC threads. - tenant_mgr::shutdown_all_tenants(); - - // Stop syncing with remote storage. - // - // FIXME: Does this wait for the sync thread to finish syncing what's queued up? - // Should it? - thread_mgr::shutdown_threads(Some(ThreadKind::StorageSync), None, None); - - // Shut down the HTTP endpoint last, so that you can still check the server's - // status while it's shutting down. - thread_mgr::shutdown_threads(Some(ThreadKind::HttpEndpointListener), None, None); - - // There should be nothing left, but let's be sure - thread_mgr::shutdown_threads(None, None, None); - - info!("Shut down successfully completed"); - std::process::exit(0); -} diff --git a/pageserver/src/config.rs b/pageserver/src/config.rs index 0bfc451a24..0d5cac8b4f 100644 --- a/pageserver/src/config.rs +++ b/pageserver/src/config.rs @@ -30,8 +30,13 @@ pub mod defaults { // FIXME: This current value is very low. I would imagine something like 1 GB or 10 GB // would be more appropriate. But a low value forces the code to be exercised more, // which is good for now to trigger bugs. + // This parameter actually determines L0 layer file size. pub const DEFAULT_CHECKPOINT_DISTANCE: u64 = 256 * 1024 * 1024; + // Target file size, when creating image and delta layers. + // This parameter determines L1 layer file size. + pub const DEFAULT_COMPACTION_TARGET_SIZE: u64 = 128 * 1024 * 1024; + pub const DEFAULT_COMPACTION_PERIOD: &str = "1 s"; pub const DEFAULT_GC_HORIZON: u64 = 64 * 1024 * 1024; @@ -41,7 +46,7 @@ pub mod defaults { pub const DEFAULT_WAL_REDO_TIMEOUT: &str = "60 s"; pub const DEFAULT_SUPERUSER: &str = "zenith_admin"; - pub const DEFAULT_REMOTE_STORAGE_MAX_CONCURRENT_SYNC: usize = 100; + pub const DEFAULT_REMOTE_STORAGE_MAX_CONCURRENT_SYNC: usize = 10; pub const DEFAULT_REMOTE_STORAGE_MAX_SYNC_ERRORS: u32 = 10; pub const DEFAULT_PAGE_CACHE_SIZE: usize = 8192; @@ -58,6 +63,7 @@ pub mod defaults { #listen_http_addr = '{DEFAULT_HTTP_LISTEN_ADDR}' #checkpoint_distance = {DEFAULT_CHECKPOINT_DISTANCE} # in bytes +#compaction_target_size = {DEFAULT_COMPACTION_TARGET_SIZE} # in bytes #compaction_period = '{DEFAULT_COMPACTION_PERIOD}' #gc_period = '{DEFAULT_GC_PERIOD}' @@ -91,8 +97,13 @@ pub struct PageServerConf { // Flush out an inmemory layer, if it's holding WAL older than this // This puts a backstop on how much WAL needs to be re-digested if the // page server crashes. + // This parameter actually determines L0 layer file size. pub checkpoint_distance: u64, + // Target file size, when creating image and delta layers. + // This parameter determines L1 layer file size. + pub compaction_target_size: u64, + // How often to check if there's compaction work to be done. pub compaction_period: Duration, @@ -149,6 +160,7 @@ struct PageServerConfigBuilder { checkpoint_distance: BuilderValue, + compaction_target_size: BuilderValue, compaction_period: BuilderValue, gc_horizon: BuilderValue, @@ -183,6 +195,7 @@ impl Default for PageServerConfigBuilder { listen_pg_addr: Set(DEFAULT_PG_LISTEN_ADDR.to_string()), listen_http_addr: Set(DEFAULT_HTTP_LISTEN_ADDR.to_string()), checkpoint_distance: Set(DEFAULT_CHECKPOINT_DISTANCE), + compaction_target_size: Set(DEFAULT_COMPACTION_TARGET_SIZE), compaction_period: Set(humantime::parse_duration(DEFAULT_COMPACTION_PERIOD) .expect("cannot parse default compaction period")), gc_horizon: Set(DEFAULT_GC_HORIZON), @@ -220,6 +233,10 @@ impl PageServerConfigBuilder { self.checkpoint_distance = BuilderValue::Set(checkpoint_distance) } + pub fn compaction_target_size(&mut self, compaction_target_size: u64) { + self.compaction_target_size = BuilderValue::Set(compaction_target_size) + } + pub fn compaction_period(&mut self, compaction_period: Duration) { self.compaction_period = BuilderValue::Set(compaction_period) } @@ -290,6 +307,9 @@ impl PageServerConfigBuilder { checkpoint_distance: self .checkpoint_distance .ok_or(anyhow::anyhow!("missing checkpoint_distance"))?, + compaction_target_size: self + .compaction_target_size + .ok_or(anyhow::anyhow!("missing compaction_target_size"))?, compaction_period: self .compaction_period .ok_or(anyhow::anyhow!("missing compaction_period"))?, @@ -341,10 +361,10 @@ pub struct RemoteStorageConfig { #[derive(Debug, Clone, PartialEq, Eq)] pub enum RemoteStorageKind { /// Storage based on local file system. - /// Specify a root folder to place all stored relish data into. + /// Specify a root folder to place all stored files into. LocalFs(PathBuf), - /// AWS S3 based storage, storing all relishes into the root - /// of the S3 bucket from the config. + /// AWS S3 based storage, storing all files in the S3 bucket + /// specified by the config AwsS3(S3Config), } @@ -429,6 +449,9 @@ impl PageServerConf { "listen_pg_addr" => builder.listen_pg_addr(parse_toml_string(key, item)?), "listen_http_addr" => builder.listen_http_addr(parse_toml_string(key, item)?), "checkpoint_distance" => builder.checkpoint_distance(parse_toml_u64(key, item)?), + "compaction_target_size" => { + builder.compaction_target_size(parse_toml_u64(key, item)?) + } "compaction_period" => builder.compaction_period(parse_toml_duration(key, item)?), "gc_horizon" => builder.gc_horizon(parse_toml_u64(key, item)?), "gc_period" => builder.gc_period(parse_toml_duration(key, item)?), @@ -565,6 +588,7 @@ impl PageServerConf { PageServerConf { id: ZNodeId(0), checkpoint_distance: defaults::DEFAULT_CHECKPOINT_DISTANCE, + compaction_target_size: 4 * 1024 * 1024, compaction_period: Duration::from_secs(10), gc_horizon: defaults::DEFAULT_GC_HORIZON, gc_period: Duration::from_secs(10), @@ -636,6 +660,7 @@ listen_http_addr = '127.0.0.1:9898' checkpoint_distance = 111 # in bytes +compaction_target_size = 111 # in bytes compaction_period = '111 s' gc_period = '222 s' @@ -673,6 +698,7 @@ id = 10 listen_pg_addr: defaults::DEFAULT_PG_LISTEN_ADDR.to_string(), listen_http_addr: defaults::DEFAULT_HTTP_LISTEN_ADDR.to_string(), checkpoint_distance: defaults::DEFAULT_CHECKPOINT_DISTANCE, + compaction_target_size: defaults::DEFAULT_COMPACTION_TARGET_SIZE, compaction_period: humantime::parse_duration(defaults::DEFAULT_COMPACTION_PERIOD)?, gc_horizon: defaults::DEFAULT_GC_HORIZON, gc_period: humantime::parse_duration(defaults::DEFAULT_GC_PERIOD)?, @@ -717,6 +743,7 @@ id = 10 listen_pg_addr: "127.0.0.1:64000".to_string(), listen_http_addr: "127.0.0.1:9898".to_string(), checkpoint_distance: 111, + compaction_target_size: 111, compaction_period: Duration::from_secs(111), gc_horizon: 222, gc_period: Duration::from_secs(222), diff --git a/pageserver/src/http/models.rs b/pageserver/src/http/models.rs index 9844e7ea82..d1dfb911ba 100644 --- a/pageserver/src/http/models.rs +++ b/pageserver/src/http/models.rs @@ -1,122 +1,36 @@ -use crate::timelines::TimelineInfo; -use anyhow::{anyhow, bail, Context}; use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; use zenith_utils::{ lsn::Lsn, - zid::{HexZTenantId, HexZTimelineId, ZNodeId, ZTenantId, ZTimelineId}, + zid::{ZNodeId, ZTenantId, ZTimelineId}, }; +#[serde_as] #[derive(Serialize, Deserialize)] pub struct TimelineCreateRequest { - pub new_timeline_id: Option, - pub ancestor_timeline_id: Option, + #[serde(default)] + #[serde_as(as = "Option")] + pub new_timeline_id: Option, + #[serde(default)] + #[serde_as(as = "Option")] + pub ancestor_timeline_id: Option, + #[serde(default)] + #[serde_as(as = "Option")] pub ancestor_start_lsn: Option, } +#[serde_as] #[derive(Serialize, Deserialize)] pub struct TenantCreateRequest { - pub new_tenant_id: Option, + #[serde(default)] + #[serde_as(as = "Option")] + pub new_tenant_id: Option, } +#[serde_as] #[derive(Serialize, Deserialize)] -pub struct TimelineInfoResponse { - pub kind: String, - #[serde(with = "hex")] - timeline_id: ZTimelineId, - #[serde(with = "hex")] - tenant_id: ZTenantId, - disk_consistent_lsn: String, - last_record_lsn: Option, - prev_record_lsn: Option, - ancestor_timeline_id: Option, - ancestor_lsn: Option, - current_logical_size: Option, - current_logical_size_non_incremental: Option, -} - -impl From for TimelineInfoResponse { - fn from(other: TimelineInfo) -> Self { - match other { - TimelineInfo::Local { - timeline_id, - tenant_id, - last_record_lsn, - prev_record_lsn, - ancestor_timeline_id, - ancestor_lsn, - disk_consistent_lsn, - current_logical_size, - current_logical_size_non_incremental, - } => TimelineInfoResponse { - kind: "Local".to_owned(), - timeline_id, - tenant_id, - disk_consistent_lsn: disk_consistent_lsn.to_string(), - last_record_lsn: Some(last_record_lsn.to_string()), - prev_record_lsn: Some(prev_record_lsn.to_string()), - ancestor_timeline_id: ancestor_timeline_id.map(HexZTimelineId::from), - ancestor_lsn: ancestor_lsn.map(|lsn| lsn.to_string()), - current_logical_size: Some(current_logical_size), - current_logical_size_non_incremental, - }, - TimelineInfo::Remote { - timeline_id, - tenant_id, - disk_consistent_lsn, - } => TimelineInfoResponse { - kind: "Remote".to_owned(), - timeline_id, - tenant_id, - disk_consistent_lsn: disk_consistent_lsn.to_string(), - last_record_lsn: None, - prev_record_lsn: None, - ancestor_timeline_id: None, - ancestor_lsn: None, - current_logical_size: None, - current_logical_size_non_incremental: None, - }, - } - } -} - -impl TryFrom for TimelineInfo { - type Error = anyhow::Error; - - fn try_from(other: TimelineInfoResponse) -> anyhow::Result { - let parse_lsn_hex_string = |lsn_string: String| { - lsn_string - .parse::() - .with_context(|| format!("Failed to parse Lsn as hex string from '{}'", lsn_string)) - }; - - let disk_consistent_lsn = parse_lsn_hex_string(other.disk_consistent_lsn)?; - Ok(match other.kind.as_str() { - "Local" => TimelineInfo::Local { - timeline_id: other.timeline_id, - tenant_id: other.tenant_id, - last_record_lsn: other - .last_record_lsn - .ok_or(anyhow!("Local timeline should have last_record_lsn")) - .and_then(parse_lsn_hex_string)?, - prev_record_lsn: other - .prev_record_lsn - .ok_or(anyhow!("Local timeline should have prev_record_lsn")) - .and_then(parse_lsn_hex_string)?, - ancestor_timeline_id: other.ancestor_timeline_id.map(ZTimelineId::from), - ancestor_lsn: other.ancestor_lsn.map(parse_lsn_hex_string).transpose()?, - disk_consistent_lsn, - current_logical_size: other.current_logical_size.ok_or(anyhow!("No "))?, - current_logical_size_non_incremental: other.current_logical_size_non_incremental, - }, - "Remote" => TimelineInfo::Remote { - timeline_id: other.timeline_id, - tenant_id: other.tenant_id, - disk_consistent_lsn, - }, - unknown => bail!("Unknown timeline kind: {}", unknown), - }) - } -} +#[serde(transparent)] +pub struct TenantCreateResponse(#[serde_as(as = "DisplayFromStr")] pub ZTenantId); #[derive(Serialize)] pub struct StatusResponse { diff --git a/pageserver/src/http/openapi_spec.yml b/pageserver/src/http/openapi_spec.yml index d322b051a6..b2760efe85 100644 --- a/pageserver/src/http/openapi_spec.yml +++ b/pageserver/src/http/openapi_spec.yml @@ -18,7 +18,7 @@ paths: schema: type: object required: - - id + - id properties: id: type: integer @@ -122,6 +122,110 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + + + /v1/tenant/{tenant_id}/timeline/{timeline_id}/attach: + parameters: + - name: tenant_id + in: path + required: true + schema: + type: string + format: hex + - name: timeline_id + in: path + required: true + schema: + type: string + format: hex + post: + description: Attach remote timeline + responses: + "200": + description: Timeline attaching scheduled + "400": + description: Error when no tenant id found in path or no timeline id + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Unauthorized Error + content: + application/json: + schema: + $ref: "#/components/schemas/UnauthorizedError" + "403": + description: Forbidden Error + content: + application/json: + schema: + $ref: "#/components/schemas/ForbiddenError" + "404": + description: Timeline not found + content: + application/json: + schema: + $ref: "#/components/schemas/NotFoundError" + "409": + description: Timeline download is already in progress + content: + application/json: + schema: + $ref: "#/components/schemas/ConflictError" + "500": + description: Generic operation error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + + /v1/tenant/{tenant_id}/timeline/{timeline_id}/detach: + parameters: + - name: tenant_id + in: path + required: true + schema: + type: string + format: hex + - name: timeline_id + in: path + required: true + schema: + type: string + format: hex + post: + description: Detach local timeline + responses: + "200": + description: Timeline detached + "400": + description: Error when no tenant id found in path or no timeline id + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Unauthorized Error + content: + application/json: + schema: + $ref: "#/components/schemas/UnauthorizedError" + "403": + description: Forbidden Error + content: + application/json: + schema: + $ref: "#/components/schemas/ForbiddenError" + "500": + description: Generic operation error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /v1/tenant/{tenant_id}/timeline/: parameters: - name: tenant_id @@ -148,6 +252,7 @@ paths: format: hex ancestor_start_lsn: type: string + format: hex responses: "201": description: TimelineInfo @@ -178,7 +283,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/AlreadyExistsError" + $ref: "#/components/schemas/ConflictError" "500": description: Generic operation error content: @@ -259,7 +364,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/AlreadyExistsError" + $ref: "#/components/schemas/ConflictError" "500": description: Generic operation error content: @@ -289,7 +394,6 @@ components: required: - timeline_id - tenant_id - - disk_consistent_lsn properties: timeline_id: type: string @@ -297,17 +401,44 @@ components: tenant_id: type: string format: hex + local: + $ref: "#/components/schemas/LocalTimelineInfo" + remote: + $ref: "#/components/schemas/RemoteTimelineInfo" + RemoteTimelineInfo: + type: object + required: + - awaits_download + properties: + awaits_download: + type: boolean + remote_consistent_lsn: + type: string + format: hex + LocalTimelineInfo: + type: object + required: + - last_record_lsn + - disk_consistent_lsn + - timeline_state + properties: last_record_lsn: type: string - prev_record_lsn: + format: hex + disk_consistent_lsn: + type: string + format: hex + timeline_state: type: string ancestor_timeline_id: type: string format: hex ancestor_lsn: type: string - disk_consistent_lsn: + format: hex + prev_record_lsn: type: string + format: hex current_logical_size: type: integer current_logical_size_non_incremental: @@ -327,14 +458,21 @@ components: properties: msg: type: string - AlreadyExistsError: + ForbiddenError: type: object required: - msg properties: msg: type: string - ForbiddenError: + NotFoundError: + type: object + required: + - msg + properties: + msg: + type: string + ConflictError: type: object required: - msg diff --git a/pageserver/src/http/routes.rs b/pageserver/src/http/routes.rs index a0964d162e..a0d6e922a1 100644 --- a/pageserver/src/http/routes.rs +++ b/pageserver/src/http/routes.rs @@ -16,25 +16,29 @@ use zenith_utils::http::{ request::parse_request_param, }; use zenith_utils::http::{RequestExt, RouterBuilder}; -use zenith_utils::zid::{HexZTenantId, ZTimelineId}; +use zenith_utils::zid::{ZTenantTimelineId, ZTimelineId}; use super::models::{ - StatusResponse, TenantCreateRequest, TimelineCreateRequest, TimelineInfoResponse, + StatusResponse, TenantCreateRequest, TenantCreateResponse, TimelineCreateRequest, }; +use crate::remote_storage::{schedule_timeline_download, RemoteIndex}; use crate::repository::Repository; -use crate::repository::RepositoryTimeline; -use crate::timelines::TimelineInfo; +use crate::timelines::{LocalTimelineInfo, RemoteTimelineInfo, TimelineInfo}; use crate::{config::PageServerConf, tenant_mgr, timelines, ZTenantId}; -#[derive(Debug)] struct State { conf: &'static PageServerConf, auth: Option>, + remote_index: RemoteIndex, allowlist_routes: Vec, } impl State { - fn new(conf: &'static PageServerConf, auth: Option>) -> Self { + fn new( + conf: &'static PageServerConf, + auth: Option>, + remote_index: RemoteIndex, + ) -> Self { let allowlist_routes = ["/v1/status", "/v1/doc", "/swagger.yml"] .iter() .map(|v| v.parse().unwrap()) @@ -43,6 +47,7 @@ impl State { conf, auth, allowlist_routes, + remote_index, } } } @@ -63,10 +68,7 @@ fn get_config(request: &Request) -> &'static PageServerConf { // healthcheck handler async fn status_handler(request: Request) -> Result, ApiError> { let config = get_config(&request); - Ok(json_response( - StatusCode::OK, - StatusResponse { id: config.id }, - )?) + json_response(StatusCode::OK, StatusResponse { id: config.id }) } async fn timeline_create_handler(mut request: Request) -> Result, ApiError> { @@ -89,7 +91,7 @@ async fn timeline_create_handler(mut request: Request) -> Result json_response(StatusCode::CREATED, TimelineInfoResponse::from(info))?, + Some(info) => json_response(StatusCode::CREATED, info)?, None => json_response(StatusCode::CONFLICT, ())?, }) } @@ -98,16 +100,35 @@ async fn timeline_list_handler(request: Request) -> Result, let tenant_id: ZTenantId = parse_request_param(&request, "tenant_id")?; check_permission(&request, Some(tenant_id))?; let include_non_incremental_logical_size = get_include_non_incremental_logical_size(&request); - let response_data: Vec = tokio::task::spawn_blocking(move || { + let local_timeline_infos = tokio::task::spawn_blocking(move || { let _enter = info_span!("timeline_list", tenant = %tenant_id).entered(); - crate::timelines::get_timelines(tenant_id, include_non_incremental_logical_size) + crate::timelines::get_local_timelines(tenant_id, include_non_incremental_logical_size) }) .await - .map_err(ApiError::from_err)?? - .into_iter() - .map(TimelineInfoResponse::from) - .collect(); - Ok(json_response(StatusCode::OK, response_data)?) + .map_err(ApiError::from_err)??; + + let mut response_data = Vec::with_capacity(local_timeline_infos.len()); + for (timeline_id, local_timeline_info) in local_timeline_infos { + response_data.push(TimelineInfo { + tenant_id, + timeline_id, + local: Some(local_timeline_info), + remote: get_state(&request) + .remote_index + .read() + .await + .timeline_entry(&ZTenantTimelineId { + tenant_id, + timeline_id, + }) + .map(|remote_entry| RemoteTimelineInfo { + remote_consistent_lsn: remote_entry.disk_consistent_lsn(), + awaits_download: remote_entry.get_awaits_download(), + }), + }) + } + + json_response(StatusCode::OK, response_data) } // Gate non incremental logical size calculation behind a flag @@ -130,20 +151,60 @@ async fn timeline_detail_handler(request: Request) -> Result((local_timeline, entered.exit())) }) .await - .map_err(ApiError::from_err)? - .map(TimelineInfoResponse::from)?; + .map_err(ApiError::from_err)??; - Ok(json_response(StatusCode::OK, response_data)?) + let remote_timeline_info = { + let remote_index_read = get_state(&request).remote_index.read().await; + remote_index_read + .timeline_entry(&ZTenantTimelineId { + tenant_id, + timeline_id, + }) + .map(|remote_entry| RemoteTimelineInfo { + remote_consistent_lsn: remote_entry.disk_consistent_lsn(), + awaits_download: remote_entry.get_awaits_download(), + }) + }; + + let _enter = span.entered(); + + if local_timeline_info.is_none() && remote_timeline_info.is_none() { + return Err(ApiError::NotFound( + "Timeline is not found neither locally nor remotely".to_string(), + )); + } + + let timeline_info = TimelineInfo { + tenant_id, + timeline_id, + local: local_timeline_info, + remote: remote_timeline_info, + }; + + json_response(StatusCode::OK, timeline_info) } async fn timeline_attach_handler(request: Request) -> Result, ApiError> { @@ -151,32 +212,39 @@ async fn timeline_attach_handler(request: Request) -> Result { - anyhow::bail!("Timeline with id {} is already local", timeline_id) - } - RepositoryTimeline::Remote { - id: _, - disk_consistent_lsn: _, - } => { - // FIXME (rodionov) get timeline already schedules timeline for download, and duplicate tasks can cause errors - // first should be fixed in https://github.com/zenithdb/zenith/issues/997 - // TODO (rodionov) change timeline state to awaits download (incapsulate it somewhere in the repo) - // TODO (rodionov) can we safely request replication on the timeline before sync is completed? (can be implemented on top of the #997) - Ok(()) - } - } + let span = tokio::task::spawn_blocking(move || { + let entered = span.entered(); + if tenant_mgr::get_timeline_for_tenant_load(tenant_id, timeline_id).is_ok() { + // TODO: maybe answer with 309 Not Modified here? + anyhow::bail!("Timeline is already present locally") + }; + Ok(entered.exit()) }) .await .map_err(ApiError::from_err)??; - Ok(json_response(StatusCode::ACCEPTED, ())?) + let mut remote_index_write = get_state(&request).remote_index.write().await; + + let _enter = span.entered(); // entered guard cannot live across awaits (non Send) + let index_entry = remote_index_write + .timeline_entry_mut(&ZTenantTimelineId { + tenant_id, + timeline_id, + }) + .ok_or_else(|| ApiError::NotFound("Unknown remote timeline".to_string()))?; + + if index_entry.get_awaits_download() { + return Err(ApiError::Conflict( + "Timeline download is already in progress".to_string(), + )); + } + + index_entry.set_awaits_download(true); + schedule_timeline_download(tenant_id, timeline_id); + + json_response(StatusCode::ACCEPTED, ()) } async fn timeline_detach_handler(request: Request) -> Result, ApiError> { @@ -195,7 +263,7 @@ async fn timeline_detach_handler(request: Request) -> Result) -> Result, ApiError> { @@ -209,7 +277,7 @@ async fn tenant_list_handler(request: Request) -> Result, A .await .map_err(ApiError::from_err)??; - Ok(json_response(StatusCode::OK, response_data)?) + json_response(StatusCode::OK, response_data) } async fn tenant_create_handler(mut request: Request) -> Result, ApiError> { @@ -217,19 +285,23 @@ async fn tenant_create_handler(mut request: Request) -> Result json_response(StatusCode::CREATED, HexZTenantId::from(id))?, + Some(id) => json_response(StatusCode::CREATED, TenantCreateResponse(id))?, None => json_response(StatusCode::CONFLICT, ())?, }) } @@ -244,6 +316,7 @@ async fn handler_404(_: Request) -> Result, ApiError> { pub fn make_router( conf: &'static PageServerConf, auth: Option>, + remote_index: RemoteIndex, ) -> RouterBuilder { let spec = include_bytes!("openapi_spec.yml"); let mut router = attach_openapi_ui(endpoint::make_router(), spec, "/swagger.yml", "/v1/doc"); @@ -259,7 +332,7 @@ pub fn make_router( } router - .data(Arc::new(State::new(conf, auth))) + .data(Arc::new(State::new(conf, auth, remote_index))) .get("/v1/status", status_handler) .get("/v1/tenant", tenant_list_handler) .post("/v1/tenant", tenant_create_handler) diff --git a/pageserver/src/import_datadir.rs b/pageserver/src/import_datadir.rs index f31fea02f8..232892973e 100644 --- a/pageserver/src/import_datadir.rs +++ b/pageserver/src/import_datadir.rs @@ -12,7 +12,7 @@ use bytes::Bytes; use tracing::*; use crate::pgdatadir_mapping::*; -use crate::relish::*; +use crate::reltag::{RelTag, SlruKind}; use crate::repository::Repository; use crate::walingest::WalIngest; use postgres_ffi::relfile_utils::*; @@ -35,8 +35,8 @@ pub fn import_timeline_from_postgres_datadir( ) -> Result<()> { let mut pg_control: Option = None; - let mut writer = tline.begin_record(lsn); - writer.init_empty()?; + let mut modification = tline.begin_modification(lsn); + modification.init_empty()?; // Scan 'global' let mut relfiles: Vec = Vec::new(); @@ -46,11 +46,11 @@ pub fn import_timeline_from_postgres_datadir( None => continue, Some("pg_control") => { - pg_control = Some(import_control_file(&mut writer, &direntry.path())?); + pg_control = Some(import_control_file(&mut modification, &direntry.path())?); } Some("pg_filenode.map") => { import_relmap_file( - &mut writer, + &mut modification, pg_constants::GLOBALTABLESPACE_OID, 0, &direntry.path(), @@ -62,7 +62,12 @@ pub fn import_timeline_from_postgres_datadir( } } for relfile in relfiles { - import_relfile(&mut writer, &relfile, pg_constants::GLOBALTABLESPACE_OID, 0)?; + import_relfile( + &mut modification, + &relfile, + pg_constants::GLOBALTABLESPACE_OID, + 0, + )?; } // Scan 'base'. It contains database dirs, the database OID is the filename. @@ -71,11 +76,11 @@ pub fn import_timeline_from_postgres_datadir( let direntry = direntry?; //skip all temporary files - if direntry.file_name().to_str().unwrap() == "pgsql_tmp" { + if direntry.file_name().to_string_lossy() == "pgsql_tmp" { continue; } - let dboid = direntry.file_name().to_str().unwrap().parse::()?; + let dboid = direntry.file_name().to_string_lossy().parse::()?; let mut relfiles: Vec = Vec::new(); for direntry in fs::read_dir(direntry.path())? { @@ -84,10 +89,10 @@ pub fn import_timeline_from_postgres_datadir( None => continue, Some("PG_VERSION") => { - //writer.put_dbdir_creation(pg_constants::DEFAULTTABLESPACE_OID, dboid)?; + //modification.put_dbdir_creation(pg_constants::DEFAULTTABLESPACE_OID, dboid)?; } Some("pg_filenode.map") => import_relmap_file( - &mut writer, + &mut modification, pg_constants::DEFAULTTABLESPACE_OID, dboid, &direntry.path(), @@ -99,7 +104,7 @@ pub fn import_timeline_from_postgres_datadir( } for relfile in relfiles { import_relfile( - &mut writer, + &mut modification, &relfile, pg_constants::DEFAULTTABLESPACE_OID, dboid, @@ -108,25 +113,25 @@ pub fn import_timeline_from_postgres_datadir( } for entry in fs::read_dir(path.join("pg_xact"))? { let entry = entry?; - import_slru_file(&mut writer, SlruKind::Clog, &entry.path())?; + import_slru_file(&mut modification, SlruKind::Clog, &entry.path())?; } for entry in fs::read_dir(path.join("pg_multixact").join("members"))? { let entry = entry?; - import_slru_file(&mut writer, SlruKind::MultiXactMembers, &entry.path())?; + import_slru_file(&mut modification, SlruKind::MultiXactMembers, &entry.path())?; } for entry in fs::read_dir(path.join("pg_multixact").join("offsets"))? { let entry = entry?; - import_slru_file(&mut writer, SlruKind::MultiXactOffsets, &entry.path())?; + import_slru_file(&mut modification, SlruKind::MultiXactOffsets, &entry.path())?; } for entry in fs::read_dir(path.join("pg_twophase"))? { let entry = entry?; - let xid = u32::from_str_radix(entry.path().to_str().unwrap(), 16)?; - import_twophase_file(&mut writer, xid, &entry.path())?; + let xid = u32::from_str_radix(&entry.path().to_string_lossy(), 16)?; + import_twophase_file(&mut modification, xid, &entry.path())?; } // TODO: Scan pg_tblspc // We're done importing all the data files. - writer.finish()?; + modification.commit()?; // We expect the Postgres server to be shut down cleanly. let pg_control = pg_control.context("pg_control file not found")?; @@ -154,20 +159,19 @@ pub fn import_timeline_from_postgres_datadir( // subroutine of import_timeline_from_postgres_datadir(), to load one relation file. fn import_relfile( - timeline: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, path: &Path, spcoid: Oid, dboid: Oid, -) -> Result<()> { +) -> anyhow::Result<()> { // Does it look like a relation file? trace!("importing rel file {}", path.display()); - let p = parse_relfilename(path.file_name().unwrap().to_str().unwrap()); - if let Err(e) = p { - warn!("unrecognized file in postgres datadir: {:?} ({})", path, e); - return Err(e.into()); - } - let (relnode, forknum, segno) = p.unwrap(); + let (relnode, forknum, segno) = parse_relfilename(&path.file_name().unwrap().to_string_lossy()) + .map_err(|e| { + warn!("unrecognized file in postgres datadir: {:?} ({})", path, e); + e + })?; let mut file = File::open(path)?; let mut buf: [u8; 8192] = [0u8; 8192]; @@ -186,14 +190,14 @@ fn import_relfile( relnode, forknum, }; - timeline.put_rel_creation(rel, nblocks as u32)?; + modification.put_rel_creation(rel, nblocks as u32)?; let mut blknum: u32 = segno * (1024 * 1024 * 1024 / pg_constants::BLCKSZ as u32); loop { let r = file.read_exact(&mut buf); match r { Ok(_) => { - timeline.put_rel_page_image(rel, blknum, Bytes::copy_from_slice(&buf))?; + modification.put_rel_page_image(rel, blknum, Bytes::copy_from_slice(&buf))?; } // TODO: UnexpectedEof is expected @@ -216,7 +220,7 @@ fn import_relfile( /// Import a relmapper (pg_filenode.map) file into the repository fn import_relmap_file( - timeline: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, spcnode: Oid, dbnode: Oid, path: &Path, @@ -228,13 +232,13 @@ fn import_relmap_file( trace!("importing relmap file {}", path.display()); - timeline.put_relmap_file(spcnode, dbnode, Bytes::copy_from_slice(&buffer[..]))?; + modification.put_relmap_file(spcnode, dbnode, Bytes::copy_from_slice(&buffer[..]))?; Ok(()) } /// Import a twophase state file (pg_twophase/) into the repository fn import_twophase_file( - timeline: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, xid: TransactionId, path: &Path, ) -> Result<()> { @@ -245,7 +249,7 @@ fn import_twophase_file( trace!("importing non-rel file {}", path.display()); - timeline.put_twophase_file(xid, Bytes::copy_from_slice(&buffer[..]))?; + modification.put_twophase_file(xid, Bytes::copy_from_slice(&buffer[..]))?; Ok(()) } @@ -255,7 +259,7 @@ fn import_twophase_file( /// The control file is imported as is, but we also extract the checkpoint record /// from it and store it separated. fn import_control_file( - timeline: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, path: &Path, ) -> Result { let mut file = File::open(path)?; @@ -266,12 +270,12 @@ fn import_control_file( trace!("importing control file {}", path.display()); // Import it as ControlFile - timeline.put_control_file(Bytes::copy_from_slice(&buffer[..]))?; + modification.put_control_file(Bytes::copy_from_slice(&buffer[..]))?; // Extract the checkpoint record and import it separately. let pg_control = ControlFileData::decode(&buffer)?; let checkpoint_bytes = pg_control.checkPointCopy.encode(); - timeline.put_checkpoint(checkpoint_bytes)?; + modification.put_checkpoint(checkpoint_bytes)?; Ok(pg_control) } @@ -280,7 +284,7 @@ fn import_control_file( /// Import an SLRU segment file /// fn import_slru_file( - timeline: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, slru: SlruKind, path: &Path, ) -> Result<()> { @@ -288,7 +292,7 @@ fn import_slru_file( let mut file = File::open(path)?; let mut buf: [u8; 8192] = [0u8; 8192]; - let segno = u32::from_str_radix(path.file_name().unwrap().to_str().unwrap(), 16)?; + let segno = u32::from_str_radix(&path.file_name().unwrap().to_string_lossy(), 16)?; let len = file.metadata().unwrap().len(); ensure!(len % pg_constants::BLCKSZ as u64 == 0); // we assume SLRU block size is the same as BLCKSZ @@ -296,14 +300,19 @@ fn import_slru_file( ensure!(nblocks <= pg_constants::SLRU_PAGES_PER_SEGMENT as u64); - timeline.put_slru_segment_creation(slru, segno, nblocks as u32)?; + modification.put_slru_segment_creation(slru, segno, nblocks as u32)?; let mut rpageno = 0; loop { let r = file.read_exact(&mut buf); match r { Ok(_) => { - timeline.put_slru_page_image(slru, segno, rpageno, Bytes::copy_from_slice(&buf))?; + modification.put_slru_page_image( + slru, + segno, + rpageno, + Bytes::copy_from_slice(&buf), + )?; } // TODO: UnexpectedEof is expected diff --git a/pageserver/src/keyspace.rs b/pageserver/src/keyspace.rs index aeaceb37fc..f6f0d7b7cf 100644 --- a/pageserver/src/keyspace.rs +++ b/pageserver/src/keyspace.rs @@ -2,23 +2,20 @@ use crate::repository::{key_range_size, singleton_range, Key}; use postgres_ffi::pg_constants; use std::ops::Range; -// Target file size, when creating image and delta layers -pub const TARGET_FILE_SIZE_BYTES: u64 = 128 * 1024 * 1024; // 128 MB - /// /// Represents a set of Keys, in a compact form. /// #[derive(Clone, Debug)] pub struct KeySpace { - // Contiguous ranges of keys that belong to the key space. In key order, and - // with no overlap. + /// Contiguous ranges of keys that belong to the key space. In key order, + /// and with no overlap. pub ranges: Vec>, } impl KeySpace { /// - /// Partition a key space into roughly chunks of roughly 'target_size' bytes in - /// each patition. + /// Partition a key space into roughly chunks of roughly 'target_size' bytes + /// in each patition. /// pub fn partition(&self, target_size: u64) -> KeyPartitioning { // Assume that each value is 8k in size. diff --git a/pageserver/src/layered_repository.rs b/pageserver/src/layered_repository.rs index ca4dc7d6fe..5e93e3389b 100644 --- a/pageserver/src/layered_repository.rs +++ b/pageserver/src/layered_repository.rs @@ -11,8 +11,7 @@ //! parent timeline, and the last LSN that has been written to disk. //! -use anyhow::{bail, ensure, Context, Result}; -use bookfile::Book; +use anyhow::{anyhow, bail, ensure, Context, Result}; use bytes::Bytes; use fail::fail_point; use itertools::Itertools; @@ -34,13 +33,14 @@ use std::time::Instant; use self::metadata::{metadata_path, TimelineMetadata, METADATA_FILE_NAME}; use crate::config::PageServerConf; -use crate::keyspace::{KeyPartitioning, KeySpace}; +use crate::keyspace::KeySpace; use crate::page_cache; -use crate::remote_storage::{schedule_timeline_checkpoint_upload, schedule_timeline_download}; +use crate::remote_storage::{schedule_timeline_checkpoint_upload, RemoteIndex}; use crate::repository::{ - GcResult, Repository, RepositoryTimeline, Timeline, TimelineSyncState, TimelineWriter, + GcResult, Repository, RepositoryTimeline, Timeline, TimelineSyncStatusUpdate, TimelineWriter, }; use crate::repository::{Key, Value}; +use crate::tenant_mgr; use crate::thread_mgr; use crate::virtual_file::VirtualFile; use crate::walreceiver::IS_WAL_RECEIVER; @@ -48,13 +48,18 @@ use crate::walredo::WalRedoManager; use crate::CheckpointConfig; use crate::{ZTenantId, ZTimelineId}; -use zenith_metrics::{register_histogram_vec, Histogram, HistogramVec}; +use zenith_metrics::{ + register_histogram_vec, register_int_gauge_vec, Histogram, HistogramVec, IntGauge, IntGaugeVec, +}; use zenith_utils::crashsafe_dir; use zenith_utils::lsn::{AtomicLsn, Lsn, RecordLsn}; use zenith_utils::seqwait::SeqWait; +mod blob_io; +pub mod block_io; mod delta_layer; -mod ephemeral_file; +mod disk_btree; +pub(crate) mod ephemeral_file; mod filename; mod image_layer; mod inmemory_layer; @@ -95,6 +100,15 @@ lazy_static! { .expect("failed to define a metric"); } +lazy_static! { + static ref LAST_RECORD_LSN: IntGaugeVec = register_int_gauge_vec!( + "pageserver_last_record_lsn", + "Last record LSN grouped by timeline", + &["tenant_id", "timeline_id"] + ) + .expect("failed to define a metric"); +} + /// Parts of the `.zenith/tenants//timelines/` directory prefix. pub const TIMELINES_SEGMENT_NAME: &str = "timelines"; @@ -113,29 +127,48 @@ pub struct LayeredRepository { // timeout... gc_cs: Mutex<()>, walredo_mgr: Arc, + + // provides access to timeline data sitting in the remote storage + // supposed to be used for retrieval of remote consistent lsn in walreceiver + remote_index: RemoteIndex, + /// Makes every timeline to backup their files to remote storage. - upload_relishes: bool, + upload_layers: bool, } /// Public interface impl Repository for LayeredRepository { type Timeline = LayeredTimeline; - fn get_timeline(&self, timelineid: ZTimelineId) -> Result> { - Ok(RepositoryTimeline::from(self.get_or_init_timeline( - timelineid, - &mut self.timelines.lock().unwrap(), - )?)) + fn get_timeline(&self, timelineid: ZTimelineId) -> Option> { + let timelines = self.timelines.lock().unwrap(); + self.get_timeline_internal(timelineid, &timelines) + .map(RepositoryTimeline::from) } - fn list_timelines(&self) -> Result>> { - Ok(self - .timelines + fn get_timeline_load(&self, timelineid: ZTimelineId) -> Result> { + let mut timelines = self.timelines.lock().unwrap(); + match self.get_timeline_load_internal(timelineid, &mut timelines)? { + Some(local_loaded_timeline) => Ok(local_loaded_timeline), + None => anyhow::bail!( + "cannot get local timeline: unknown timeline id: {}", + timelineid + ), + } + } + + fn list_timelines(&self) -> Vec<(ZTimelineId, RepositoryTimeline)> { + self.timelines .lock() .unwrap() - .values() - .map(|timeline_entry| RepositoryTimeline::from(timeline_entry.clone())) - .collect()) + .iter() + .map(|(timeline_id, timeline_entry)| { + ( + *timeline_id, + RepositoryTimeline::from(timeline_entry.clone()), + ) + }) + .collect() } fn create_empty_timeline( @@ -158,14 +191,20 @@ impl Repository for LayeredRepository { timelineid, self.tenantid, Arc::clone(&self.walredo_mgr), - self.upload_relishes, + self.upload_layers, ); timeline.layers.lock().unwrap().next_open_layer_at = Some(initdb_lsn); - let timeline_rc = Arc::new(timeline); - let r = timelines.insert(timelineid, LayeredTimelineEntry::Local(timeline_rc.clone())); - assert!(r.is_none()); - Ok(timeline_rc) + let timeline = Arc::new(timeline); + let r = timelines.insert( + timelineid, + LayeredTimelineEntry::Loaded(Arc::clone(&timeline)), + ); + ensure!( + r.is_none(), + "assertion failure, inserted duplicate timeline" + ); + Ok(timeline) } /// Branch a timeline @@ -176,14 +215,12 @@ impl Repository for LayeredRepository { let _gc_cs = self.gc_cs.lock().unwrap(); let mut timelines = self.timelines.lock().unwrap(); - let src_timeline = match self.get_or_init_timeline(src, &mut timelines)? { - LayeredTimelineEntry::Local(timeline) => timeline, - LayeredTimelineEntry::Remote { .. } => { - bail!("Cannot branch off the timeline {} that's not local", src) - } - }; + let src_timeline = self + .get_timeline_load_internal(src, &mut timelines) + // message about timeline being remote is one .context up in the stack + .context("failed to load timeline for branching")? + .ok_or_else(|| anyhow::anyhow!("unknown timeline id: {}", &src))?; let latest_gc_cutoff_lsn = src_timeline.get_latest_gc_cutoff_lsn(); - src_timeline .check_lsn_is_in_scope(start_lsn, &latest_gc_cutoff_lsn) .context("invalid branch start lsn")?; @@ -218,6 +255,7 @@ impl Repository for LayeredRepository { ); crashsafe_dir::create_dir_all(self.conf.timeline_path(&dst, &self.tenantid))?; Self::save_metadata(self.conf, dst, self.tenantid, &metadata, true)?; + timelines.insert(dst, LayeredTimelineEntry::Unloaded { id: dst, metadata }); info!("branched timeline {} from {} at {}", dst, src, start_lsn); @@ -260,10 +298,10 @@ impl Repository for LayeredRepository { let _entered = info_span!("compact", timeline = %timelineid, tenant = %self.tenantid).entered(); match timeline { - LayeredTimelineEntry::Local(timeline) => { + LayeredTimelineEntry::Loaded(timeline) => { timeline.compact()?; } - LayeredTimelineEntry::Remote { .. } => { + LayeredTimelineEntry::Unloaded { .. } => { debug!("Cannot compact remote timeline {}", timelineid) } } @@ -280,11 +318,19 @@ impl Repository for LayeredRepository { fn checkpoint(&self) -> Result<()> { // Scan through the hashmap and collect a list of all the timelines, // while holding the lock. Then drop the lock and actually perform the - // checkpoints. We don't want to block everything else while the + // checkpoints. We don't want to block everything else while the // checkpoint runs. let timelines = self.timelines.lock().unwrap(); let timelines_to_compact = timelines .iter() + // filter to get only loaded timelines + .filter_map(|(timelineid, entry)| match entry { + LayeredTimelineEntry::Loaded(timeline) => Some((timelineid, timeline)), + LayeredTimelineEntry::Unloaded { .. } => { + debug!("Skipping checkpoint for unloaded timeline {}", timelineid); + None + } + }) .map(|(timelineid, timeline)| (*timelineid, timeline.clone())) .collect::>(); drop(timelines); @@ -292,15 +338,7 @@ impl Repository for LayeredRepository { for (timelineid, timeline) in &timelines_to_compact { let _entered = info_span!("checkpoint", timeline = %timelineid, tenant = %self.tenantid).entered(); - match timeline { - LayeredTimelineEntry::Local(timeline) => { - timeline.checkpoint(CheckpointConfig::Flush)?; - } - LayeredTimelineEntry::Remote { .. } => debug!( - "Cannot run the checkpoint for remote timeline {}", - timelineid - ), - } + timeline.checkpoint(CheckpointConfig::Flush)?; } Ok(()) @@ -309,32 +347,10 @@ impl Repository for LayeredRepository { // Detaches the timeline from the repository. fn detach_timeline(&self, timeline_id: ZTimelineId) -> Result<()> { let mut timelines = self.timelines.lock().unwrap(); - match timelines.entry(timeline_id) { - Entry::Vacant(_) => { - bail!("cannot detach non existing timeline"); - } - Entry::Occupied(mut entry) => { - let timeline_entry = entry.get_mut(); + if timelines.remove(&timeline_id).is_none() { + bail!("cannot detach timeline that is not available locally"); + } - let timeline = match timeline_entry { - LayeredTimelineEntry::Remote { .. } => { - bail!("cannot detach remote timeline {}", timeline_id); - } - LayeredTimelineEntry::Local(timeline) => timeline, - }; - - // TODO (rodionov) keep local state in timeline itself (refactoring related to https://github.com/zenithdb/zenith/issues/997 and #1104) - - // FIXME this is local disk consistent lsn, need to keep the latest succesfully uploaded checkpoint lsn in timeline (metadata?) - // https://github.com/zenithdb/zenith/issues/1104 - let remote_disk_consistent_lsn = timeline.disk_consistent_lsn.load(); - // reference to timeline is dropped here - entry.insert(LayeredTimelineEntry::Remote { - id: timeline_id, - disk_consistent_lsn: remote_disk_consistent_lsn, - }); - } - }; // Release the lock to shutdown and remove the files without holding it drop(timelines); // shutdown the timeline (this shuts down the walreceiver) @@ -345,158 +361,144 @@ impl Repository for LayeredRepository { Ok(()) } - // TODO this method currentlly does not do anything to prevent (or react to) state updates between a sync task schedule and a sync task end (that causes this update). - // Sync task is enqueued and can error and be rescheduled, so some significant time may pass between the events. - // - /// Reacts on the timeline sync state change, changing pageserver's memory state for this timeline (unload or load of the timeline files). - fn set_timeline_state( + fn apply_timeline_remote_sync_status_update( &self, timeline_id: ZTimelineId, - new_state: TimelineSyncState, + timeline_sync_status_update: TimelineSyncStatusUpdate, ) -> Result<()> { debug!( - "set_timeline_state: timeline_id: {}, new_state: {:?}", - timeline_id, new_state + "apply_timeline_remote_sync_status_update timeline_id: {} update: {:?}", + timeline_id, timeline_sync_status_update ); - let mut timelines_accessor = self.timelines.lock().unwrap(); - - match new_state { - TimelineSyncState::Ready(_) => { - let reloaded_timeline = - self.init_local_timeline(timeline_id, &mut timelines_accessor)?; - timelines_accessor - .insert(timeline_id, LayeredTimelineEntry::Local(reloaded_timeline)); - None + match timeline_sync_status_update { + TimelineSyncStatusUpdate::Uploaded => { /* nothing to do, remote consistent lsn is managed by the remote storage */ } - TimelineSyncState::Evicted(_) => timelines_accessor.remove(&timeline_id), - TimelineSyncState::AwaitsDownload(disk_consistent_lsn) - | TimelineSyncState::CloudOnly(disk_consistent_lsn) => timelines_accessor.insert( - timeline_id, - LayeredTimelineEntry::Remote { - id: timeline_id, - disk_consistent_lsn, - }, - ), - }; - // NOTE we do not delete local data in case timeline became cloud only, this is performed in detach_timeline - drop(timelines_accessor); - + TimelineSyncStatusUpdate::Downloaded => { + match self.timelines.lock().unwrap().entry(timeline_id) { + Entry::Occupied(_) => bail!("We completed a download for a timeline that already exists in repository. This is a bug."), + Entry::Vacant(entry) => { + // we need to get metadata of a timeline, another option is to pass it along with Downloaded status + let metadata = Self::load_metadata(self.conf, timeline_id, self.tenantid).context("failed to load local metadata")?; + // finally we make newly downloaded timeline visible to repository + entry.insert(LayeredTimelineEntry::Unloaded { id: timeline_id, metadata, }) + }, + }; + } + } Ok(()) } - /// Layered repo does not store anything but - /// * local, fully loaded timelines, ready for usage - /// * remote timelines, that need a download task scheduled first before they can be used - /// - /// [`TimelineSyncState::Evicted`] and other non-local and non-remote states are not stored in the layered repo at all, - /// hence their statuses cannot be returned by the repo. - fn get_timeline_state(&self, timeline_id: ZTimelineId) -> Option { - let timelines_accessor = self.timelines.lock().unwrap(); - let timeline_entry = timelines_accessor.get(&timeline_id)?; - Some( - if timeline_entry - .local_or_schedule_download(self.tenantid) - .is_some() - { - TimelineSyncState::Ready(timeline_entry.disk_consistent_lsn()) - } else { - TimelineSyncState::CloudOnly(timeline_entry.disk_consistent_lsn()) - }, - ) + fn get_remote_index(&self) -> &RemoteIndex { + &self.remote_index } } #[derive(Clone)] enum LayeredTimelineEntry { - Local(Arc), - Remote { + Loaded(Arc), + Unloaded { id: ZTimelineId, - /// metadata contents of the latest successfully uploaded checkpoint - disk_consistent_lsn: Lsn, + metadata: TimelineMetadata, }, } impl LayeredTimelineEntry { fn timeline_id(&self) -> ZTimelineId { match self { - LayeredTimelineEntry::Local(timeline) => timeline.timelineid, - LayeredTimelineEntry::Remote { id, .. } => *id, + LayeredTimelineEntry::Loaded(timeline) => timeline.timelineid, + LayeredTimelineEntry::Unloaded { id, .. } => *id, } } - /// Gets local timeline data, if it's present. Otherwise schedules a download fot the remote timeline and returns `None`. - fn local_or_schedule_download(&self, tenant_id: ZTenantId) -> Option> { + fn ancestor_timeline_id(&self) -> Option { match self { - Self::Local(local) => Some(Arc::clone(local)), - Self::Remote { - id: timeline_id, .. - } => { - debug!( - "Accessed a remote timeline {} for tenant {}, scheduling a timeline download", - timeline_id, tenant_id - ); - schedule_timeline_download(tenant_id, *timeline_id); - None + LayeredTimelineEntry::Loaded(timeline) => { + timeline.ancestor_timeline.as_ref().map(|t| t.timeline_id()) } + LayeredTimelineEntry::Unloaded { metadata, .. } => metadata.ancestor_timeline(), } } - /// Gets a current (latest for the remote case) disk consistent Lsn for the timeline. - fn disk_consistent_lsn(&self) -> Lsn { + fn ancestor_lsn(&self) -> Lsn { match self { - Self::Local(local) => local.disk_consistent_lsn.load(), - Self::Remote { - disk_consistent_lsn, - .. - } => *disk_consistent_lsn, + LayeredTimelineEntry::Loaded(timeline) => timeline.ancestor_lsn, + LayeredTimelineEntry::Unloaded { metadata, .. } => metadata.ancestor_lsn(), + } + } + + fn ensure_loaded(&self) -> anyhow::Result<&Arc> { + match self { + LayeredTimelineEntry::Loaded(timeline) => Ok(timeline), + LayeredTimelineEntry::Unloaded { .. } => { + anyhow::bail!("timeline is unloaded") + } } } } impl From for RepositoryTimeline { - fn from(layered_timeline: LayeredTimelineEntry) -> Self { - match layered_timeline { - LayeredTimelineEntry::Local(timeline) => RepositoryTimeline::Local { - id: timeline.timelineid, - timeline, - }, - LayeredTimelineEntry::Remote { - id, - disk_consistent_lsn, - } => RepositoryTimeline::Remote { - id, - disk_consistent_lsn, - }, + fn from(entry: LayeredTimelineEntry) -> Self { + match entry { + LayeredTimelineEntry::Loaded(timeline) => RepositoryTimeline::Loaded(timeline as _), + LayeredTimelineEntry::Unloaded { metadata, .. } => { + RepositoryTimeline::Unloaded { metadata } + } } } } /// Private functions impl LayeredRepository { - // Implementation of the public `get_timeline` function. This differs from the public - // interface in that the caller must already hold the mutex on the 'timelines' hashmap. - fn get_or_init_timeline( + // Implementation of the public `get_timeline` function. + // Differences from the public: + // * interface in that the caller must already hold the mutex on the 'timelines' hashmap. + fn get_timeline_internal( + &self, + timelineid: ZTimelineId, + timelines: &HashMap, + ) -> Option { + timelines.get(&timelineid).cloned() + } + + // Implementation of the public `get_timeline_load` function. + // Differences from the public: + // * interface in that the caller must already hold the mutex on the 'timelines' hashmap. + fn get_timeline_load_internal( &self, timelineid: ZTimelineId, timelines: &mut HashMap, - ) -> Result { + ) -> anyhow::Result>> { match timelines.get(&timelineid) { - Some(timeline_entry) => { - let _ = timeline_entry.local_or_schedule_download(self.tenantid); - Ok(timeline_entry.clone()) - } + Some(entry) => match entry { + LayeredTimelineEntry::Loaded(local_timeline) => { + debug!("timeline {} found loaded into memory", &timelineid); + return Ok(Some(Arc::clone(local_timeline))); + } + LayeredTimelineEntry::Unloaded { .. } => {} + }, None => { - let timeline = self.init_local_timeline(timelineid, timelines)?; - timelines.insert( - timelineid, - LayeredTimelineEntry::Local(Arc::clone(&timeline)), - ); - Ok(LayeredTimelineEntry::Local(timeline)) + debug!("timeline {} not found", &timelineid); + return Ok(None); } - } + }; + debug!( + "timeline {} found on a local disk, but not loaded into the memory, loading", + &timelineid + ); + let timeline = self.load_local_timeline(timelineid, timelines)?; + let was_loaded = timelines.insert( + timelineid, + LayeredTimelineEntry::Loaded(Arc::clone(&timeline)), + ); + ensure!( + was_loaded.is_none() + || matches!(was_loaded, Some(LayeredTimelineEntry::Unloaded { .. })), + "assertion failure, inserted wrong timeline in an incorrect state" + ); + Ok(Some(timeline)) } - fn init_local_timeline( + fn load_local_timeline( &self, timelineid: ZTimelineId, timelines: &mut HashMap, @@ -507,11 +509,19 @@ impl LayeredRepository { let ancestor = metadata .ancestor_timeline() - .map(|ancestor_timelineid| self.get_or_init_timeline(ancestor_timelineid, timelines)) - .transpose()?; - let _enter = - info_span!("loading timeline", timeline = %timelineid, tenant = %self.tenantid) - .entered(); + .map(|ancestor_timeline_id| { + trace!( + "loading {}'s ancestor {}", + timelineid, + &ancestor_timeline_id + ); + self.get_timeline_load_internal(ancestor_timeline_id, timelines) + }) + .transpose() + .context("cannot load ancestor timeline")? + .flatten() + .map(LayeredTimelineEntry::Loaded); + let _enter = info_span!("loading local timeline").entered(); let timeline = LayeredTimeline::new( self.conf, metadata, @@ -519,7 +529,7 @@ impl LayeredRepository { timelineid, self.tenantid, Arc::clone(&self.walredo_mgr), - self.upload_relishes, + self.upload_layers, ); timeline .load_layer_map(disk_consistent_lsn) @@ -532,7 +542,8 @@ impl LayeredRepository { conf: &'static PageServerConf, walredo_mgr: Arc, tenantid: ZTenantId, - upload_relishes: bool, + remote_index: RemoteIndex, + upload_layers: bool, ) -> LayeredRepository { LayeredRepository { tenantid, @@ -540,7 +551,8 @@ impl LayeredRepository { timelines: Mutex::new(HashMap::new()), gc_cs: Mutex::new(()), walredo_mgr, - upload_relishes, + remote_index, + upload_layers, } } @@ -621,92 +633,54 @@ impl LayeredRepository { horizon: u64, checkpoint_before_gc: bool, ) -> Result { + let _span_guard = + info_span!("gc iteration", tenant = %self.tenantid, timeline = ?target_timelineid); let mut totals: GcResult = Default::default(); let now = Instant::now(); // grab mutex to prevent new timelines from being created here. let _gc_cs = self.gc_cs.lock().unwrap(); - let mut timelines = self.timelines.lock().unwrap(); - // Scan all timelines. For each timeline, remember the timeline ID and // the branch point where it was created. - // - let mut timelineids: Vec = Vec::new(); - - // We scan the directory, not the in-memory hash table, because the hash - // table only contains entries for timelines that have been accessed. We - // need to take all timelines into account, not only the active ones. - let timelines_path = self.conf.timelines_path(&self.tenantid); - - for direntry in fs::read_dir(timelines_path)? { - let direntry = direntry?; - if let Some(fname) = direntry.file_name().to_str() { - if let Ok(timelineid) = fname.parse::() { - timelineids.push(timelineid); - } - } - } - - // Now collect info about branchpoints let mut all_branchpoints: BTreeSet<(ZTimelineId, Lsn)> = BTreeSet::new(); - for &timelineid in &timelineids { - let timeline = match self.get_or_init_timeline(timelineid, &mut timelines)? { - LayeredTimelineEntry::Local(timeline) => timeline, - LayeredTimelineEntry::Remote { .. } => { - warn!( - "Timeline {} is not local, cannot proceed with gc", - timelineid - ); - return Ok(totals); - } - }; + let mut timeline_ids = Vec::new(); + let mut timelines = self.timelines.lock().unwrap(); - if let Some(ancestor_timeline) = &timeline.ancestor_timeline { - let ancestor_timeline = - match ancestor_timeline.local_or_schedule_download(self.tenantid) { - Some(timeline) => timeline, - None => { - warn!( - "Timeline {} has ancestor {} is not local, cannot proceed with gc", - timelineid, - ancestor_timeline.timeline_id() - ); - return Ok(totals); - } - }; + for (timeline_id, timeline_entry) in timelines.iter() { + timeline_ids.push(*timeline_id); + + // This is unresolved question for now, how to do gc in presense of remote timelines + // especially when this is combined with branching. + // Somewhat related: https://github.com/zenithdb/zenith/issues/999 + if let Some(ancestor_timeline_id) = &timeline_entry.ancestor_timeline_id() { // If target_timeline is specified, we only need to know branchpoints of its children if let Some(timelineid) = target_timelineid { - if ancestor_timeline.timelineid == timelineid { + if ancestor_timeline_id == &timelineid { all_branchpoints - .insert((ancestor_timeline.timelineid, timeline.ancestor_lsn)); + .insert((*ancestor_timeline_id, timeline_entry.ancestor_lsn())); } } // Collect branchpoints for all timelines else { - all_branchpoints.insert((ancestor_timeline.timelineid, timeline.ancestor_lsn)); + all_branchpoints.insert((*ancestor_timeline_id, timeline_entry.ancestor_lsn())); } } } // Ok, we now know all the branch points. // Perform GC for each timeline. - for timelineid in timelineids { + for timelineid in timeline_ids.into_iter() { if thread_mgr::is_shutdown_requested() { // We were requested to shut down. Stop and return with the progress we // made. break; } - // We have already loaded all timelines above - // so this operation is just a quick map lookup. - let timeline = match self.get_or_init_timeline(timelineid, &mut *timelines)? { - LayeredTimelineEntry::Local(timeline) => timeline, - LayeredTimelineEntry::Remote { .. } => { - debug!("Skipping GC for non-local timeline {}", timelineid); - continue; - } - }; + // Timeline is known to be local and loaded. + let timeline = self + .get_timeline_load_internal(timelineid, &mut *timelines)? + .expect("checked above that timeline is local and loaded"); // If target_timeline is specified, only GC it if let Some(target_timelineid) = target_timelineid { @@ -787,14 +761,15 @@ pub struct LayeredTimeline { ancestor_timeline: Option, ancestor_lsn: Lsn, - // Metrics histograms + // Metrics reconstruct_time_histo: Histogram, flush_time_histo: Histogram, compact_time_histo: Histogram, create_images_time_histo: Histogram, + last_record_gauge: IntGauge, /// If `true`, will backup its files that appear after each checkpointing to the remote storage. - upload_relishes: AtomicBool, + upload_layers: AtomicBool, /// Ensures layers aren't frozen by checkpointer between /// [`LayeredTimeline::get_layer_for_write`] and layer reads. @@ -819,8 +794,6 @@ pub struct LayeredTimeline { // garbage collecting data that is still needed by the child timelines. gc_info: RwLock, - partitioning: RwLock>, - // It may change across major versions so for simplicity // keep it after running initdb for a timeline. // It is needed in checks when we want to error on some operations @@ -830,8 +803,25 @@ pub struct LayeredTimeline { initdb_lsn: Lsn, } +/// +/// Information about how much history needs to be retained, needed by +/// Garbage Collection. +/// struct GcInfo { + /// Specific LSNs that are needed. + /// + /// Currently, this includes all points where child branches have + /// been forked off from. In the future, could also include + /// explicit user-defined snapshot points. retain_lsns: Vec, + + /// In addition to 'retain_lsns', keep everything newer than this + /// point. + /// + /// This is calculated by subtracting 'gc_horizon' setting from + /// last-record LSN + /// + /// FIXME: is this inclusive or exclusive? cutoff: Lsn, } @@ -848,10 +838,10 @@ impl Timeline for LayeredTimeline { } /// Wait until WAL has been received up to the given LSN. - fn wait_lsn(&self, lsn: Lsn) -> Result<()> { + fn wait_lsn(&self, lsn: Lsn) -> anyhow::Result<()> { // This should never be called from the WAL receiver thread, because that could lead // to a deadlock. - assert!( + ensure!( !IS_WAL_RECEIVER.with(|c| c.get()), "wait_lsn called by WAL receiver thread" ); @@ -906,7 +896,7 @@ impl Timeline for LayeredTimeline { /// Public entry point for checkpoint(). All the logic is in the private /// checkpoint_internal function, this public facade just wraps it for /// metrics collection. - fn checkpoint(&self, cconf: CheckpointConfig) -> Result<()> { + fn checkpoint(&self, cconf: CheckpointConfig) -> anyhow::Result<()> { match cconf { CheckpointConfig::Flush => { self.freeze_inmem_layer(false); @@ -953,14 +943,6 @@ impl Timeline for LayeredTimeline { self.disk_consistent_lsn.load() } - fn hint_partitioning(&self, partitioning: KeyPartitioning, lsn: Lsn) -> Result<()> { - self.partitioning - .write() - .unwrap() - .replace((partitioning, lsn)); - Ok(()) - } - fn writer<'a>(&'a self) -> Box { Box::new(LayeredTimelineWriter { tl: self, @@ -981,7 +963,7 @@ impl LayeredTimeline { timelineid: ZTimelineId, tenantid: ZTenantId, walredo_mgr: Arc, - upload_relishes: bool, + upload_layers: bool, ) -> LayeredTimeline { let reconstruct_time_histo = RECONSTRUCT_TIME .get_metric_with_label_values(&[&tenantid.to_string(), &timelineid.to_string()]) @@ -1007,6 +989,9 @@ impl LayeredTimeline { &timelineid.to_string(), ]) .unwrap(); + let last_record_gauge = LAST_RECORD_LSN + .get_metric_with_label_values(&[&tenantid.to_string(), &timelineid.to_string()]) + .unwrap(); LayeredTimeline { conf, @@ -1032,8 +1017,9 @@ impl LayeredTimeline { flush_time_histo, compact_time_histo, create_images_time_histo, + last_record_gauge, - upload_relishes: AtomicBool::new(upload_relishes), + upload_layers: AtomicBool::new(upload_layers), write_lock: Mutex::new(()), layer_flush_lock: Mutex::new(()), @@ -1043,7 +1029,6 @@ impl LayeredTimeline { retain_lsns: Vec::new(), cutoff: Lsn(0), }), - partitioning: RwLock::new(None), latest_gc_cutoff_lsn: RwLock::new(metadata.latest_gc_cutoff_lsn()), initdb_lsn: metadata.initdb_lsn(), @@ -1065,9 +1050,9 @@ impl LayeredTimeline { for direntry in fs::read_dir(timeline_path)? { let direntry = direntry?; let fname = direntry.file_name(); - let fname = fname.to_str().unwrap(); + let fname = fname.to_string_lossy(); - if let Some(imgfilename) = ImageFileName::parse_str(fname) { + if let Some(imgfilename) = ImageFileName::parse_str(&fname) { // create an ImageLayer struct for each image file. if imgfilename.lsn > disk_consistent_lsn { warn!( @@ -1085,7 +1070,7 @@ impl LayeredTimeline { trace!("found layer {}", layer.filename().display()); layers.insert_historic(Arc::new(layer)); num_layers += 1; - } else if let Some(deltafilename) = DeltaFileName::parse_str(fname) { + } else if let Some(deltafilename) = DeltaFileName::parse_str(&fname) { // Create a DeltaLayer struct for each delta file. // The end-LSN is exclusive, while disk_consistent_lsn is // inclusive. For example, if disk_consistent_lsn is 100, it is @@ -1110,7 +1095,7 @@ impl LayeredTimeline { num_layers += 1; } else if fname == METADATA_FILE_NAME || fname.ends_with(".old") { // ignore these - } else if is_ephemeral_file(fname) { + } else if is_ephemeral_file(&fname) { // Delete any old ephemeral files trace!("deleting old ephemeral file in timeline dir: {}", fname); fs::remove_file(direntry.path())?; @@ -1142,7 +1127,7 @@ impl LayeredTimeline { key: Key, request_lsn: Lsn, reconstruct_state: &mut ValueReconstructState, - ) -> Result<()> { + ) -> anyhow::Result<()> { // Start from the current timeline. let mut timeline_owned; let mut timeline = self; @@ -1274,33 +1259,29 @@ impl LayeredTimeline { } fn get_ancestor_timeline(&self) -> Result> { - let ancestor_entry = self + let ancestor = self .ancestor_timeline .as_ref() - .expect("get_ancestor_timeline() called on timeline with no parent"); - - let timeline = match ancestor_entry.local_or_schedule_download(self.tenantid) { - Some(timeline) => timeline, - None => { - bail!( - "Cannot get the whole layer for read locked: ancestor of timeline {} is not present locally", - self.timelineid - ) - } - }; - Ok(timeline) + .expect("there should be an ancestor") + .ensure_loaded() + .with_context(|| { + format!( + "Cannot get the whole layer for read locked: timeline {} is not present locally", + self.get_ancestor_timeline_id().unwrap()) + })?; + Ok(Arc::clone(ancestor)) } /// /// Get a handle to the latest layer for appending. /// - fn get_layer_for_write(&self, lsn: Lsn) -> Result> { + fn get_layer_for_write(&self, lsn: Lsn) -> anyhow::Result> { let mut layers = self.layers.lock().unwrap(); - assert!(lsn.is_aligned()); + ensure!(lsn.is_aligned()); let last_record_lsn = self.get_last_record_lsn(); - assert!( + ensure!( lsn > last_record_lsn, "cannot modify relation after advancing last_record_lsn (incoming_lsn={}, last_record_lsn={})", lsn, @@ -1354,6 +1335,7 @@ impl LayeredTimeline { fn finish_write(&self, new_lsn: Lsn) { assert!(new_lsn.is_aligned()); + self.last_record_gauge.set(new_lsn.0 as i64); self.last_record_lsn.advance(new_lsn); } @@ -1382,6 +1364,10 @@ impl LayeredTimeline { drop(layers); } + /// + /// Check if more than 'checkpoint_distance' of WAL has been accumulated + /// in the in-memory layer, and initiate flushing it if so. + /// pub fn check_checkpoint_distance(self: &Arc) -> Result<()> { let last_lsn = self.get_last_record_lsn(); @@ -1398,6 +1384,7 @@ impl LayeredTimeline { Some(self.tenantid), Some(self.timelineid), "layer flush thread", + false, move || self_clone.flush_frozen_layers(false), )?; } @@ -1487,8 +1474,7 @@ impl LayeredTimeline { // // TODO: This perhaps should be done in 'flush_frozen_layers', after flushing // *all* the layers, to avoid fsyncing the file multiple times. - let disk_consistent_lsn; - disk_consistent_lsn = Lsn(frozen_layer.get_lsn_range().end.0 - 1); + let disk_consistent_lsn = Lsn(frozen_layer.get_lsn_range().end.0 - 1); // If we were able to advance 'disk_consistent_lsn', save it the metadata file. // After crash, we will restart WAL streaming and processing from that point. @@ -1538,7 +1524,7 @@ impl LayeredTimeline { &metadata, false, )?; - if self.upload_relishes.load(atomic::Ordering::Relaxed) { + if self.upload_layers.load(atomic::Ordering::Relaxed) { schedule_timeline_checkpoint_upload( self.tenantid, self.timelineid, @@ -1593,20 +1579,15 @@ impl LayeredTimeline { let target_file_size = self.conf.checkpoint_distance; - // 1. The partitioning was already done by the code in - // pgdatadir_mapping.rs. We just use it here. - let partitioning_guard = self.partitioning.read().unwrap(); - if let Some((partitioning, lsn)) = partitioning_guard.as_ref() { + // Define partitioning schema if needed + if let Ok(pgdir) = tenant_mgr::get_timeline_for_tenant_load(self.tenantid, self.timelineid) + { + let (partitioning, lsn) = + pgdir.repartition(self.get_last_record_lsn(), self.conf.compaction_target_size)?; let timer = self.create_images_time_histo.start_timer(); - // Make a copy of the partitioning, so that we can release - // the lock. Otherwise we could block the WAL receiver. - let lsn = *lsn; - let parts = partitioning.parts.clone(); - drop(partitioning_guard); - // 2. Create new image layers for partitions that have been modified // "enough". - for part in parts.iter() { + for part in partitioning.parts.iter() { if self.time_for_new_image_layer(part, lsn, 3)? { self.create_image_layer(part, lsn)?; } @@ -1618,18 +1599,9 @@ impl LayeredTimeline { self.compact_level0(target_file_size)?; timer.stop_and_record(); } else { - info!("Could not compact because no partitioning specified yet"); + debug!("Could not compact because no partitioning specified yet"); } - // Call unload() on all frozen layers, to release memory. - // This shouldn't be much memory, as only metadata is slurped - // into memory. - let layers = self.layers.lock().unwrap(); - for layer in layers.iter_historic_layers() { - layer.unload()?; - } - drop(layers); - Ok(()) } @@ -1653,7 +1625,7 @@ impl LayeredTimeline { let num_deltas = layers.count_deltas(&img_range, &(img_lsn..lsn))?; - info!( + debug!( "range {}-{}, has {} deltas on this timeline", img_range.start, img_range.end, num_deltas ); @@ -1896,7 +1868,7 @@ impl LayeredTimeline { // 1. Is it newer than cutoff point? if l.get_lsn_range().end > cutoff { - info!( + debug!( "keeping {} because it's newer than cutoff {}", l.filename().display(), cutoff @@ -1913,7 +1885,7 @@ impl LayeredTimeline { for retain_lsn in retain_lsns { // start_lsn is inclusive if &l.get_lsn_range().start <= retain_lsn { - info!( + debug!( "keeping {} because it's still might be referenced by child branch forked at {} is_dropped: xx is_incremental: {}", l.filename().display(), retain_lsn, @@ -1931,12 +1903,14 @@ impl LayeredTimeline { // OK for a delta layer to have end LSN 101, but if the end LSN // is 102, then it might not have been fully flushed to disk // before crash. + // + // FIXME: This logic is wrong. See https://github.com/zenithdb/zenith/issues/707 if !layers.newer_image_layer_exists( &l.get_key_range(), l.get_lsn_range().end, disk_consistent_lsn + 1, )? { - info!( + debug!( "keeping {} because it is the latest layer", l.filename().display() ); @@ -1945,7 +1919,7 @@ impl LayeredTimeline { } // We didn't find any reason to keep this file, so remove it. - info!( + debug!( "garbage collecting {} is_dropped: xx is_incremental: {}", l.filename().display(), l.is_incremental(), @@ -2071,17 +2045,18 @@ impl<'a> TimelineWriter<'_> for LayeredTimelineWriter<'a> { } /// Dump contents of a layer file to stdout. -pub fn dump_layerfile_from_path(path: &Path) -> Result<()> { - let file = File::open(path)?; - let book = Book::new(file)?; +pub fn dump_layerfile_from_path(path: &Path, verbose: bool) -> Result<()> { + use std::os::unix::fs::FileExt; - match book.magic() { - delta_layer::DELTA_FILE_MAGIC => { - DeltaLayer::new_for_path(path, &book)?.dump()?; - } - image_layer::IMAGE_FILE_MAGIC => { - ImageLayer::new_for_path(path, &book)?.dump()?; - } + // All layer files start with a two-byte "magic" value, to identify the kind of + // file. + let file = File::open(path)?; + let mut header_buf = [0u8; 2]; + file.read_exact_at(&mut header_buf, 0)?; + + match u16::from_be_bytes(header_buf) { + crate::IMAGE_FILE_MAGIC => ImageLayer::new_for_path(path, file)?.dump(verbose)?, + crate::DELTA_FILE_MAGIC => DeltaLayer::new_for_path(path, file)?.dump(verbose)?, magic => bail!("unrecognized magic identifier: {:?}", magic), } @@ -2091,7 +2066,10 @@ pub fn dump_layerfile_from_path(path: &Path) -> Result<()> { /// Add a suffix to a layer file's name: .{num}.old /// Uses the first available num (starts at 0) fn rename_to_backup(path: PathBuf) -> anyhow::Result<()> { - let filename = path.file_name().unwrap().to_str().unwrap(); + let filename = path + .file_name() + .ok_or_else(|| anyhow!("Path {} don't have a file name", path.display()))? + .to_string_lossy(); let mut new_path = path.clone(); for i in 0u32.. { @@ -2117,8 +2095,7 @@ pub mod tests { use super::*; use crate::keyspace::KeySpaceAccum; use crate::repository::repo_harness::*; - use rand::thread_rng; - use rand::Rng; + use rand::{thread_rng, Rng}; #[test] fn corrupt_metadata() -> Result<()> { @@ -2135,12 +2112,11 @@ pub mod tests { let mut metadata_bytes = std::fs::read(&metadata_path)?; assert_eq!(metadata_bytes.len(), 512); - metadata_bytes[512 - 4 - 2] ^= 1; + metadata_bytes[8] ^= 1; std::fs::write(metadata_path, metadata_bytes)?; - let new_repo = harness.load(); - let err = new_repo.get_timeline(TIMELINE_ID).err().unwrap(); - assert_eq!(err.to_string(), "failed to load metadata"); + let err = harness.try_load().err().expect("should fail"); + assert_eq!(err.to_string(), "failed to load local metadata"); assert_eq!( err.source().unwrap().to_string(), "metadata checksum mismatch" @@ -2220,7 +2196,7 @@ pub mod tests { let mut test_key = Key::from_hex("012222222233333333444444445500000000").unwrap(); let mut blknum = 0; for _ in 0..50 { - for _ in 0..1000 { + for _ in 0..10000 { test_key.field6 = blknum; let writer = tline.writer(); writer.put( @@ -2238,12 +2214,6 @@ pub mod tests { } let cutoff = tline.get_last_record_lsn(); - let parts = keyspace - .clone() - .to_keyspace() - .partition(TEST_FILE_SIZE as u64); - tline.hint_partitioning(parts.clone(), lsn)?; - tline.update_gc_info(Vec::new(), cutoff); tline.checkpoint(CheckpointConfig::Forced)?; tline.compact()?; @@ -2286,9 +2256,6 @@ pub mod tests { keyspace.add_key(test_key); } - let parts = keyspace.to_keyspace().partition(TEST_FILE_SIZE as u64); - tline.hint_partitioning(parts, lsn)?; - for _ in 0..50 { for _ in 0..NUM_KEYS { lsn = Lsn(lsn.0 + 0x10); @@ -2300,7 +2267,6 @@ pub mod tests { lsn, Value::Image(TEST_IMG(&format!("{} at {}", blknum, lsn))), )?; - println!("updating {} at {}", blknum, lsn); writer.finish_write(lsn); drop(writer); updated[blknum] = lsn; @@ -2360,20 +2326,11 @@ pub mod tests { keyspace.add_key(test_key); } - let parts = keyspace.to_keyspace().partition(TEST_FILE_SIZE as u64); - tline.hint_partitioning(parts, lsn)?; - let mut tline_id = TIMELINE_ID; for _ in 0..50 { let new_tline_id = ZTimelineId::generate(); repo.branch_timeline(tline_id, new_tline_id, lsn)?; - tline = if let RepositoryTimeline::Local { id: _, timeline } = - repo.get_timeline(new_tline_id)? - { - timeline - } else { - panic!("unexpected timeline state"); - }; + tline = repo.get_timeline_load(new_tline_id)?; tline_id = new_tline_id; for _ in 0..NUM_KEYS { diff --git a/pageserver/src/layered_repository/README.md b/pageserver/src/layered_repository/README.md index 20f89ddc70..519478e417 100644 --- a/pageserver/src/layered_repository/README.md +++ b/pageserver/src/layered_repository/README.md @@ -1,40 +1,42 @@ # Overview -The on-disk format is based on immutable files. The page server receives a -stream of incoming WAL, parses the WAL records to determine which pages they -apply to, and accumulates the incoming changes in memory. Every now and then, -the accumulated changes are written out to new immutable files. This process is -called checkpointing. Old versions of on-disk files that are not needed by any -timeline are removed by GC process. - The main responsibility of the Page Server is to process the incoming WAL, and reprocess it into a format that allows reasonably quick access to any page -version. +version. The page server slices the incoming WAL per relation and page, and +packages the sliced WAL into suitably-sized "layer files". The layer files +contain all the history of the database, back to some reasonable retention +period. This system replaces the base backups and the WAL archive used in a +traditional PostgreSQL installation. The layer files are immutable, they are not +modified in-place after creation. New layer files are created for new incoming +WAL, and old layer files are removed when they are no longer needed. + +The on-disk format is based on immutable files. The page server receives a +stream of incoming WAL, parses the WAL records to determine which pages they +apply to, and accumulates the incoming changes in memory. Whenever enough WAL +has been accumulated in memory, it is written out to a new immutable file. That +process accumulates "L0 delta files" on disk. When enough L0 files have been +accumulated, they are merged and re-partitioned into L1 files, and old files +that are no longer needed are removed by Garbage Collection (GC). The incoming WAL contains updates to arbitrary pages in the system. The distribution depends on the workload: the updates could be totally random, or there could be a long stream of updates to a single relation when data is bulk -loaded, for example, or something in between. The page server slices the -incoming WAL per relation and page, and packages the sliced WAL into -suitably-sized "layer files". The layer files contain all the history of the -database, back to some reasonable retention period. This system replaces the -base backups and the WAL archive used in a traditional PostgreSQL -installation. The layer files are immutable, they are not modified in-place -after creation. New layer files are created for new incoming WAL, and old layer -files are removed when they are no longer needed. We could also replace layer -files with new files that contain the same information, merging small files for -example, but that hasn't been implemented yet. +loaded, for example, or something in between. +Cloud Storage Page Server Safekeeper + L1 L0 Memory WAL -Cloud Storage Page Server Safekeeper - Local disk Memory WAL - -|AAAA| |AAAA|AAAA| |AA -|BBBB| |BBBB|BBBB| | -|CCCC|CCCC| <---- |CCCC|CCCC|CCCC| <--- |CC <---- ADEBAABED -|DDDD|DDDD| |DDDD|DDDD| |DDD -|EEEE| |EEEE|EEEE|EEEE| |E - ++----+ +----+----+ +|AAAA| |AAAA|AAAA| +---+-----+ | ++----+ +----+----+ | | | |AA +|BBBB| |BBBB|BBBB| |BB | AA | |BB ++----+----+ +----+----+ |C | BB | |CC +|CCCC|CCCC| <---- |CCCC|CCCC| <--- |D | CC | <--- |DDD <---- ADEBAABED ++----+----+ +----+----+ | | DDD | |E +|DDDD|DDDD| |DDDD|DDDD| |E | | | ++----+----+ +----+----+ | | | +|EEEE| |EEEE|EEEE| +---+-----+ ++----+ +----+----+ In this illustration, WAL is received as a stream from the Safekeeper, from the right. It is immediately captured by the page server and stored quickly in @@ -42,39 +44,29 @@ memory. The page server memory can be thought of as a quick "reorder buffer", used to hold the incoming WAL and reorder it so that we keep the WAL records for the same page and relation close to each other. -From the page server memory, whenever enough WAL has been accumulated for one -relation segment, it is moved to local disk, as a new layer file, and the memory -is released. +From the page server memory, whenever enough WAL has been accumulated, it is flushed +to disk into a new L0 layer file, and the memory is released. + +When enough L0 files have been accumulated, they are merged together rand sliced +per key-space, producing a new set of files where each file contains a more +narrow key range, but larger LSN range. From the local disk, the layers are further copied to Cloud Storage, for long-term archival. After a layer has been copied to Cloud Storage, it can be removed from local disk, although we currently keep everything locally for fast access. If a layer is needed that isn't found locally, it is fetched from Cloud -Storage and stored in local disk. - -# Terms used in layered repository - -- Relish - one PostgreSQL relation or similarly treated file. -- Segment - one slice of a Relish that is stored in a LayeredTimeline. -- Layer - specific version of a relish Segment in a range of LSNs. +Storage and stored in local disk. L0 and L1 files are both uploaded to Cloud +Storage. # Layer map -The LayerMap tracks what layers exist for all the relishes in a timeline. - -LayerMap consists of two data structures: -- segs - All the layers keyed by segment tag -- open_layers - data structure that hold all open layers ordered by oldest_pending_lsn for quick access during checkpointing. oldest_pending_lsn is the LSN of the oldest page version stored in this layer. - -All operations that update InMemory Layers should update both structures to keep them up-to-date. - -- LayeredTimeline - implements Timeline interface. - -All methods of LayeredTimeline are aware of its ancestors and return data taking them into account. -TODO: Are there any exceptions to this? -For example, timeline.list_rels(lsn) will return all segments that are visible in this timeline at the LSN, -including ones that were not modified in this timeline and thus don't have a layer in the timeline's LayerMap. +The LayerMap tracks what layers exist in a timeline. +Currently, the layer map is just a resizeable array (Vec). On a GetPage@LSN or +other read request, the layer map scans through the array to find the right layer +that contains the data for the requested page. The read-code in LayeredTimeline +is aware of the ancestor, and returns data from the ancestor timeline if it's +not found on the current timeline. # Different kinds of layers @@ -92,11 +84,11 @@ To avoid OOM errors, InMemory layers can be spilled to disk into ephemeral file. TODO: Clarify the difference between Closed, Historic and Frozen. There are two kinds of OnDisk layers: -- ImageLayer represents an image or a snapshot of a 10 MB relish segment, at one particular LSN. -- DeltaLayer represents a collection of WAL records or page images in a range of LSNs, for one - relish segment. - -Dropped segments are always represented on disk by DeltaLayer. +- ImageLayer represents a snapshot of all the keys in a particular range, at one + particular LSN. Any keys that are not present in the ImageLayer are known not + to exist at that LSN. +- DeltaLayer represents a collection of WAL records or page images in a range of + LSNs, for a range of keys. # Layer life cycle @@ -109,71 +101,71 @@ layer or a delta layer, it is a valid end bound. An image layer represents snapshot at one LSN, so end_lsn is always the snapshot LSN + 1 Every layer starts its life as an Open In-Memory layer. When the page server -receives the first WAL record for a segment, it creates a new In-Memory layer -for it, and puts it to the layer map. Later, the layer is old enough, its -contents are written to disk, as On-Disk layers. This process is called -"evicting" a layer. +receives the first WAL record for a timeline, it creates a new In-Memory layer +for it, and puts it to the layer map. Later, when the layer becomes full, its +contents are written to disk, as an on-disk layers. -Layer eviction is a two-step process: First, the layer is marked as closed, so -that it no longer accepts new WAL records, and the layer map is updated -accordingly. If a new WAL record for that segment arrives after this step, a new -Open layer is created to hold it. After this first step, the layer is a Closed +Flushing a layer is a two-step process: First, the layer is marked as closed, so +that it no longer accepts new WAL records, and a new in-memory layer is created +to hold any WAL after that point. After this first step, the layer is a Closed InMemory state. This first step is called "freezing" the layer. -In the second step, new Delta and Image layers are created, containing all the -data in the Frozen InMemory layer. When the new layers are ready, the original -frozen layer is replaced with the new layers in the layer map, and the original -frozen layer is dropped, releasing the memory. +In the second step, a new Delta layers is created, containing all the data from +the Frozen InMemory layer. When it has been created and flushed to disk, the +original frozen layer is replaced with the new layers in the layer map, and the +original frozen layer is dropped, releasing the memory. # Layer files (On-disk layers) -The files are called "layer files". Each layer file corresponds -to one RELISH_SEG_SIZE slice of a PostgreSQL relation fork or -non-rel file in a range of LSNs. The layer files -for each timeline are stored in the timeline's subdirectory under +The files are called "layer files". Each layer file covers a range of keys, and +a range of LSNs (or a single LSN, in case of image layers). You can think of it +as a rectangle in the two-dimensional key-LSN space. The layer files for each +timeline are stored in the timeline's subdirectory under .zenith/tenants//timelines. -There are two kind of layer file: base images, and deltas. A base -image file contains a layer of a segment as it was at one LSN, -whereas a delta file contains modifications to a segment - mostly in -the form of WAL records - in a range of LSN +There are two kind of layer files: images, and delta layers. An image file +contains a snapshot of all keys at a particular LSN, whereas a delta file +contains modifications to a segment - mostly in the form of WAL records - in a +range of LSN. -base image file: +image file: - rel______ + 000000067F000032BE0000400000000070B6-000000067F000032BE0000400000000080B6__00000000346BC568 + start key end key LSN + +The first parts define the key range that the layer covers. See +pgdatadir_mapping.rs for how the key space is used. The last part is the LSN. delta file: - rel_______ +Delta files are named similarly, but they cover a range of LSNs: -For example: + 000000067F000032BE0000400000000020B6-000000067F000032BE0000400000000030B6__000000578C6B29-0000000057A50051 + start key end key start LSN end LSN - rel_1663_13990_2609_0_10_000000000169C348 - rel_1663_13990_2609_0_10_000000000169C348_0000000001702000 +A delta file contains all the key-values in the key-range that were updated in +the LSN range. If a key has not been modified, there is no trace of it in the +delta layer. -In addition to the relations, with "rel_*" prefix, we use the same -format for storing various smaller files from the PostgreSQL data -directory. They will use different suffixes and the naming scheme up -to the LSNs vary. The Zenith source code uses the term "relish" to -mean "a relation, or other file that's treated like a relation in the -storage" For example, a base image of a CLOG segment would be named -like this: - pg_xact_0000_0_00000000198B06B0 +A delta layer file can cover a part of the overall key space, as in the previous +example, or the whole key range like this: -There is no difference in how the relation and non-relation files are -managed, except that the first part of file names is different. -Internally, the relations and non-relation files that are managed in -the versioned store are together called "relishes". + 000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__000000578C6B29-0000000057A50051 -If a file has been dropped, the last layer file for it is created -with the _DROPPED suffix, e.g. - - rel_1663_13990_2609_0_10_000000000169C348_0000000001702000_DROPPED +A file that covers the whole key range is called a L0 file (Level 0), while a +file that covers only part of the key range is called a L1 file. The "level" of +a file is not explicitly stored anywhere, you can only distinguish them by +looking at the key range that a file covers. The read-path doesn't need to +treat L0 and L1 files any differently. ## Notation used in this document +FIXME: This is somewhat obsolete, the layer files cover a key-range rather than +a particular relation nowadays. However, the description on how you find a page +version, and how branching and GC works is still valid. + The full path of a delta file looks like this: .zenith/tenants/941ddc8604413b88b3d208bddf90396c/timelines/4af489b06af8eed9e27a841775616962/rel_1663_13990_2609_0_10_000000000169C348_0000000001702000 diff --git a/pageserver/src/layered_repository/blob_io.rs b/pageserver/src/layered_repository/blob_io.rs new file mode 100644 index 0000000000..aa90bbd0cf --- /dev/null +++ b/pageserver/src/layered_repository/blob_io.rs @@ -0,0 +1,139 @@ +//! +//! Functions for reading and writing variable-sized "blobs". +//! +//! Each blob begins with a 4-byte length, followed by the actual data. +//! +use crate::layered_repository::block_io::{BlockCursor, BlockReader}; +use crate::page_cache::PAGE_SZ; +use std::cmp::min; +use std::io::Error; + +/// For reading +pub trait BlobCursor { + /// Read a blob into a new buffer. + fn read_blob(&mut self, offset: u64) -> Result, std::io::Error> { + let mut buf = Vec::new(); + self.read_blob_into_buf(offset, &mut buf)?; + Ok(buf) + } + + /// Read blob into the given buffer. Any previous contents in the buffer + /// are overwritten. + fn read_blob_into_buf( + &mut self, + offset: u64, + dstbuf: &mut Vec, + ) -> Result<(), std::io::Error>; +} + +impl<'a, R> BlobCursor for BlockCursor +where + R: BlockReader, +{ + fn read_blob_into_buf( + &mut self, + offset: u64, + dstbuf: &mut Vec, + ) -> Result<(), std::io::Error> { + let mut blknum = (offset / PAGE_SZ as u64) as u32; + let mut off = (offset % PAGE_SZ as u64) as usize; + + let mut buf = self.read_blk(blknum)?; + + // read length + let mut len_buf = [0u8; 4]; + let thislen = PAGE_SZ - off; + if thislen < 4 { + // it is split across two pages + len_buf[..thislen].copy_from_slice(&buf[off..PAGE_SZ]); + blknum += 1; + buf = self.read_blk(blknum)?; + len_buf[thislen..].copy_from_slice(&buf[0..4 - thislen]); + off = 4 - thislen; + } else { + len_buf.copy_from_slice(&buf[off..off + 4]); + off += 4; + } + let len = u32::from_ne_bytes(len_buf) as usize; + + dstbuf.clear(); + + // Read the payload + let mut remain = len; + while remain > 0 { + let mut page_remain = PAGE_SZ - off; + if page_remain == 0 { + // continue on next page + blknum += 1; + buf = self.read_blk(blknum)?; + off = 0; + page_remain = PAGE_SZ; + } + let this_blk_len = min(remain, page_remain); + dstbuf.extend_from_slice(&buf[off..off + this_blk_len]); + remain -= this_blk_len; + off += this_blk_len; + } + Ok(()) + } +} + +/// +/// Abstract trait for a data sink that you can write blobs to. +/// +pub trait BlobWriter { + /// Write a blob of data. Returns the offset that it was written to, + /// which can be used to retrieve the data later. + fn write_blob(&mut self, srcbuf: &[u8]) -> Result; +} + +/// +/// An implementation of BlobWriter to write blobs to anything that +/// implements std::io::Write. +/// +pub struct WriteBlobWriter +where + W: std::io::Write, +{ + inner: W, + offset: u64, +} + +impl WriteBlobWriter +where + W: std::io::Write, +{ + pub fn new(inner: W, start_offset: u64) -> Self { + WriteBlobWriter { + inner, + offset: start_offset, + } + } + + pub fn size(&self) -> u64 { + self.offset + } + + /// Access the underlying Write object. + /// + /// NOTE: WriteBlobWriter keeps track of the current write offset. If + /// you write something directly to the inner Write object, it makes the + /// internally tracked 'offset' to go out of sync. So don't do that. + pub fn into_inner(self) -> W { + self.inner + } +} + +impl BlobWriter for WriteBlobWriter +where + W: std::io::Write, +{ + fn write_blob(&mut self, srcbuf: &[u8]) -> Result { + let offset = self.offset; + self.inner + .write_all(&((srcbuf.len()) as u32).to_ne_bytes())?; + self.inner.write_all(srcbuf)?; + self.offset += 4 + srcbuf.len() as u64; + Ok(offset) + } +} diff --git a/pageserver/src/layered_repository/block_io.rs b/pageserver/src/layered_repository/block_io.rs new file mode 100644 index 0000000000..2eba0aa403 --- /dev/null +++ b/pageserver/src/layered_repository/block_io.rs @@ -0,0 +1,219 @@ +//! +//! Low-level Block-oriented I/O functions +//! + +use crate::page_cache; +use crate::page_cache::{ReadBufResult, PAGE_SZ}; +use bytes::Bytes; +use lazy_static::lazy_static; +use std::ops::{Deref, DerefMut}; +use std::os::unix::fs::FileExt; +use std::sync::atomic::AtomicU64; + +/// This is implemented by anything that can read 8 kB (PAGE_SZ) +/// blocks, using the page cache +/// +/// There are currently two implementations: EphemeralFile, and FileBlockReader +/// below. +pub trait BlockReader { + type BlockLease: Deref + 'static; + + /// + /// Read a block. Returns a "lease" object that can be used to + /// access to the contents of the page. (For the page cache, the + /// lease object represents a lock on the buffer.) + /// + fn read_blk(&self, blknum: u32) -> Result; + + /// + /// Create a new "cursor" for reading from this reader. + /// + /// A cursor caches the last accessed page, allowing for faster + /// access if the same block is accessed repeatedly. + fn block_cursor(&self) -> BlockCursor<&Self> + where + Self: Sized, + { + BlockCursor::new(self) + } +} + +impl BlockReader for &B +where + B: BlockReader, +{ + type BlockLease = B::BlockLease; + + fn read_blk(&self, blknum: u32) -> Result { + (*self).read_blk(blknum) + } +} + +/// +/// A "cursor" for efficiently reading multiple pages from a BlockReader +/// +/// A cursor caches the last accessed page, allowing for faster access if the +/// same block is accessed repeatedly. +/// +/// You can access the last page with `*cursor`. 'read_blk' returns 'self', so +/// that in many cases you can use a BlockCursor as a drop-in replacement for +/// the underlying BlockReader. For example: +/// +/// ```no_run +/// # use pageserver::layered_repository::block_io::{BlockReader, FileBlockReader}; +/// # let reader: FileBlockReader = todo!(); +/// let cursor = reader.block_cursor(); +/// let buf = cursor.read_blk(1); +/// // do stuff with 'buf' +/// let buf = cursor.read_blk(2); +/// // do stuff with 'buf' +/// ``` +/// +pub struct BlockCursor +where + R: BlockReader, +{ + reader: R, + /// last accessed page + cache: Option<(u32, R::BlockLease)>, +} + +impl BlockCursor +where + R: BlockReader, +{ + pub fn new(reader: R) -> Self { + BlockCursor { + reader, + cache: None, + } + } + + pub fn read_blk(&mut self, blknum: u32) -> Result<&Self, std::io::Error> { + // Fast return if this is the same block as before + if let Some((cached_blk, _buf)) = &self.cache { + if *cached_blk == blknum { + return Ok(self); + } + } + + // Read the block from the underlying reader, and cache it + self.cache = None; + let buf = self.reader.read_blk(blknum)?; + self.cache = Some((blknum, buf)); + + Ok(self) + } +} + +impl Deref for BlockCursor +where + R: BlockReader, +{ + type Target = [u8; PAGE_SZ]; + + fn deref(&self) -> &::Target { + &self.cache.as_ref().unwrap().1 + } +} + +lazy_static! { + static ref NEXT_ID: AtomicU64 = AtomicU64::new(1); +} + +/// An adapter for reading a (virtual) file using the page cache. +/// +/// The file is assumed to be immutable. This doesn't provide any functions +/// for modifying the file, nor for invalidating the cache if it is modified. +pub struct FileBlockReader { + pub file: F, + + /// Unique ID of this file, used as key in the page cache. + file_id: u64, +} + +impl FileBlockReader +where + F: FileExt, +{ + pub fn new(file: F) -> Self { + let file_id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + + FileBlockReader { file_id, file } + } + + /// Read a page from the underlying file into given buffer. + fn fill_buffer(&self, buf: &mut [u8], blkno: u32) -> Result<(), std::io::Error> { + assert!(buf.len() == PAGE_SZ); + self.file.read_exact_at(buf, blkno as u64 * PAGE_SZ as u64) + } +} + +impl BlockReader for FileBlockReader +where + F: FileExt, +{ + type BlockLease = page_cache::PageReadGuard<'static>; + + fn read_blk(&self, blknum: u32) -> Result { + // Look up the right page + let cache = page_cache::get(); + loop { + match cache.read_immutable_buf(self.file_id, blknum) { + ReadBufResult::Found(guard) => break Ok(guard), + ReadBufResult::NotFound(mut write_guard) => { + // Read the page from disk into the buffer + self.fill_buffer(write_guard.deref_mut(), blknum)?; + write_guard.mark_valid(); + + // Swap for read lock + continue; + } + }; + } + } +} + +/// +/// Trait for block-oriented output +/// +pub trait BlockWriter { + /// + /// Write a page to the underlying storage. + /// + /// 'buf' must be of size PAGE_SZ. Returns the block number the page was + /// written to. + /// + fn write_blk(&mut self, buf: Bytes) -> Result; +} + +/// +/// A simple in-memory buffer of blocks. +/// +pub struct BlockBuf { + pub blocks: Vec, +} +impl BlockWriter for BlockBuf { + fn write_blk(&mut self, buf: Bytes) -> Result { + assert!(buf.len() == PAGE_SZ); + let blknum = self.blocks.len(); + self.blocks.push(buf); + tracing::info!("buffered block {}", blknum); + Ok(blknum as u32) + } +} + +impl BlockBuf { + pub fn new() -> Self { + BlockBuf { blocks: Vec::new() } + } + + pub fn size(&self) -> u64 { + (self.blocks.len() * PAGE_SZ) as u64 + } +} +impl Default for BlockBuf { + fn default() -> Self { + Self::new() + } +} diff --git a/pageserver/src/layered_repository/delta_layer.rs b/pageserver/src/layered_repository/delta_layer.rs index 56fd86b4c0..dd6b5d3afa 100644 --- a/pageserver/src/layered_repository/delta_layer.rs +++ b/pageserver/src/layered_repository/delta_layer.rs @@ -1,23 +1,14 @@ //! A DeltaLayer represents a collection of WAL records or page images in a range of //! LSNs, and in a range of Keys. It is stored on a file on disk. //! -//! Usually a delta layer only contains differences - in the form of WAL records against -//! a base LSN. However, if a segment is newly created, by creating a new relation or -//! extending an old one, there might be no base image. In that case, all the entries in -//! the delta layer must be page images or WAL records with the 'will_init' flag set, so -//! that they can be replayed without referring to an older page version. Also in some -//! circumstances, the predecessor layer might actually be another delta layer. That -//! can happen when you create a new branch in the middle of a delta layer, and the WAL -//! records on the new branch are put in a new delta layer. +//! Usually a delta layer only contains differences, in the form of WAL records +//! against a base LSN. However, if a relation extended or a whole new relation +//! is created, there would be no base for the new pages. The entries for them +//! must be page images or WAL records with the 'will_init' flag set, so that +//! they can be replayed without referring to an older page version. //! -//! When a delta file needs to be accessed, we slurp the 'index' metadata -//! into memory, into the DeltaLayerInner struct. See load() and unload() functions. -//! To access a particular value, we search `index` for the given key. -//! The byte offset in the index can be used to find the value in -//! VALUES_CHAPTER. -//! -//! On disk, the delta files are stored in timelines/ directory. -//! Currently, there are no subdirectories, and each delta file is named like this: +//! The delta files are stored in timelines/ directory. Currently, +//! there are no subdirectories, and each delta file is named like this: //! //! -__- page/WAL record -/// byte ranges in VALUES_CHAPTER -static INDEX_CHAPTER: u64 = 1; - -/// Page/WAL bytes - cannot be interpreted -/// without the page versions from the INDEX_CHAPTER -static VALUES_CHAPTER: u64 = 2; - -/// Contains the [`Summary`] struct -static SUMMARY_CHAPTER: u64 = 3; - +/// +/// Header stored in the beginning of the file +/// +/// After this comes the 'values' part, starting on block 1. After that, +/// the 'index' starts at the block indicated by 'index_start_blk' +/// #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] struct Summary { + /// Magic value to identify this as a zenith delta file. Always DELTA_FILE_MAGIC. + magic: u16, + format_version: u16, + tenantid: ZTenantId, timelineid: ZTimelineId, key_range: Range, lsn_range: Range, + + /// Block number where the 'index' part of the file begins. + index_start_blk: u32, + /// Block within the 'index', where the B-tree root page is stored + index_root_blk: u32, } impl From<&DeltaLayer> for Summary { fn from(layer: &DeltaLayer) -> Self { Self { + magic: DELTA_FILE_MAGIC, + format_version: STORAGE_FORMAT_VERSION, + tenantid: layer.tenantid, timelineid: layer.timelineid, key_range: layer.key_range.clone(), lsn_range: layer.lsn_range.clone(), + + index_start_blk: 0, + index_root_blk: 0, } } } +// Flag indicating that this version initialize the page +const WILL_INIT: u64 = 1; + +/// +/// Struct representing reference to BLOB in layers. Reference contains BLOB +/// offset, and for WAL records it also contains `will_init` flag. The flag +/// helps to determine the range of records that needs to be applied, without +/// reading/deserializing records themselves. +/// +#[derive(Debug, Serialize, Deserialize, Copy, Clone)] +struct BlobRef(u64); + +impl BlobRef { + pub fn will_init(&self) -> bool { + (self.0 & WILL_INIT) != 0 + } + + pub fn pos(&self) -> u64 { + self.0 >> 1 + } + + pub fn new(pos: u64, will_init: bool) -> BlobRef { + let mut blob_ref = pos << 1; + if will_init { + blob_ref |= WILL_INIT; + } + BlobRef(blob_ref) + } +} + +const DELTA_KEY_SIZE: usize = KEY_SIZE + 8; +struct DeltaKey([u8; DELTA_KEY_SIZE]); + +/// +/// This is the key of the B-tree index stored in the delta layer. It consists +/// of the serialized representation of a Key and LSN. +/// +impl DeltaKey { + fn from_slice(buf: &[u8]) -> Self { + let mut bytes: [u8; DELTA_KEY_SIZE] = [0u8; DELTA_KEY_SIZE]; + bytes.copy_from_slice(buf); + DeltaKey(bytes) + } + + fn from_key_lsn(key: &Key, lsn: Lsn) -> Self { + let mut bytes: [u8; DELTA_KEY_SIZE] = [0u8; DELTA_KEY_SIZE]; + key.write_to_byte_slice(&mut bytes[0..KEY_SIZE]); + bytes[KEY_SIZE..].copy_from_slice(&u64::to_be_bytes(lsn.0)); + DeltaKey(bytes) + } + + fn key(&self) -> Key { + Key::from_slice(&self.0) + } + + fn lsn(&self) -> Lsn { + Lsn(u64::from_be_bytes(self.0[KEY_SIZE..].try_into().unwrap())) + } + + fn extract_key_from_buf(buf: &[u8]) -> Key { + Key::from_slice(&buf[..KEY_SIZE]) + } + + fn extract_lsn_from_buf(buf: &[u8]) -> Lsn { + let mut lsn_buf = [0u8; 8]; + lsn_buf.copy_from_slice(&buf[KEY_SIZE..]); + Lsn(u64::from_be_bytes(lsn_buf)) + } +} + /// /// DeltaLayer is the in-memory data structure associated with an /// on-disk delta file. We keep a DeltaLayer in memory for each @@ -113,17 +184,15 @@ pub struct DeltaLayer { } pub struct DeltaLayerInner { - /// If false, the 'index' has not been loaded into memory yet. + /// If false, the fields below have not been loaded into memory yet. loaded: bool, - /// - /// All versions of all pages in the layer are kept here. - /// Indexed by block number and LSN. The value is an offset into the - /// chapter where the page version is stored. - /// - index: HashMap>, + // values copied from summary + index_start_blk: u32, + index_root_blk: u32, - book: Option>, + /// Reader object for reading blocks from the file. (None if not loaded yet) + file: Option>, } impl Layer for DeltaLayer { @@ -152,53 +221,55 @@ impl Layer for DeltaLayer { key: Key, lsn_range: Range, reconstruct_state: &mut ValueReconstructState, - ) -> Result { + ) -> anyhow::Result { let mut need_image = true; - assert!(self.key_range.contains(&key)); + ensure!(self.key_range.contains(&key)); { // Open the file and lock the metadata in memory let inner = self.load()?; - let values_reader = inner - .book - .as_ref() - .expect("should be loaded in load call above") - .chapter_reader(VALUES_CHAPTER)?; // Scan the page versions backwards, starting from `lsn`. - if let Some(vec_map) = inner.index.get(&key) { - let slice = vec_map.slice_range(lsn_range); - let mut size = 0usize; - let mut first_pos = 0u64; - for (_entry_lsn, blob_ref) in slice.iter().rev() { - size += blob_ref.size(); - first_pos = blob_ref.pos(); - if blob_ref.will_init() { + let file = inner.file.as_ref().unwrap(); + let tree_reader = DiskBtreeReader::<_, DELTA_KEY_SIZE>::new( + inner.index_start_blk, + inner.index_root_blk, + file, + ); + let search_key = DeltaKey::from_key_lsn(&key, Lsn(lsn_range.end.0 - 1)); + + let mut offsets: Vec<(Lsn, u64)> = Vec::new(); + + tree_reader.visit(&search_key.0, VisitDirection::Backwards, |key, value| { + let blob_ref = BlobRef(value); + if key[..KEY_SIZE] != search_key.0[..KEY_SIZE] { + return false; + } + let entry_lsn = DeltaKey::extract_lsn_from_buf(key); + offsets.push((entry_lsn, blob_ref.pos())); + + !blob_ref.will_init() + })?; + + // Ok, 'offsets' now contains the offsets of all the entries we need to read + let mut cursor = file.block_cursor(); + for (entry_lsn, pos) in offsets { + let buf = cursor.read_blob(pos)?; + let val = Value::des(&buf)?; + match val { + Value::Image(img) => { + reconstruct_state.img = Some((entry_lsn, img)); + need_image = false; break; } - } - if size != 0 { - let mut buf = vec![0u8; size]; - values_reader.read_exact_at(&mut buf, first_pos)?; - for (entry_lsn, blob_ref) in slice.iter().rev() { - let offs = (blob_ref.pos() - first_pos) as usize; - let val = Value::des(&buf[offs..offs + blob_ref.size()])?; - match val { - Value::Image(img) => { - reconstruct_state.img = Some((*entry_lsn, img)); - need_image = false; - break; - } - Value::WalRecord(rec) => { - let will_init = rec.will_init(); - reconstruct_state.records.push((*entry_lsn, rec)); - if will_init { - // This WAL record initializes the page, so no need to go further back - need_image = false; - break; - } - } + Value::WalRecord(rec) => { + let will_init = rec.will_init(); + reconstruct_state.records.push((entry_lsn, rec)); + if will_init { + // This WAL record initializes the page, so no need to go further back + need_image = false; + break; } } } @@ -215,7 +286,7 @@ impl Layer for DeltaLayer { } } - fn iter(&self) -> Box> + '_> { + fn iter<'a>(&'a self) -> Box> + 'a> { let inner = self.load().unwrap(); match DeltaValueIter::new(inner) { @@ -224,33 +295,6 @@ impl Layer for DeltaLayer { } } - /// - /// Release most of the memory used by this layer. If it's accessed again later, - /// it will need to be loaded back. - /// - fn unload(&self) -> Result<()> { - // FIXME: In debug mode, loading and unloading the index slows - // things down so much that you get timeout errors. At least - // with the test_parallel_copy test. So as an even more ad hoc - // stopgap fix for that, only unload every on average 10 - // checkpoint cycles. - use rand::RngCore; - if rand::thread_rng().next_u32() > (u32::MAX / 10) { - return Ok(()); - } - - if let Ok(mut inner) = self.inner.try_write() { - inner.index = HashMap::default(); - inner.loaded = false; - - // Note: we keep the Book open. Is that a good idea? The virtual file - // machinery has its own rules for closing the file descriptor if it's not - // needed, but the Book struct uses up some memory, too. - } - - Ok(()) - } - fn delete(&self) -> Result<()> { // delete underlying file fs::remove_file(self.path())?; @@ -266,7 +310,7 @@ impl Layer for DeltaLayer { } /// debugging function to print out the contents of the layer - fn dump(&self) -> Result<()> { + fn dump(&self, verbose: bool) -> Result<()> { println!( "----- delta layer for ten {} tli {} keys {}-{} lsn {}-{} ----", self.tenantid, @@ -277,44 +321,67 @@ impl Layer for DeltaLayer { self.lsn_range.end ); + if !verbose { + return Ok(()); + } + let inner = self.load()?; - let path = self.path(); - let file = std::fs::File::open(&path)?; - let book = Book::new(file)?; - let chapter = book.chapter_reader(VALUES_CHAPTER)?; + println!( + "index_start_blk: {}, root {}", + inner.index_start_blk, inner.index_root_blk + ); - let mut values: Vec<(&Key, &VecMap)> = inner.index.iter().collect(); - values.sort_by_key(|k| k.0); + let file = inner.file.as_ref().unwrap(); + let tree_reader = DiskBtreeReader::<_, DELTA_KEY_SIZE>::new( + inner.index_start_blk, + inner.index_root_blk, + file, + ); + + tree_reader.dump()?; + + let mut cursor = file.block_cursor(); + tree_reader.visit( + &[0u8; DELTA_KEY_SIZE], + VisitDirection::Forwards, + |delta_key, val| { + let blob_ref = BlobRef(val); + let key = DeltaKey::extract_key_from_buf(delta_key); + let lsn = DeltaKey::extract_lsn_from_buf(delta_key); - for (key, versions) in values { - for (lsn, blob_ref) in versions.as_slice() { let mut desc = String::new(); - let mut buf = vec![0u8; blob_ref.size()]; - chapter.read_exact_at(&mut buf, blob_ref.pos())?; - let val = Value::des(&buf); - - match val { - Ok(Value::Image(img)) => { - write!(&mut desc, " img {} bytes", img.len())?; - } - Ok(Value::WalRecord(rec)) => { - let wal_desc = walrecord::describe_wal_record(&rec); - write!( - &mut desc, - " rec {} bytes will_init: {} {}", - buf.len(), - rec.will_init(), - wal_desc - )?; + match cursor.read_blob(blob_ref.pos()) { + Ok(buf) => { + let val = Value::des(&buf); + match val { + Ok(Value::Image(img)) => { + write!(&mut desc, " img {} bytes", img.len()).unwrap(); + } + Ok(Value::WalRecord(rec)) => { + let wal_desc = walrecord::describe_wal_record(&rec); + write!( + &mut desc, + " rec {} bytes will_init: {} {}", + buf.len(), + rec.will_init(), + wal_desc + ) + .unwrap(); + } + Err(err) => { + write!(&mut desc, " DESERIALIZATION ERROR: {}", err).unwrap(); + } + } } Err(err) => { - write!(&mut desc, " DESERIALIZATION ERROR: {}", err)?; + write!(&mut desc, " READ ERROR: {}", err).unwrap(); } } println!(" key {} at {}: {}", key, lsn, desc); - } - } + true + }, + )?; Ok(()) } @@ -336,65 +403,81 @@ impl DeltaLayer { } /// - /// Load the contents of the file into memory + /// Open the underlying file and read the metadata into memory, if it's + /// not loaded already. /// fn load(&self) -> Result> { loop { - // quick exit if already loaded - { - let inner = self.inner.read().unwrap(); - - if inner.loaded { - return Ok(inner); - } - } - // need to upgrade to write lock - let mut inner = self.inner.write().unwrap(); - - let path = self.path(); - - // Open the file if it's not open already. - if inner.book.is_none() { - let file = VirtualFile::open(&path)?; - inner.book = Some(Book::new(file)?); - } - let book = inner.book.as_ref().unwrap(); - - match &self.path_or_conf { - PathOrConf::Conf(_) => { - let chapter = book.read_chapter(SUMMARY_CHAPTER)?; - let actual_summary = Summary::des(&chapter)?; - - let expected_summary = Summary::from(self); - - if actual_summary != expected_summary { - bail!("in-file summary does not match expected summary. actual = {:?} expected = {:?}", actual_summary, expected_summary); - } - } - PathOrConf::Path(path) => { - let actual_filename = Path::new(path.file_name().unwrap()); - let expected_filename = self.filename(); - - if actual_filename != expected_filename { - println!( - "warning: filename does not match what is expected from in-file summary" - ); - println!("actual: {:?}", actual_filename); - println!("expected: {:?}", expected_filename); - } - } + // Quick exit if already loaded + let inner = self.inner.read().unwrap(); + if inner.loaded { + return Ok(inner); } - let chapter = book.read_chapter(INDEX_CHAPTER)?; - let index = HashMap::des(&chapter)?; + // Need to open the file and load the metadata. Upgrade our lock to + // a write lock. (Or rather, release and re-lock in write mode.) + drop(inner); + let inner = self.inner.write().unwrap(); + if !inner.loaded { + self.load_inner(inner)?; + } else { + // Another thread loaded it while we were not holding the lock. + } - debug!("loaded from {}", &path.display()); - - inner.index = index; - inner.loaded = true; + // We now have the file open and loaded. There's no function to do + // that in the std library RwLock, so we have to release and re-lock + // in read mode. (To be precise, the lock guard was moved in the + // above call to `load_inner`, so it's already been released). And + // while we do that, another thread could unload again, so we have + // to re-check and retry if that happens. } } + fn load_inner(&self, mut inner: RwLockWriteGuard) -> Result<()> { + let path = self.path(); + + // Open the file if it's not open already. + if inner.file.is_none() { + let file = VirtualFile::open(&path) + .with_context(|| format!("Failed to open file '{}'", path.display()))?; + inner.file = Some(FileBlockReader::new(file)); + } + let file = inner.file.as_mut().unwrap(); + let summary_blk = file.read_blk(0)?; + let actual_summary = Summary::des_prefix(summary_blk.as_ref())?; + + match &self.path_or_conf { + PathOrConf::Conf(_) => { + let mut expected_summary = Summary::from(self); + expected_summary.index_start_blk = actual_summary.index_start_blk; + expected_summary.index_root_blk = actual_summary.index_root_blk; + if actual_summary != expected_summary { + bail!("in-file summary does not match expected summary. actual = {:?} expected = {:?}", actual_summary, expected_summary); + } + } + PathOrConf::Path(path) => { + let actual_filename = Path::new(path.file_name().unwrap()); + let expected_filename = self.filename(); + + if actual_filename != expected_filename { + println!( + "warning: filename does not match what is expected from in-file summary" + ); + println!("actual: {:?}", actual_filename); + println!("expected: {:?}", expected_filename); + } + } + } + + inner.index_start_blk = actual_summary.index_start_blk; + inner.index_root_blk = actual_summary.index_root_blk; + + debug!("loaded from {}", &path.display()); + + inner.loaded = true; + Ok(()) + } + /// Create a DeltaLayer struct representing an existing file on disk. pub fn new( conf: &'static PageServerConf, @@ -410,8 +493,9 @@ impl DeltaLayer { lsn_range: filename.lsn_range.clone(), inner: RwLock::new(DeltaLayerInner { loaded: false, - book: None, - index: HashMap::default(), + file: None, + index_start_blk: 0, + index_root_blk: 0, }), } } @@ -419,12 +503,14 @@ impl DeltaLayer { /// Create a DeltaLayer struct representing an existing file on disk. /// /// This variant is only used for debugging purposes, by the 'dump_layerfile' binary. - pub fn new_for_path(path: &Path, book: &Book) -> Result + pub fn new_for_path(path: &Path, file: F) -> Result where F: FileExt, { - let chapter = book.read_chapter(SUMMARY_CHAPTER)?; - let summary = Summary::des(&chapter)?; + let mut summary_buf = Vec::new(); + summary_buf.resize(PAGE_SZ, 0); + file.read_exact_at(&mut summary_buf, 0)?; + let summary = Summary::des_prefix(&summary_buf)?; Ok(DeltaLayer { path_or_conf: PathOrConf::Path(path.to_path_buf()), @@ -434,8 +520,9 @@ impl DeltaLayer { lsn_range: summary.lsn_range, inner: RwLock::new(DeltaLayerInner { loaded: false, - book: None, - index: HashMap::default(), + file: None, + index_start_blk: 0, + index_root_blk: 0, }), }) } @@ -478,10 +565,9 @@ pub struct DeltaLayerWriter { key_start: Key, lsn_range: Range, - index: HashMap>, + tree: DiskBtreeBuilder, - values_writer: ChapterWriter>, - end_offset: u64, + blob_writer: WriteBlobWriter>, } impl DeltaLayerWriter { @@ -495,25 +581,27 @@ impl DeltaLayerWriter { key_start: Key, lsn_range: Range, ) -> Result { - // Create the file + // Create the file initially with a temporary filename. We don't know + // the end key yet, so we cannot form the final filename yet. We will + // rename it when we're done. // // Note: This overwrites any existing file. There shouldn't be any. // FIXME: throw an error instead? - let path = conf.timeline_path(&timelineid, &tenantid).join(format!( "{}-XXX__{:016X}-{:016X}.temp", key_start, u64::from(lsn_range.start), u64::from(lsn_range.end) )); - info!("temp deltalayer path {}", path.display()); - let file = VirtualFile::create(&path)?; + let mut file = VirtualFile::create(&path)?; + // make room for the header block + file.seek(SeekFrom::Start(PAGE_SZ as u64))?; let buf_writer = BufWriter::new(file); - let book = BookWriter::new(buf_writer, DELTA_FILE_MAGIC)?; + let blob_writer = WriteBlobWriter::new(buf_writer, PAGE_SZ as u64); - // Open the page-versions chapter for writing. The calls to - // `put_value` will use this to write the contents. - let values_writer = book.new_chapter(VALUES_CHAPTER); + // Initialize the b-tree index builder + let block_buf = BlockBuf::new(); + let tree_builder = DiskBtreeBuilder::new(block_buf); Ok(DeltaLayerWriter { conf, @@ -522,9 +610,8 @@ impl DeltaLayerWriter { tenantid, key_start, lsn_range, - index: HashMap::new(), - values_writer, - end_offset: 0, + tree: tree_builder, + blob_writer, }) } @@ -534,65 +621,56 @@ impl DeltaLayerWriter { /// The values must be appended in key, lsn order. /// pub fn put_value(&mut self, key: Key, lsn: Lsn, val: Value) -> Result<()> { - //info!("DELTA: key {} at {} on {}", key, lsn, self.path.display()); assert!(self.lsn_range.start <= lsn); - // Remember the offset and size metadata. The metadata is written - // to a separate chapter, in `finish`. - let off = self.end_offset; - let buf = Value::ser(&val)?; - let len = buf.len(); - self.values_writer.write_all(&buf)?; - self.end_offset += len as u64; - let vec_map = self.index.entry(key).or_default(); - let blob_ref = BlobRef::new(off, len, val.will_init()); - let old = vec_map.append_or_update_last(lsn, blob_ref).unwrap().0; - if old.is_some() { - // We already had an entry for this LSN. That's odd.. - bail!( - "Value for {} at {} already exists in delta layer being built", - key, - lsn - ); - } + + let off = self.blob_writer.write_blob(&Value::ser(&val)?)?; + + let blob_ref = BlobRef::new(off, val.will_init()); + + let delta_key = DeltaKey::from_key_lsn(&key, lsn); + self.tree.append(&delta_key.0, blob_ref.0)?; Ok(()) } pub fn size(&self) -> u64 { - self.end_offset + self.blob_writer.size() + self.tree.borrow_writer().size() } /// /// Finish writing the delta layer. /// - /// 'seg_sizes' is a list of size changes to store with the actual data. - /// - pub fn finish(self, key_end: Key) -> Result { - // Close the values chapter - let book = self.values_writer.close()?; + pub fn finish(self, key_end: Key) -> anyhow::Result { + let index_start_blk = + ((self.blob_writer.size() + PAGE_SZ as u64 - 1) / PAGE_SZ as u64) as u32; + + let buf_writer = self.blob_writer.into_inner(); + let mut file = buf_writer.into_inner()?; // Write out the index - let mut chapter = book.new_chapter(INDEX_CHAPTER); - let buf = HashMap::ser(&self.index)?; - chapter.write_all(&buf)?; - let book = chapter.close()?; + let (index_root_blk, block_buf) = self.tree.finish()?; + file.seek(SeekFrom::Start(index_start_blk as u64 * PAGE_SZ as u64))?; + for buf in block_buf.blocks { + file.write_all(buf.as_ref())?; + } - let mut chapter = book.new_chapter(SUMMARY_CHAPTER); + // Fill in the summary on blk 0 let summary = Summary { + magic: DELTA_FILE_MAGIC, + format_version: STORAGE_FORMAT_VERSION, tenantid: self.tenantid, timelineid: self.timelineid, key_range: self.key_start..key_end, lsn_range: self.lsn_range.clone(), + index_start_blk, + index_root_blk, }; - Summary::ser_into(&summary, &mut chapter)?; - let book = chapter.close()?; - - // This flushes the underlying 'buf_writer'. - book.close()?; + file.seek(SeekFrom::Start(0))?; + Summary::ser_into(&summary, &mut file)?; // Note: Because we opened the file in write-only mode, we cannot // reuse the same VirtualFile for reading later. That's why we don't - // set inner.book here. The first read will have to re-open it. + // set inner.file here. The first read will have to re-open it. let layer = DeltaLayer { path_or_conf: PathOrConf::Conf(self.conf), tenantid: self.tenantid, @@ -601,8 +679,9 @@ impl DeltaLayerWriter { lsn_range: self.lsn_range.clone(), inner: RwLock::new(DeltaLayerInner { loaded: false, - index: HashMap::new(), - book: None, + file: None, + index_start_blk, + index_root_blk, }), }; @@ -625,22 +704,6 @@ impl DeltaLayerWriter { Ok(layer) } - - pub fn abort(self) { - match self.values_writer.close() { - Ok(book) => { - if let Err(err) = book.close() { - error!("error while closing delta layer file: {}", err); - } - } - Err(err) => { - error!("error while closing chapter writer: {}", err); - } - } - if let Err(err) = std::fs::remove_file(self.path) { - error!("error removing unfinished delta layer file: {}", err); - } - } } /// @@ -650,13 +713,23 @@ impl DeltaLayerWriter { /// That takes up quite a lot of memory. Should do this in a more streaming /// fashion. /// -struct DeltaValueIter { - all_offsets: Vec<(Key, Lsn, BlobRef)>, +struct DeltaValueIter<'a> { + all_offsets: Vec<(DeltaKey, BlobRef)>, next_idx: usize, - data: Vec, + reader: BlockCursor>, } -impl Iterator for DeltaValueIter { +struct Adapter<'a>(RwLockReadGuard<'a, DeltaLayerInner>); + +impl<'a> BlockReader for Adapter<'a> { + type BlockLease = PageReadGuard<'static>; + + fn read_blk(&self, blknum: u32) -> Result { + self.0.file.as_ref().unwrap().read_blk(blknum) + } +} + +impl<'a> Iterator for DeltaValueIter<'a> { type Item = Result<(Key, Lsn, Value)>; fn next(&mut self) -> Option { @@ -664,40 +737,43 @@ impl Iterator for DeltaValueIter { } } -impl DeltaValueIter { - fn new(inner: RwLockReadGuard) -> Result { - let mut index: Vec<(&Key, &VecMap)> = inner.index.iter().collect(); - index.sort_by_key(|x| x.0); +impl<'a> DeltaValueIter<'a> { + fn new(inner: RwLockReadGuard<'a, DeltaLayerInner>) -> Result { + let file = inner.file.as_ref().unwrap(); + let tree_reader = DiskBtreeReader::<_, DELTA_KEY_SIZE>::new( + inner.index_start_blk, + inner.index_root_blk, + file, + ); - let mut all_offsets: Vec<(Key, Lsn, BlobRef)> = Vec::new(); - for (key, vec_map) in index.iter() { - for (lsn, blob_ref) in vec_map.as_slice().iter() { - all_offsets.push((**key, *lsn, *blob_ref)); - } - } + let mut all_offsets: Vec<(DeltaKey, BlobRef)> = Vec::new(); + tree_reader.visit( + &[0u8; DELTA_KEY_SIZE], + VisitDirection::Forwards, + |key, value| { + all_offsets.push((DeltaKey::from_slice(key), BlobRef(value))); + true + }, + )?; - let values_reader = inner - .book - .as_ref() - .expect("should be loaded in load call above") - .chapter_reader(VALUES_CHAPTER)?; - let file_size = values_reader.len() as usize; - let mut layer = DeltaValueIter { + let iter = DeltaValueIter { all_offsets, next_idx: 0, - data: vec![0u8; file_size], + reader: BlockCursor::new(Adapter(inner)), }; - values_reader.read_exact_at(&mut layer.data, 0)?; - Ok(layer) + Ok(iter) } fn next_res(&mut self) -> Result> { if self.next_idx < self.all_offsets.len() { - let (key, lsn, blob_ref) = self.all_offsets[self.next_idx]; - let offs = blob_ref.pos() as usize; - let size = blob_ref.size(); - let val = Value::des(&self.data[offs..offs + size])?; + let (delta_key, blob_ref) = &self.all_offsets[self.next_idx]; + + let key = delta_key.key(); + let lsn = delta_key.lsn(); + + let buf = self.reader.read_blob(blob_ref.pos())?; + let val = Value::des(&buf)?; self.next_idx += 1; Ok(Some((key, lsn, val))) } else { diff --git a/pageserver/src/layered_repository/disk_btree.rs b/pageserver/src/layered_repository/disk_btree.rs new file mode 100644 index 0000000000..7a9fe6f2b7 --- /dev/null +++ b/pageserver/src/layered_repository/disk_btree.rs @@ -0,0 +1,979 @@ +//! +//! Simple on-disk B-tree implementation +//! +//! This is used as the index structure within image and delta layers +//! +//! Features: +//! - Fixed-width keys +//! - Fixed-width values (VALUE_SZ) +//! - The tree is created in a bulk operation. Insert/deletion after creation +//! is not suppported +//! - page-oriented +//! +//! TODO: +//! - better errors (e.g. with thiserror?) +//! - maybe something like an Adaptive Radix Tree would be more efficient? +//! - the values stored by image and delta layers are offsets into the file, +//! and they are in monotonically increasing order. Prefix compression would +//! be very useful for them, too. +//! - An Iterator interface would be more convenient for the callers than the +//! 'visit' function +//! +use anyhow; +use byteorder::{ReadBytesExt, BE}; +use bytes::{BufMut, Bytes, BytesMut}; +use hex; +use std::cmp::Ordering; + +use crate::layered_repository::block_io::{BlockReader, BlockWriter}; + +// The maximum size of a value stored in the B-tree. 5 bytes is enough currently. +pub const VALUE_SZ: usize = 5; +pub const MAX_VALUE: u64 = 0x007f_ffff_ffff; + +#[allow(dead_code)] +pub const PAGE_SZ: usize = 8192; + +#[derive(Clone, Copy, Debug)] +struct Value([u8; VALUE_SZ]); + +impl Value { + fn from_slice(slice: &[u8]) -> Value { + let mut b = [0u8; VALUE_SZ]; + b.copy_from_slice(slice); + Value(b) + } + + fn from_u64(x: u64) -> Value { + assert!(x <= 0x007f_ffff_ffff); + Value([ + (x >> 32) as u8, + (x >> 24) as u8, + (x >> 16) as u8, + (x >> 8) as u8, + x as u8, + ]) + } + + fn from_blknum(x: u32) -> Value { + Value([ + 0x80, + (x >> 24) as u8, + (x >> 16) as u8, + (x >> 8) as u8, + x as u8, + ]) + } + + #[allow(dead_code)] + fn is_offset(self) -> bool { + self.0[0] & 0x80 != 0 + } + + fn to_u64(self) -> u64 { + let b = &self.0; + (b[0] as u64) << 32 + | (b[1] as u64) << 24 + | (b[2] as u64) << 16 + | (b[3] as u64) << 8 + | b[4] as u64 + } + + fn to_blknum(self) -> u32 { + let b = &self.0; + assert!(b[0] == 0x80); + (b[1] as u32) << 24 | (b[2] as u32) << 16 | (b[3] as u32) << 8 | b[4] as u32 + } +} + +/// This is the on-disk representation. +struct OnDiskNode<'a, const L: usize> { + // Fixed-width fields + num_children: u16, + level: u8, + prefix_len: u8, + suffix_len: u8, + + // Variable-length fields. These are stored on-disk after the fixed-width + // fields, in this order. In the in-memory representation, these point to + // the right parts in the page buffer. + prefix: &'a [u8], + keys: &'a [u8], + values: &'a [u8], +} + +impl<'a, const L: usize> OnDiskNode<'a, L> { + /// + /// Interpret a PAGE_SZ page as a node. + /// + fn deparse(buf: &[u8]) -> OnDiskNode { + let mut cursor = std::io::Cursor::new(buf); + let num_children = cursor.read_u16::().unwrap(); + let level = cursor.read_u8().unwrap(); + let prefix_len = cursor.read_u8().unwrap(); + let suffix_len = cursor.read_u8().unwrap(); + + let mut off = cursor.position(); + let prefix_off = off as usize; + off += prefix_len as u64; + + let keys_off = off as usize; + let keys_len = num_children as usize * suffix_len as usize; + off += keys_len as u64; + + let values_off = off as usize; + let values_len = num_children as usize * VALUE_SZ as usize; + //off += values_len as u64; + + let prefix = &buf[prefix_off..prefix_off + prefix_len as usize]; + let keys = &buf[keys_off..keys_off + keys_len]; + let values = &buf[values_off..values_off + values_len]; + + OnDiskNode { + num_children, + level, + prefix_len, + suffix_len, + prefix, + keys, + values, + } + } + + /// + /// Read a value at 'idx' + /// + fn value(&self, idx: usize) -> Value { + let value_off = idx * VALUE_SZ; + let value_slice = &self.values[value_off..value_off + VALUE_SZ]; + Value::from_slice(value_slice) + } + + fn binary_search(&self, search_key: &[u8; L], keybuf: &mut [u8]) -> Result { + let mut size = self.num_children as usize; + let mut low = 0; + let mut high = size; + while low < high { + let mid = low + size / 2; + + let key_off = mid as usize * self.suffix_len as usize; + let suffix = &self.keys[key_off..key_off + self.suffix_len as usize]; + // Does this match? + keybuf[self.prefix_len as usize..].copy_from_slice(suffix); + + let cmp = keybuf[..].cmp(search_key); + + if cmp == Ordering::Less { + low = mid + 1; + } else if cmp == Ordering::Greater { + high = mid; + } else { + return Ok(mid); + } + size = high - low; + } + Err(low) + } +} + +/// +/// Public reader object, to search the tree. +/// +pub struct DiskBtreeReader +where + R: BlockReader, +{ + start_blk: u32, + root_blk: u32, + reader: R, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum VisitDirection { + Forwards, + Backwards, +} + +impl DiskBtreeReader +where + R: BlockReader, +{ + pub fn new(start_blk: u32, root_blk: u32, reader: R) -> Self { + DiskBtreeReader { + start_blk, + root_blk, + reader, + } + } + + /// + /// Read the value for given key. Returns the value, or None if it doesn't exist. + /// + pub fn get(&self, search_key: &[u8; L]) -> anyhow::Result> { + let mut result: Option = None; + self.visit(search_key, VisitDirection::Forwards, |key, value| { + if key == search_key { + result = Some(value); + } + false + })?; + Ok(result) + } + + /// + /// Scan the tree, starting from 'search_key', in the given direction. 'visitor' + /// will be called for every key >= 'search_key' (or <= 'search_key', if scanning + /// backwards) + /// + pub fn visit( + &self, + search_key: &[u8; L], + dir: VisitDirection, + mut visitor: V, + ) -> anyhow::Result + where + V: FnMut(&[u8], u64) -> bool, + { + self.search_recurse(self.root_blk, search_key, dir, &mut visitor) + } + + fn search_recurse( + &self, + node_blknum: u32, + search_key: &[u8; L], + dir: VisitDirection, + visitor: &mut V, + ) -> anyhow::Result + where + V: FnMut(&[u8], u64) -> bool, + { + // Locate the node. + let blk = self.reader.read_blk(self.start_blk + node_blknum)?; + + // Search all entries on this node + self.search_node(blk.as_ref(), search_key, dir, visitor) + } + + fn search_node( + &self, + node_buf: &[u8], + search_key: &[u8; L], + dir: VisitDirection, + visitor: &mut V, + ) -> anyhow::Result + where + V: FnMut(&[u8], u64) -> bool, + { + let node = OnDiskNode::deparse(node_buf); + let prefix_len = node.prefix_len as usize; + let suffix_len = node.suffix_len as usize; + + assert!(node.num_children > 0); + + let mut keybuf = Vec::new(); + keybuf.extend(node.prefix); + keybuf.resize(prefix_len + suffix_len, 0); + + if dir == VisitDirection::Forwards { + // Locate the first match + let mut idx = match node.binary_search(search_key, keybuf.as_mut_slice()) { + Ok(idx) => idx, + Err(idx) => { + if node.level == 0 { + // Imagine that the node contains the following keys: + // + // 1 + // 3 <-- idx + // 5 + // + // If the search key is '2' and there is exact match, + // the binary search would return the index of key + // '3'. That's cool, '3' is the first key to return. + idx + } else { + // This is an internal page, so each key represents a lower + // bound for what's in the child page. If there is no exact + // match, we have to return the *previous* entry. + // + // 1 <-- return this + // 3 <-- idx + // 5 + idx.saturating_sub(1) + } + } + }; + // idx points to the first match now. Keep going from there + let mut key_off = idx * suffix_len; + while idx < node.num_children as usize { + let suffix = &node.keys[key_off..key_off + suffix_len]; + keybuf[prefix_len..].copy_from_slice(suffix); + let value = node.value(idx as usize); + #[allow(clippy::collapsible_if)] + if node.level == 0 { + // leaf + if !visitor(&keybuf, value.to_u64()) { + return Ok(false); + } + } else { + #[allow(clippy::collapsible_if)] + if !self.search_recurse(value.to_blknum(), search_key, dir, visitor)? { + return Ok(false); + } + } + idx += 1; + key_off += suffix_len; + } + } else { + let mut idx = match node.binary_search(search_key, keybuf.as_mut_slice()) { + Ok(idx) => { + // Exact match. That's the first entry to return, and walk + // backwards from there. (The loop below starts from 'idx - + // 1', so add one here to compensate.) + idx + 1 + } + Err(idx) => { + // No exact match. The binary search returned the index of the + // first key that's > search_key. Back off by one, and walk + // backwards from there. (The loop below starts from idx - 1, + // so we don't need to subtract one here) + idx + } + }; + + // idx points to the first match + 1 now. Keep going from there. + let mut key_off = idx * suffix_len; + while idx > 0 { + idx -= 1; + key_off -= suffix_len; + let suffix = &node.keys[key_off..key_off + suffix_len]; + keybuf[prefix_len..].copy_from_slice(suffix); + let value = node.value(idx as usize); + #[allow(clippy::collapsible_if)] + if node.level == 0 { + // leaf + if !visitor(&keybuf, value.to_u64()) { + return Ok(false); + } + } else { + #[allow(clippy::collapsible_if)] + if !self.search_recurse(value.to_blknum(), search_key, dir, visitor)? { + return Ok(false); + } + } + if idx == 0 { + break; + } + } + } + Ok(true) + } + + #[allow(dead_code)] + pub fn dump(&self) -> anyhow::Result<()> { + self.dump_recurse(self.root_blk, &[], 0) + } + + fn dump_recurse(&self, blknum: u32, path: &[u8], depth: usize) -> anyhow::Result<()> { + let blk = self.reader.read_blk(self.start_blk + blknum)?; + let buf: &[u8] = blk.as_ref(); + + let node = OnDiskNode::::deparse(buf); + + print!("{:indent$}", "", indent = depth * 2); + println!( + "blk #{}: path {}: prefix {}, suffix_len {}", + blknum, + hex::encode(path), + hex::encode(node.prefix), + node.suffix_len + ); + + let mut idx = 0; + let mut key_off = 0; + while idx < node.num_children { + let key = &node.keys[key_off..key_off + node.suffix_len as usize]; + let val = node.value(idx as usize); + print!("{:indent$}", "", indent = depth * 2 + 2); + println!("{}: {}", hex::encode(key), hex::encode(val.0)); + + if node.level > 0 { + let child_path = [path, node.prefix].concat(); + self.dump_recurse(val.to_blknum(), &child_path, depth + 1)?; + } + idx += 1; + key_off += node.suffix_len as usize; + } + Ok(()) + } +} + +/// +/// Public builder object, for creating a new tree. +/// +/// Usage: Create a builder object by calling 'new', load all the data into the +/// tree by calling 'append' for each key-value pair, and then call 'finish' +/// +/// 'L' is the key length in bytes +pub struct DiskBtreeBuilder +where + W: BlockWriter, +{ + writer: W, + + /// + /// stack[0] is the current root page, stack.last() is the leaf. + /// + stack: Vec>, + + /// Last key that was appended to the tree. Used to sanity check that append + /// is called in increasing key order. + last_key: Option<[u8; L]>, +} + +impl DiskBtreeBuilder +where + W: BlockWriter, +{ + pub fn new(writer: W) -> Self { + DiskBtreeBuilder { + writer, + last_key: None, + stack: vec![BuildNode::new(0)], + } + } + + pub fn append(&mut self, key: &[u8; L], value: u64) -> Result<(), anyhow::Error> { + assert!(value <= MAX_VALUE); + if let Some(last_key) = &self.last_key { + assert!(key > last_key, "unsorted input"); + } + self.last_key = Some(*key); + + Ok(self.append_internal(key, Value::from_u64(value))?) + } + + fn append_internal(&mut self, key: &[u8; L], value: Value) -> Result<(), std::io::Error> { + // Try to append to the current leaf buffer + let last = self.stack.last_mut().unwrap(); + let level = last.level; + if last.push(key, value) { + return Ok(()); + } + + // It did not fit. Try to compress, and it it succeeds to make some room + // on the node, try appending to it again. + #[allow(clippy::collapsible_if)] + if last.compress() { + if last.push(key, value) { + return Ok(()); + } + } + + // Could not append to the current leaf. Flush it and create a new one. + self.flush_node()?; + + // Replace the node we flushed with an empty one and append the new + // key to it. + let mut last = BuildNode::new(level); + if !last.push(key, value) { + panic!("could not push to new leaf node"); + } + self.stack.push(last); + + Ok(()) + } + + fn flush_node(&mut self) -> Result<(), std::io::Error> { + let last = self.stack.pop().unwrap(); + let buf = last.pack(); + let downlink_key = last.first_key(); + let downlink_ptr = self.writer.write_blk(buf)?; + + // Append the downlink to the parent + if self.stack.is_empty() { + self.stack.push(BuildNode::new(last.level + 1)); + } + self.append_internal(&downlink_key, Value::from_blknum(downlink_ptr))?; + + Ok(()) + } + + /// + /// Flushes everything to disk, and returns the block number of the root page. + /// The caller must store the root block number "out-of-band", and pass it + /// to the DiskBtreeReader::new() when you want to read the tree again. + /// (In the image and delta layers, it is stored in the beginning of the file, + /// in the summary header) + /// + pub fn finish(mut self) -> Result<(u32, W), std::io::Error> { + // flush all levels, except the root. + while self.stack.len() > 1 { + self.flush_node()?; + } + + let root = self.stack.first().unwrap(); + let buf = root.pack(); + let root_blknum = self.writer.write_blk(buf)?; + + Ok((root_blknum, self.writer)) + } + + pub fn borrow_writer(&self) -> &W { + &self.writer + } +} + +/// +/// BuildNode represesnts an incomplete page that we are appending to. +/// +#[derive(Clone, Debug)] +struct BuildNode { + num_children: u16, + level: u8, + prefix: Vec, + suffix_len: usize, + + keys: Vec, + values: Vec, + + size: usize, // physical size of this node, if it was written to disk like this +} + +const NODE_SIZE: usize = PAGE_SZ; + +const NODE_HDR_SIZE: usize = 2 + 1 + 1 + 1; + +impl BuildNode { + fn new(level: u8) -> Self { + BuildNode { + num_children: 0, + level, + prefix: Vec::new(), + suffix_len: 0, + keys: Vec::new(), + values: Vec::new(), + size: NODE_HDR_SIZE, + } + } + + /// Try to append a key-value pair to this node. Returns 'true' on + /// success, 'false' if the page was full or the key was + /// incompatible with the prefix of the existing keys. + fn push(&mut self, key: &[u8; L], value: Value) -> bool { + // If we have already performed prefix-compression on the page, + // check that the incoming key has the same prefix. + if self.num_children > 0 { + // does the prefix allow it? + if !key.starts_with(&self.prefix) { + return false; + } + } else { + self.suffix_len = key.len(); + } + + // Is the node too full? + if self.size + self.suffix_len + VALUE_SZ >= NODE_SIZE { + return false; + } + + // All clear + self.num_children += 1; + self.keys.extend(&key[self.prefix.len()..]); + self.values.extend(value.0); + + assert!(self.keys.len() == self.num_children as usize * self.suffix_len as usize); + assert!(self.values.len() == self.num_children as usize * VALUE_SZ); + + self.size += self.suffix_len + VALUE_SZ; + + true + } + + /// + /// Perform prefix-compression. + /// + /// Returns 'true' on success, 'false' if no compression was possible. + /// + fn compress(&mut self) -> bool { + let first_suffix = self.first_suffix(); + let last_suffix = self.last_suffix(); + + // Find the common prefix among all keys + let mut prefix_len = 0; + while prefix_len < self.suffix_len { + if first_suffix[prefix_len] != last_suffix[prefix_len] { + break; + } + prefix_len += 1; + } + if prefix_len == 0 { + return false; + } + + // Can compress. Rewrite the keys without the common prefix. + self.prefix.extend(&self.keys[..prefix_len]); + + let mut new_keys = Vec::new(); + let mut key_off = 0; + while key_off < self.keys.len() { + let next_key_off = key_off + self.suffix_len; + new_keys.extend(&self.keys[key_off + prefix_len..next_key_off]); + key_off = next_key_off; + } + self.keys = new_keys; + self.suffix_len -= prefix_len; + + self.size -= prefix_len * self.num_children as usize; + self.size += prefix_len; + + assert!(self.keys.len() == self.num_children as usize * self.suffix_len as usize); + assert!(self.values.len() == self.num_children as usize * VALUE_SZ); + + true + } + + /// + /// Serialize the node to on-disk format. + /// + fn pack(&self) -> Bytes { + assert!(self.keys.len() == self.num_children as usize * self.suffix_len as usize); + assert!(self.values.len() == self.num_children as usize * VALUE_SZ); + assert!(self.num_children > 0); + + let mut buf = BytesMut::new(); + + buf.put_u16(self.num_children); + buf.put_u8(self.level); + buf.put_u8(self.prefix.len() as u8); + buf.put_u8(self.suffix_len as u8); + buf.put(&self.prefix[..]); + buf.put(&self.keys[..]); + buf.put(&self.values[..]); + + assert!(buf.len() == self.size); + + assert!(buf.len() <= PAGE_SZ); + buf.resize(PAGE_SZ, 0); + buf.freeze() + } + + fn first_suffix(&self) -> &[u8] { + &self.keys[..self.suffix_len] + } + fn last_suffix(&self) -> &[u8] { + &self.keys[self.keys.len() - self.suffix_len..] + } + + /// Return the full first key of the page, including the prefix + fn first_key(&self) -> [u8; L] { + let mut key = [0u8; L]; + key[..self.prefix.len()].copy_from_slice(&self.prefix); + key[self.prefix.len()..].copy_from_slice(self.first_suffix()); + key + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::Rng; + use std::collections::BTreeMap; + use std::sync::atomic::{AtomicUsize, Ordering}; + + #[derive(Clone, Default)] + struct TestDisk { + blocks: Vec, + } + impl TestDisk { + fn new() -> Self { + Self::default() + } + } + impl BlockReader for TestDisk { + type BlockLease = std::rc::Rc<[u8; PAGE_SZ]>; + + fn read_blk(&self, blknum: u32) -> Result { + let mut buf = [0u8; PAGE_SZ]; + buf.copy_from_slice(&self.blocks[blknum as usize]); + Ok(std::rc::Rc::new(buf)) + } + } + impl BlockWriter for &mut TestDisk { + fn write_blk(&mut self, buf: Bytes) -> Result { + let blknum = self.blocks.len(); + self.blocks.push(buf); + Ok(blknum as u32) + } + } + + #[test] + fn basic() -> anyhow::Result<()> { + let mut disk = TestDisk::new(); + let mut writer = DiskBtreeBuilder::<_, 6>::new(&mut disk); + + let all_keys: Vec<&[u8; 6]> = vec![ + b"xaaaaa", b"xaaaba", b"xaaaca", b"xabaaa", b"xababa", b"xabaca", b"xabada", b"xabadb", + ]; + let all_data: Vec<(&[u8; 6], u64)> = all_keys + .iter() + .enumerate() + .map(|(idx, key)| (*key, idx as u64)) + .collect(); + for (key, val) in all_data.iter() { + writer.append(key, *val)?; + } + + let (root_offset, _writer) = writer.finish()?; + + let reader = DiskBtreeReader::new(0, root_offset, disk); + + reader.dump()?; + + // Test the `get` function on all the keys. + for (key, val) in all_data.iter() { + assert_eq!(reader.get(key)?, Some(*val)); + } + // And on some keys that don't exist + assert_eq!(reader.get(b"aaaaaa")?, None); + assert_eq!(reader.get(b"zzzzzz")?, None); + assert_eq!(reader.get(b"xaaabx")?, None); + + // Test search with `visit` function + let search_key = b"xabaaa"; + let expected: Vec<(Vec, u64)> = all_data + .iter() + .filter(|(key, _value)| key[..] >= search_key[..]) + .map(|(key, value)| (key.to_vec(), *value)) + .collect(); + + let mut data = Vec::new(); + reader.visit(search_key, VisitDirection::Forwards, |key, value| { + data.push((key.to_vec(), value)); + true + })?; + assert_eq!(data, expected); + + // Test a backwards scan + let mut expected: Vec<(Vec, u64)> = all_data + .iter() + .filter(|(key, _value)| key[..] <= search_key[..]) + .map(|(key, value)| (key.to_vec(), *value)) + .collect(); + expected.reverse(); + let mut data = Vec::new(); + reader.visit(search_key, VisitDirection::Backwards, |key, value| { + data.push((key.to_vec(), value)); + true + })?; + assert_eq!(data, expected); + + // Backward scan where nothing matches + reader.visit(b"aaaaaa", VisitDirection::Backwards, |key, value| { + panic!("found unexpected key {}: {}", hex::encode(key), value); + })?; + + // Full scan + let expected: Vec<(Vec, u64)> = all_data + .iter() + .map(|(key, value)| (key.to_vec(), *value)) + .collect(); + let mut data = Vec::new(); + reader.visit(&[0u8; 6], VisitDirection::Forwards, |key, value| { + data.push((key.to_vec(), value)); + true + })?; + assert_eq!(data, expected); + + Ok(()) + } + + #[test] + fn lots_of_keys() -> anyhow::Result<()> { + let mut disk = TestDisk::new(); + let mut writer = DiskBtreeBuilder::<_, 8>::new(&mut disk); + + const NUM_KEYS: u64 = 1000; + + let mut all_data: BTreeMap = BTreeMap::new(); + + for idx in 0..NUM_KEYS { + let key_int: u64 = 1 + idx * 2; + let key = u64::to_be_bytes(key_int); + writer.append(&key, idx)?; + + all_data.insert(key_int, idx); + } + + let (root_offset, _writer) = writer.finish()?; + + let reader = DiskBtreeReader::new(0, root_offset, disk); + + reader.dump()?; + + use std::sync::Mutex; + + let result = Mutex::new(Vec::new()); + let limit: AtomicUsize = AtomicUsize::new(10); + let take_ten = |key: &[u8], value: u64| { + let mut keybuf = [0u8; 8]; + keybuf.copy_from_slice(key); + let key_int = u64::from_be_bytes(keybuf); + + let mut result = result.lock().unwrap(); + result.push((key_int, value)); + + // keep going until we have 10 matches + result.len() < limit.load(Ordering::Relaxed) + }; + + for search_key_int in 0..(NUM_KEYS * 2 + 10) { + let search_key = u64::to_be_bytes(search_key_int); + assert_eq!( + reader.get(&search_key)?, + all_data.get(&search_key_int).cloned() + ); + + // Test a forward scan starting with this key + result.lock().unwrap().clear(); + reader.visit(&search_key, VisitDirection::Forwards, take_ten)?; + let expected = all_data + .range(search_key_int..) + .take(10) + .map(|(&key, &val)| (key, val)) + .collect::>(); + assert_eq!(*result.lock().unwrap(), expected); + + // And a backwards scan + result.lock().unwrap().clear(); + reader.visit(&search_key, VisitDirection::Backwards, take_ten)?; + let expected = all_data + .range(..=search_key_int) + .rev() + .take(10) + .map(|(&key, &val)| (key, val)) + .collect::>(); + assert_eq!(*result.lock().unwrap(), expected); + } + + // full scan + let search_key = u64::to_be_bytes(0); + limit.store(usize::MAX, Ordering::Relaxed); + result.lock().unwrap().clear(); + reader.visit(&search_key, VisitDirection::Forwards, take_ten)?; + let expected = all_data + .iter() + .map(|(&key, &val)| (key, val)) + .collect::>(); + assert_eq!(*result.lock().unwrap(), expected); + + // full scan + let search_key = u64::to_be_bytes(u64::MAX); + limit.store(usize::MAX, Ordering::Relaxed); + result.lock().unwrap().clear(); + reader.visit(&search_key, VisitDirection::Backwards, take_ten)?; + let expected = all_data + .iter() + .rev() + .map(|(&key, &val)| (key, val)) + .collect::>(); + assert_eq!(*result.lock().unwrap(), expected); + + Ok(()) + } + + #[test] + fn random_data() -> anyhow::Result<()> { + // Generate random keys with exponential distribution, to + // exercise the prefix compression + const NUM_KEYS: usize = 100000; + let mut all_data: BTreeMap = BTreeMap::new(); + for idx in 0..NUM_KEYS { + let u: f64 = rand::thread_rng().gen_range(0.0..1.0); + let t = -(f64::ln(u)); + let key_int = (t * 1000000.0) as u128; + + all_data.insert(key_int as u128, idx as u64); + } + + // Build a tree from it + let mut disk = TestDisk::new(); + let mut writer = DiskBtreeBuilder::<_, 16>::new(&mut disk); + + for (&key, &val) in all_data.iter() { + writer.append(&u128::to_be_bytes(key), val)?; + } + let (root_offset, _writer) = writer.finish()?; + + let reader = DiskBtreeReader::new(0, root_offset, disk); + + // Test get() operation on all the keys + for (&key, &val) in all_data.iter() { + let search_key = u128::to_be_bytes(key); + assert_eq!(reader.get(&search_key)?, Some(val)); + } + + // Test get() operations on random keys, most of which will not exist + for _ in 0..100000 { + let key_int = rand::thread_rng().gen::(); + let search_key = u128::to_be_bytes(key_int); + assert!(reader.get(&search_key)? == all_data.get(&key_int).cloned()); + } + + // Test boundary cases + assert!(reader.get(&u128::to_be_bytes(u128::MIN))? == all_data.get(&u128::MIN).cloned()); + assert!(reader.get(&u128::to_be_bytes(u128::MAX))? == all_data.get(&u128::MAX).cloned()); + + Ok(()) + } + + #[test] + #[should_panic(expected = "unsorted input")] + fn unsorted_input() { + let mut disk = TestDisk::new(); + let mut writer = DiskBtreeBuilder::<_, 2>::new(&mut disk); + + let _ = writer.append(b"ba", 1); + let _ = writer.append(b"bb", 2); + let _ = writer.append(b"aa", 3); + } + + /// + /// This test contains a particular data set, see disk_btree_test_data.rs + /// + #[test] + fn particular_data() -> anyhow::Result<()> { + // Build a tree from it + let mut disk = TestDisk::new(); + let mut writer = DiskBtreeBuilder::<_, 26>::new(&mut disk); + + for (key, val) in disk_btree_test_data::TEST_DATA { + writer.append(&key, val)?; + } + let (root_offset, writer) = writer.finish()?; + + println!("SIZE: {} blocks", writer.blocks.len()); + + let reader = DiskBtreeReader::new(0, root_offset, disk); + + // Test get() operation on all the keys + for (key, val) in disk_btree_test_data::TEST_DATA { + assert_eq!(reader.get(&key)?, Some(val)); + } + + // Test full scan + let mut count = 0; + reader.visit(&[0u8; 26], VisitDirection::Forwards, |_key, _value| { + count += 1; + true + })?; + assert_eq!(count, disk_btree_test_data::TEST_DATA.len()); + + reader.dump()?; + + Ok(()) + } +} + +#[cfg(test)] +#[path = "disk_btree_test_data.rs"] +mod disk_btree_test_data; diff --git a/pageserver/src/layered_repository/disk_btree_test_data.rs b/pageserver/src/layered_repository/disk_btree_test_data.rs new file mode 100644 index 0000000000..9462573f03 --- /dev/null +++ b/pageserver/src/layered_repository/disk_btree_test_data.rs @@ -0,0 +1,2013 @@ +use hex_literal::hex; + +/// Test data set for the 'particular_data' test in disk_btree.rs +/// +/// This test contains a particular data set, representing all the keys +/// generated by the 'test_random_updates' unit test. I extracted this while +/// trying to debug a failure in that test. The bug turned out to be +/// elsewhere, and I'm not sure if this is still useful, but keeping it for +/// now... Maybe it's a useful data set to show the typical key-values used +/// by a delta layer, for evaluating how well the prefix compression works. +#[rustfmt::skip] +pub static TEST_DATA: [([u8; 26], u64); 2000] = [ + (hex!("0122222222333333334444444455000000000000000000000010"), 0x004001), + (hex!("0122222222333333334444444455000000000000000000007cb0"), 0x0040a1), + (hex!("0122222222333333334444444455000000010000000000000020"), 0x004141), + (hex!("0122222222333333334444444455000000020000000000000030"), 0x0041e1), + (hex!("01222222223333333344444444550000000200000000000051a0"), 0x004281), + (hex!("0122222222333333334444444455000000030000000000000040"), 0x004321), + (hex!("0122222222333333334444444455000000030000000000006cf0"), 0x0043c1), + (hex!("0122222222333333334444444455000000030000000000007140"), 0x004461), + (hex!("0122222222333333334444444455000000040000000000000050"), 0x004501), + (hex!("01222222223333333344444444550000000400000000000047f0"), 0x0045a1), + (hex!("01222222223333333344444444550000000400000000000072b0"), 0x004641), + (hex!("0122222222333333334444444455000000050000000000000060"), 0x0046e1), + (hex!("0122222222333333334444444455000000050000000000005550"), 0x004781), + (hex!("0122222222333333334444444455000000060000000000000070"), 0x004821), + (hex!("01222222223333333344444444550000000600000000000044a0"), 0x0048c1), + (hex!("0122222222333333334444444455000000060000000000006870"), 0x004961), + (hex!("0122222222333333334444444455000000070000000000000080"), 0x004a01), + (hex!("0122222222333333334444444455000000080000000000000090"), 0x004aa1), + (hex!("0122222222333333334444444455000000080000000000004150"), 0x004b41), + (hex!("01222222223333333344444444550000000900000000000000a0"), 0x004be1), + (hex!("01222222223333333344444444550000000a00000000000000b0"), 0x004c81), + (hex!("01222222223333333344444444550000000a0000000000006680"), 0x004d21), + (hex!("01222222223333333344444444550000000b00000000000000c0"), 0x004dc1), + (hex!("01222222223333333344444444550000000b0000000000006230"), 0x004e61), + (hex!("01222222223333333344444444550000000c00000000000000d0"), 0x004f01), + (hex!("01222222223333333344444444550000000d00000000000000e0"), 0x004fa1), + (hex!("01222222223333333344444444550000000e00000000000000f0"), 0x005041), + (hex!("01222222223333333344444444550000000e0000000000006000"), 0x0050e1), + (hex!("01222222223333333344444444550000000f0000000000000100"), 0x005181), + (hex!("01222222223333333344444444550000000f00000000000053c0"), 0x005221), + (hex!("01222222223333333344444444550000000f0000000000006580"), 0x0052c1), + (hex!("0122222222333333334444444455000000100000000000000110"), 0x005361), + (hex!("01222222223333333344444444550000001000000000000046c0"), 0x005401), + (hex!("0122222222333333334444444455000000100000000000004e40"), 0x0054a1), + (hex!("0122222222333333334444444455000000110000000000000120"), 0x005541), + (hex!("0122222222333333334444444455000000120000000000000130"), 0x0055e1), + (hex!("01222222223333333344444444550000001200000000000066d0"), 0x005681), + (hex!("0122222222333333334444444455000000130000000000000140"), 0x005721), + (hex!("0122222222333333334444444455000000130000000000007710"), 0x0057c1), + (hex!("0122222222333333334444444455000000140000000000000150"), 0x005861), + (hex!("0122222222333333334444444455000000140000000000006c40"), 0x005901), + (hex!("0122222222333333334444444455000000150000000000000160"), 0x0059a1), + (hex!("0122222222333333334444444455000000150000000000005990"), 0x005a41), + (hex!("0122222222333333334444444455000000160000000000000170"), 0x005ae1), + (hex!("0122222222333333334444444455000000160000000000005530"), 0x005b81), + (hex!("0122222222333333334444444455000000170000000000000180"), 0x005c21), + (hex!("0122222222333333334444444455000000170000000000004290"), 0x005cc1), + (hex!("0122222222333333334444444455000000180000000000000190"), 0x005d61), + (hex!("01222222223333333344444444550000001800000000000051c0"), 0x005e01), + (hex!("01222222223333333344444444550000001900000000000001a0"), 0x005ea1), + (hex!("0122222222333333334444444455000000190000000000005420"), 0x005f41), + (hex!("0122222222333333334444444455000000190000000000005770"), 0x005fe1), + (hex!("01222222223333333344444444550000001900000000000079d0"), 0x006081), + (hex!("01222222223333333344444444550000001a00000000000001b0"), 0x006121), + (hex!("01222222223333333344444444550000001a0000000000006f70"), 0x0061c1), + (hex!("01222222223333333344444444550000001a0000000000007150"), 0x006261), + (hex!("01222222223333333344444444550000001b00000000000001c0"), 0x006301), + (hex!("01222222223333333344444444550000001b0000000000005070"), 0x0063a1), + (hex!("01222222223333333344444444550000001c00000000000001d0"), 0x006441), + (hex!("01222222223333333344444444550000001d00000000000001e0"), 0x0064e1), + (hex!("01222222223333333344444444550000001e00000000000001f0"), 0x006581), + (hex!("01222222223333333344444444550000001e0000000000005650"), 0x006621), + (hex!("01222222223333333344444444550000001f0000000000000200"), 0x0066c1), + (hex!("01222222223333333344444444550000001f0000000000006ca0"), 0x006761), + (hex!("0122222222333333334444444455000000200000000000000210"), 0x006801), + (hex!("0122222222333333334444444455000000200000000000005fc0"), 0x0068a1), + (hex!("0122222222333333334444444455000000210000000000000220"), 0x006941), + (hex!("0122222222333333334444444455000000210000000000006430"), 0x0069e1), + (hex!("0122222222333333334444444455000000220000000000000230"), 0x006a81), + (hex!("01222222223333333344444444550000002200000000000040e0"), 0x006b21), + (hex!("0122222222333333334444444455000000230000000000000240"), 0x006bc1), + (hex!("01222222223333333344444444550000002300000000000042d0"), 0x006c61), + (hex!("0122222222333333334444444455000000240000000000000250"), 0x006d01), + (hex!("0122222222333333334444444455000000250000000000000260"), 0x006da1), + (hex!("01222222223333333344444444550000002500000000000058c0"), 0x006e41), + (hex!("0122222222333333334444444455000000260000000000000270"), 0x006ee1), + (hex!("0122222222333333334444444455000000260000000000004020"), 0x006f81), + (hex!("0122222222333333334444444455000000270000000000000280"), 0x007021), + (hex!("0122222222333333334444444455000000280000000000000290"), 0x0070c1), + (hex!("0122222222333333334444444455000000280000000000007c00"), 0x007161), + (hex!("01222222223333333344444444550000002900000000000002a0"), 0x007201), + (hex!("01222222223333333344444444550000002a00000000000002b0"), 0x0072a1), + (hex!("01222222223333333344444444550000002b00000000000002c0"), 0x007341), + (hex!("01222222223333333344444444550000002c00000000000002d0"), 0x0073e1), + (hex!("01222222223333333344444444550000002c00000000000041b0"), 0x007481), + (hex!("01222222223333333344444444550000002c0000000000004c30"), 0x007521), + (hex!("01222222223333333344444444550000002d00000000000002e0"), 0x0075c1), + (hex!("01222222223333333344444444550000002d0000000000005e40"), 0x007661), + (hex!("01222222223333333344444444550000002d0000000000006990"), 0x007701), + (hex!("01222222223333333344444444550000002e00000000000002f0"), 0x0077a1), + (hex!("01222222223333333344444444550000002f0000000000000300"), 0x007841), + (hex!("01222222223333333344444444550000002f0000000000004a70"), 0x0078e1), + (hex!("01222222223333333344444444550000002f0000000000006b40"), 0x007981), + (hex!("0122222222333333334444444455000000300000000000000310"), 0x007a21), + (hex!("0122222222333333334444444455000000310000000000000320"), 0x007ac1), + (hex!("0122222222333333334444444455000000320000000000000330"), 0x007b61), + (hex!("01222222223333333344444444550000003200000000000041a0"), 0x007c01), + (hex!("0122222222333333334444444455000000320000000000007340"), 0x007ca1), + (hex!("0122222222333333334444444455000000320000000000007730"), 0x007d41), + (hex!("0122222222333333334444444455000000330000000000000340"), 0x007de1), + (hex!("01222222223333333344444444550000003300000000000055a0"), 0x007e81), + (hex!("0122222222333333334444444455000000340000000000000350"), 0x007f21), + (hex!("0122222222333333334444444455000000350000000000000360"), 0x007fc1), + (hex!("01222222223333333344444444550000003500000000000077a0"), 0x008061), + (hex!("0122222222333333334444444455000000360000000000000370"), 0x008101), + (hex!("0122222222333333334444444455000000370000000000000380"), 0x0081a1), + (hex!("0122222222333333334444444455000000380000000000000390"), 0x008241), + (hex!("01222222223333333344444444550000003900000000000003a0"), 0x0082e1), + (hex!("01222222223333333344444444550000003a00000000000003b0"), 0x008381), + (hex!("01222222223333333344444444550000003a00000000000071c0"), 0x008421), + (hex!("01222222223333333344444444550000003b00000000000003c0"), 0x0084c1), + (hex!("01222222223333333344444444550000003c00000000000003d0"), 0x008561), + (hex!("01222222223333333344444444550000003d00000000000003e0"), 0x008601), + (hex!("01222222223333333344444444550000003e00000000000003f0"), 0x0086a1), + (hex!("01222222223333333344444444550000003e00000000000062e0"), 0x008741), + (hex!("01222222223333333344444444550000003f0000000000000400"), 0x0087e1), + (hex!("0122222222333333334444444455000000400000000000000410"), 0x008881), + (hex!("0122222222333333334444444455000000400000000000004460"), 0x008921), + (hex!("0122222222333333334444444455000000400000000000005b90"), 0x0089c1), + (hex!("01222222223333333344444444550000004000000000000079b0"), 0x008a61), + (hex!("0122222222333333334444444455000000410000000000000420"), 0x008b01), + (hex!("0122222222333333334444444455000000420000000000000430"), 0x008ba1), + (hex!("0122222222333333334444444455000000420000000000005640"), 0x008c41), + (hex!("0122222222333333334444444455000000430000000000000440"), 0x008ce1), + (hex!("01222222223333333344444444550000004300000000000072a0"), 0x008d81), + (hex!("0122222222333333334444444455000000440000000000000450"), 0x008e21), + (hex!("0122222222333333334444444455000000450000000000000460"), 0x008ec1), + (hex!("0122222222333333334444444455000000450000000000005750"), 0x008f61), + (hex!("01222222223333333344444444550000004500000000000077b0"), 0x009001), + (hex!("0122222222333333334444444455000000460000000000000470"), 0x0090a1), + (hex!("0122222222333333334444444455000000470000000000000480"), 0x009141), + (hex!("0122222222333333334444444455000000480000000000000490"), 0x0091e1), + (hex!("01222222223333333344444444550000004800000000000069e0"), 0x009281), + (hex!("01222222223333333344444444550000004900000000000004a0"), 0x009321), + (hex!("0122222222333333334444444455000000490000000000007370"), 0x0093c1), + (hex!("01222222223333333344444444550000004a00000000000004b0"), 0x009461), + (hex!("01222222223333333344444444550000004a0000000000005cb0"), 0x009501), + (hex!("01222222223333333344444444550000004b00000000000004c0"), 0x0095a1), + (hex!("01222222223333333344444444550000004c00000000000004d0"), 0x009641), + (hex!("01222222223333333344444444550000004c0000000000004880"), 0x0096e1), + (hex!("01222222223333333344444444550000004c0000000000007a40"), 0x009781), + (hex!("01222222223333333344444444550000004d00000000000004e0"), 0x009821), + (hex!("01222222223333333344444444550000004d0000000000006390"), 0x0098c1), + (hex!("01222222223333333344444444550000004e00000000000004f0"), 0x009961), + (hex!("01222222223333333344444444550000004e0000000000004db0"), 0x009a01), + (hex!("01222222223333333344444444550000004f0000000000000500"), 0x009aa1), + (hex!("0122222222333333334444444455000000500000000000000510"), 0x009b41), + (hex!("0122222222333333334444444455000000510000000000000520"), 0x009be1), + (hex!("01222222223333333344444444550000005100000000000069c0"), 0x009c81), + (hex!("0122222222333333334444444455000000520000000000000530"), 0x009d21), + (hex!("0122222222333333334444444455000000520000000000006e60"), 0x009dc1), + (hex!("01222222223333333344444444550000005200000000000070c0"), 0x009e61), + (hex!("0122222222333333334444444455000000530000000000000540"), 0x009f01), + (hex!("0122222222333333334444444455000000530000000000005840"), 0x009fa1), + (hex!("0122222222333333334444444455000000540000000000000550"), 0x00a041), + (hex!("01222222223333333344444444550000005400000000000043e0"), 0x00a0e1), + (hex!("01222222223333333344444444550000005400000000000074e0"), 0x00a181), + (hex!("0122222222333333334444444455000000550000000000000560"), 0x00a221), + (hex!("0122222222333333334444444455000000550000000000003ee0"), 0x00a2c1), + (hex!("0122222222333333334444444455000000560000000000000570"), 0x00a361), + (hex!("0122222222333333334444444455000000570000000000000580"), 0x00a401), + (hex!("0122222222333333334444444455000000570000000000007030"), 0x00a4a1), + (hex!("0122222222333333334444444455000000580000000000000590"), 0x00a541), + (hex!("0122222222333333334444444455000000580000000000005340"), 0x00a5e1), + (hex!("01222222223333333344444444550000005800000000000059f0"), 0x00a681), + (hex!("0122222222333333334444444455000000580000000000006930"), 0x00a721), + (hex!("01222222223333333344444444550000005900000000000005a0"), 0x00a7c1), + (hex!("0122222222333333334444444455000000590000000000003f90"), 0x00a861), + (hex!("01222222223333333344444444550000005a00000000000005b0"), 0x00a901), + (hex!("01222222223333333344444444550000005b00000000000005c0"), 0x00a9a1), + (hex!("01222222223333333344444444550000005b00000000000062c0"), 0x00aa41), + (hex!("01222222223333333344444444550000005c00000000000005d0"), 0x00aae1), + (hex!("01222222223333333344444444550000005c0000000000005a70"), 0x00ab81), + (hex!("01222222223333333344444444550000005c0000000000005dd0"), 0x00ac21), + (hex!("01222222223333333344444444550000005d00000000000005e0"), 0x00acc1), + (hex!("01222222223333333344444444550000005d0000000000005730"), 0x00ad61), + (hex!("01222222223333333344444444550000005e00000000000005f0"), 0x00ae01), + (hex!("01222222223333333344444444550000005e0000000000004f40"), 0x00aea1), + (hex!("01222222223333333344444444550000005f0000000000000600"), 0x00af41), + (hex!("0122222222333333334444444455000000600000000000000610"), 0x00afe1), + (hex!("0122222222333333334444444455000000600000000000007c40"), 0x00b081), + (hex!("0122222222333333334444444455000000610000000000000620"), 0x00b121), + (hex!("0122222222333333334444444455000000610000000000007860"), 0x00b1c1), + (hex!("0122222222333333334444444455000000620000000000000630"), 0x00b261), + (hex!("0122222222333333334444444455000000620000000000005050"), 0x00b301), + (hex!("0122222222333333334444444455000000630000000000000640"), 0x00b3a1), + (hex!("0122222222333333334444444455000000640000000000000650"), 0x00b441), + (hex!("0122222222333333334444444455000000650000000000000660"), 0x00b4e1), + (hex!("0122222222333333334444444455000000650000000000005330"), 0x00b581), + (hex!("0122222222333333334444444455000000660000000000000670"), 0x00b621), + (hex!("0122222222333333334444444455000000660000000000004e20"), 0x00b6c1), + (hex!("0122222222333333334444444455000000660000000000005ee0"), 0x00b761), + (hex!("0122222222333333334444444455000000660000000000006360"), 0x00b801), + (hex!("0122222222333333334444444455000000670000000000000680"), 0x00b8a1), + (hex!("0122222222333333334444444455000000670000000000004040"), 0x00b941), + (hex!("0122222222333333334444444455000000680000000000000690"), 0x00b9e1), + (hex!("0122222222333333334444444455000000680000000000003f80"), 0x00ba81), + (hex!("01222222223333333344444444550000006800000000000041e0"), 0x00bb21), + (hex!("01222222223333333344444444550000006900000000000006a0"), 0x00bbc1), + (hex!("0122222222333333334444444455000000690000000000006080"), 0x00bc61), + (hex!("01222222223333333344444444550000006a00000000000006b0"), 0x00bd01), + (hex!("01222222223333333344444444550000006a00000000000042f0"), 0x00bda1), + (hex!("01222222223333333344444444550000006b00000000000006c0"), 0x00be41), + (hex!("01222222223333333344444444550000006b00000000000052f0"), 0x00bee1), + (hex!("01222222223333333344444444550000006b0000000000005980"), 0x00bf81), + (hex!("01222222223333333344444444550000006b0000000000006170"), 0x00c021), + (hex!("01222222223333333344444444550000006c00000000000006d0"), 0x00c0c1), + (hex!("01222222223333333344444444550000006d00000000000006e0"), 0x00c161), + (hex!("01222222223333333344444444550000006d0000000000006fb0"), 0x00c201), + (hex!("01222222223333333344444444550000006e00000000000006f0"), 0x00c2a1), + (hex!("01222222223333333344444444550000006e00000000000065b0"), 0x00c341), + (hex!("01222222223333333344444444550000006e0000000000007970"), 0x00c3e1), + (hex!("01222222223333333344444444550000006f0000000000000700"), 0x00c481), + (hex!("01222222223333333344444444550000006f0000000000005900"), 0x00c521), + (hex!("01222222223333333344444444550000006f0000000000006d90"), 0x00c5c1), + (hex!("0122222222333333334444444455000000700000000000000710"), 0x00c661), + (hex!("01222222223333333344444444550000007000000000000045c0"), 0x00c701), + (hex!("0122222222333333334444444455000000700000000000004d40"), 0x00c7a1), + (hex!("0122222222333333334444444455000000710000000000000720"), 0x00c841), + (hex!("0122222222333333334444444455000000710000000000004dc0"), 0x00c8e1), + (hex!("0122222222333333334444444455000000710000000000007550"), 0x00c981), + (hex!("0122222222333333334444444455000000720000000000000730"), 0x00ca21), + (hex!("0122222222333333334444444455000000720000000000003ec0"), 0x00cac1), + (hex!("01222222223333333344444444550000007200000000000045a0"), 0x00cb61), + (hex!("0122222222333333334444444455000000720000000000006770"), 0x00cc01), + (hex!("0122222222333333334444444455000000720000000000006bc0"), 0x00cca1), + (hex!("0122222222333333334444444455000000730000000000000740"), 0x00cd41), + (hex!("0122222222333333334444444455000000730000000000005250"), 0x00cde1), + (hex!("01222222223333333344444444550000007300000000000075f0"), 0x00ce81), + (hex!("0122222222333333334444444455000000740000000000000750"), 0x00cf21), + (hex!("0122222222333333334444444455000000740000000000003ff0"), 0x00cfc1), + (hex!("01222222223333333344444444550000007400000000000079e0"), 0x00d061), + (hex!("0122222222333333334444444455000000750000000000000760"), 0x00d101), + (hex!("0122222222333333334444444455000000750000000000004310"), 0x00d1a1), + (hex!("0122222222333333334444444455000000760000000000000770"), 0x00d241), + (hex!("0122222222333333334444444455000000770000000000000780"), 0x00d2e1), + (hex!("01222222223333333344444444550000007700000000000062f0"), 0x00d381), + (hex!("0122222222333333334444444455000000770000000000006940"), 0x00d421), + (hex!("0122222222333333334444444455000000780000000000000790"), 0x00d4c1), + (hex!("01222222223333333344444444550000007900000000000007a0"), 0x00d561), + (hex!("0122222222333333334444444455000000790000000000007af0"), 0x00d601), + (hex!("01222222223333333344444444550000007a00000000000007b0"), 0x00d6a1), + (hex!("01222222223333333344444444550000007b00000000000007c0"), 0x00d741), + (hex!("01222222223333333344444444550000007b00000000000067e0"), 0x00d7e1), + (hex!("01222222223333333344444444550000007b0000000000007890"), 0x00d881), + (hex!("01222222223333333344444444550000007c00000000000007d0"), 0x00d921), + (hex!("01222222223333333344444444550000007d00000000000007e0"), 0x00d9c1), + (hex!("01222222223333333344444444550000007e00000000000007f0"), 0x00da61), + (hex!("01222222223333333344444444550000007f0000000000000800"), 0x00db01), + (hex!("01222222223333333344444444550000007f0000000000005be0"), 0x00dba1), + (hex!("0122222222333333334444444455000000800000000000000810"), 0x00dc41), + (hex!("0122222222333333334444444455000000810000000000000820"), 0x00dce1), + (hex!("0122222222333333334444444455000000810000000000007190"), 0x00dd81), + (hex!("0122222222333333334444444455000000820000000000000830"), 0x00de21), + (hex!("0122222222333333334444444455000000820000000000004ab0"), 0x00dec1), + (hex!("0122222222333333334444444455000000830000000000000840"), 0x00df61), + (hex!("0122222222333333334444444455000000830000000000006720"), 0x00e001), + (hex!("0122222222333333334444444455000000840000000000000850"), 0x00e0a1), + (hex!("0122222222333333334444444455000000850000000000000860"), 0x00e141), + (hex!("01222222223333333344444444550000008500000000000054f0"), 0x00e1e1), + (hex!("0122222222333333334444444455000000850000000000007920"), 0x00e281), + (hex!("0122222222333333334444444455000000860000000000000870"), 0x00e321), + (hex!("01222222223333333344444444550000008600000000000060e0"), 0x00e3c1), + (hex!("0122222222333333334444444455000000860000000000006be0"), 0x00e461), + (hex!("0122222222333333334444444455000000870000000000000880"), 0x00e501), + (hex!("0122222222333333334444444455000000870000000000006820"), 0x00e5a1), + (hex!("0122222222333333334444444455000000880000000000000890"), 0x00e641), + (hex!("01222222223333333344444444550000008900000000000008a0"), 0x00e6e1), + (hex!("0122222222333333334444444455000000890000000000007c30"), 0x00e781), + (hex!("01222222223333333344444444550000008a00000000000008b0"), 0x00e821), + (hex!("01222222223333333344444444550000008b00000000000008c0"), 0x00e8c1), + (hex!("01222222223333333344444444550000008b0000000000005910"), 0x00e961), + (hex!("01222222223333333344444444550000008b0000000000006fe0"), 0x00ea01), + (hex!("01222222223333333344444444550000008c00000000000008d0"), 0x00eaa1), + (hex!("01222222223333333344444444550000008c0000000000006800"), 0x00eb41), + (hex!("01222222223333333344444444550000008d00000000000008e0"), 0x00ebe1), + (hex!("01222222223333333344444444550000008d0000000000005810"), 0x00ec81), + (hex!("01222222223333333344444444550000008d0000000000007c90"), 0x00ed21), + (hex!("01222222223333333344444444550000008e00000000000008f0"), 0x00edc1), + (hex!("01222222223333333344444444550000008e00000000000058f0"), 0x00ee61), + (hex!("01222222223333333344444444550000008f0000000000000900"), 0x00ef01), + (hex!("01222222223333333344444444550000008f0000000000005a30"), 0x00efa1), + (hex!("0122222222333333334444444455000000900000000000000910"), 0x00f041), + (hex!("0122222222333333334444444455000000900000000000006130"), 0x00f0e1), + (hex!("0122222222333333334444444455000000900000000000006550"), 0x00f181), + (hex!("0122222222333333334444444455000000910000000000000920"), 0x00f221), + (hex!("01222222223333333344444444550000009100000000000079f0"), 0x00f2c1), + (hex!("0122222222333333334444444455000000920000000000000930"), 0x00f361), + (hex!("0122222222333333334444444455000000920000000000005620"), 0x00f401), + (hex!("0122222222333333334444444455000000920000000000005e90"), 0x00f4a1), + (hex!("01222222223333333344444444550000009200000000000063d0"), 0x00f541), + (hex!("01222222223333333344444444550000009200000000000076c0"), 0x00f5e1), + (hex!("0122222222333333334444444455000000930000000000000940"), 0x00f681), + (hex!("01222222223333333344444444550000009300000000000044e0"), 0x00f721), + (hex!("0122222222333333334444444455000000940000000000000950"), 0x00f7c1), + (hex!("0122222222333333334444444455000000940000000000007a30"), 0x00f861), + (hex!("0122222222333333334444444455000000950000000000000960"), 0x00f901), + (hex!("0122222222333333334444444455000000950000000000007a70"), 0x00f9a1), + (hex!("0122222222333333334444444455000000960000000000000970"), 0x00fa41), + (hex!("0122222222333333334444444455000000970000000000000980"), 0x00fae1), + (hex!("0122222222333333334444444455000000970000000000007330"), 0x00fb81), + (hex!("0122222222333333334444444455000000980000000000000990"), 0x00fc21), + (hex!("0122222222333333334444444455000000980000000000005af0"), 0x00fcc1), + (hex!("0122222222333333334444444455000000980000000000007ae0"), 0x00fd61), + (hex!("01222222223333333344444444550000009900000000000009a0"), 0x00fe01), + (hex!("0122222222333333334444444455000000990000000000005160"), 0x00fea1), + (hex!("0122222222333333334444444455000000990000000000006850"), 0x00ff41), + (hex!("01222222223333333344444444550000009a00000000000009b0"), 0x00ffe1), + (hex!("01222222223333333344444444550000009b00000000000009c0"), 0x010081), + (hex!("01222222223333333344444444550000009b0000000000005010"), 0x010121), + (hex!("01222222223333333344444444550000009c00000000000009d0"), 0x0101c1), + (hex!("01222222223333333344444444550000009c00000000000042e0"), 0x010261), + (hex!("01222222223333333344444444550000009d00000000000009e0"), 0x010301), + (hex!("01222222223333333344444444550000009d00000000000057f0"), 0x0103a1), + (hex!("01222222223333333344444444550000009e00000000000009f0"), 0x010441), + (hex!("01222222223333333344444444550000009e0000000000004ef0"), 0x0104e1), + (hex!("01222222223333333344444444550000009f0000000000000a00"), 0x010581), + (hex!("01222222223333333344444444550000009f0000000000006110"), 0x010621), + (hex!("0122222222333333334444444455000000a00000000000000a10"), 0x0106c1), + (hex!("0122222222333333334444444455000000a10000000000000a20"), 0x010761), + (hex!("0122222222333333334444444455000000a100000000000040d0"), 0x010801), + (hex!("0122222222333333334444444455000000a10000000000007670"), 0x0108a1), + (hex!("0122222222333333334444444455000000a20000000000000a30"), 0x010941), + (hex!("0122222222333333334444444455000000a200000000000074d0"), 0x0109e1), + (hex!("0122222222333333334444444455000000a30000000000000a40"), 0x010a81), + (hex!("0122222222333333334444444455000000a30000000000004c90"), 0x010b21), + (hex!("0122222222333333334444444455000000a40000000000000a50"), 0x010bc1), + (hex!("0122222222333333334444444455000000a50000000000000a60"), 0x010c61), + (hex!("0122222222333333334444444455000000a60000000000000a70"), 0x010d01), + (hex!("0122222222333333334444444455000000a60000000000006d80"), 0x010da1), + (hex!("0122222222333333334444444455000000a60000000000007830"), 0x010e41), + (hex!("0122222222333333334444444455000000a70000000000000a80"), 0x010ee1), + (hex!("0122222222333333334444444455000000a700000000000064f0"), 0x010f81), + (hex!("0122222222333333334444444455000000a80000000000000a90"), 0x011021), + (hex!("0122222222333333334444444455000000a90000000000000aa0"), 0x0110c1), + (hex!("0122222222333333334444444455000000a90000000000005e30"), 0x011161), + (hex!("0122222222333333334444444455000000aa0000000000000ab0"), 0x011201), + (hex!("0122222222333333334444444455000000ab0000000000000ac0"), 0x0112a1), + (hex!("0122222222333333334444444455000000ac0000000000000ad0"), 0x011341), + (hex!("0122222222333333334444444455000000ac0000000000006d20"), 0x0113e1), + (hex!("0122222222333333334444444455000000ac0000000000007000"), 0x011481), + (hex!("0122222222333333334444444455000000ad0000000000000ae0"), 0x011521), + (hex!("0122222222333333334444444455000000ae0000000000000af0"), 0x0115c1), + (hex!("0122222222333333334444444455000000ae0000000000004a10"), 0x011661), + (hex!("0122222222333333334444444455000000af0000000000000b00"), 0x011701), + (hex!("0122222222333333334444444455000000af0000000000004e10"), 0x0117a1), + (hex!("0122222222333333334444444455000000b00000000000000b10"), 0x011841), + (hex!("0122222222333333334444444455000000b00000000000004280"), 0x0118e1), + (hex!("0122222222333333334444444455000000b000000000000077e0"), 0x011981), + (hex!("0122222222333333334444444455000000b10000000000000b20"), 0x011a21), + (hex!("0122222222333333334444444455000000b20000000000000b30"), 0x011ac1), + (hex!("0122222222333333334444444455000000b30000000000000b40"), 0x011b61), + (hex!("0122222222333333334444444455000000b30000000000004bc0"), 0x011c01), + (hex!("0122222222333333334444444455000000b40000000000000b50"), 0x011ca1), + (hex!("0122222222333333334444444455000000b50000000000000b60"), 0x011d41), + (hex!("0122222222333333334444444455000000b50000000000004fa0"), 0x011de1), + (hex!("0122222222333333334444444455000000b50000000000006a60"), 0x011e81), + (hex!("0122222222333333334444444455000000b60000000000000b70"), 0x011f21), + (hex!("0122222222333333334444444455000000b60000000000005630"), 0x011fc1), + (hex!("0122222222333333334444444455000000b70000000000000b80"), 0x012061), + (hex!("0122222222333333334444444455000000b80000000000000b90"), 0x012101), + (hex!("0122222222333333334444444455000000b80000000000006f80"), 0x0121a1), + (hex!("0122222222333333334444444455000000b90000000000000ba0"), 0x012241), + (hex!("0122222222333333334444444455000000ba0000000000000bb0"), 0x0122e1), + (hex!("0122222222333333334444444455000000bb0000000000000bc0"), 0x012381), + (hex!("0122222222333333334444444455000000bb00000000000047c0"), 0x012421), + (hex!("0122222222333333334444444455000000bb0000000000006060"), 0x0124c1), + (hex!("0122222222333333334444444455000000bc0000000000000bd0"), 0x012561), + (hex!("0122222222333333334444444455000000bd0000000000000be0"), 0x012601), + (hex!("0122222222333333334444444455000000bd0000000000004e80"), 0x0126a1), + (hex!("0122222222333333334444444455000000be0000000000000bf0"), 0x012741), + (hex!("0122222222333333334444444455000000bf0000000000000c00"), 0x0127e1), + (hex!("0122222222333333334444444455000000bf00000000000047a0"), 0x012881), + (hex!("0122222222333333334444444455000000bf0000000000006da0"), 0x012921), + (hex!("0122222222333333334444444455000000c00000000000000c10"), 0x0129c1), + (hex!("0122222222333333334444444455000000c10000000000000c20"), 0x012a61), + (hex!("0122222222333333334444444455000000c20000000000000c30"), 0x012b01), + (hex!("0122222222333333334444444455000000c20000000000004bd0"), 0x012ba1), + (hex!("0122222222333333334444444455000000c20000000000006ac0"), 0x012c41), + (hex!("0122222222333333334444444455000000c30000000000000c40"), 0x012ce1), + (hex!("0122222222333333334444444455000000c30000000000004660"), 0x012d81), + (hex!("0122222222333333334444444455000000c40000000000000c50"), 0x012e21), + (hex!("0122222222333333334444444455000000c50000000000000c60"), 0x012ec1), + (hex!("0122222222333333334444444455000000c60000000000000c70"), 0x012f61), + (hex!("0122222222333333334444444455000000c60000000000005880"), 0x013001), + (hex!("0122222222333333334444444455000000c60000000000006b70"), 0x0130a1), + (hex!("0122222222333333334444444455000000c70000000000000c80"), 0x013141), + (hex!("0122222222333333334444444455000000c80000000000000c90"), 0x0131e1), + (hex!("0122222222333333334444444455000000c80000000000005310"), 0x013281), + (hex!("0122222222333333334444444455000000c80000000000005db0"), 0x013321), + (hex!("0122222222333333334444444455000000c80000000000007040"), 0x0133c1), + (hex!("0122222222333333334444444455000000c80000000000007290"), 0x013461), + (hex!("0122222222333333334444444455000000c90000000000000ca0"), 0x013501), + (hex!("0122222222333333334444444455000000c90000000000004fe0"), 0x0135a1), + (hex!("0122222222333333334444444455000000ca0000000000000cb0"), 0x013641), + (hex!("0122222222333333334444444455000000ca0000000000006140"), 0x0136e1), + (hex!("0122222222333333334444444455000000ca0000000000007700"), 0x013781), + (hex!("0122222222333333334444444455000000cb0000000000000cc0"), 0x013821), + (hex!("0122222222333333334444444455000000cc0000000000000cd0"), 0x0138c1), + (hex!("0122222222333333334444444455000000cd0000000000000ce0"), 0x013961), + (hex!("0122222222333333334444444455000000cd0000000000003f20"), 0x013a01), + (hex!("0122222222333333334444444455000000cd00000000000040f0"), 0x013aa1), + (hex!("0122222222333333334444444455000000cd0000000000004ec0"), 0x013b41), + (hex!("0122222222333333334444444455000000ce0000000000000cf0"), 0x013be1), + (hex!("0122222222333333334444444455000000ce0000000000007200"), 0x013c81), + (hex!("0122222222333333334444444455000000cf0000000000000d00"), 0x013d21), + (hex!("0122222222333333334444444455000000cf00000000000046a0"), 0x013dc1), + (hex!("0122222222333333334444444455000000cf0000000000005960"), 0x013e61), + (hex!("0122222222333333334444444455000000d00000000000000d10"), 0x013f01), + (hex!("0122222222333333334444444455000000d00000000000005f30"), 0x013fa1), + (hex!("0122222222333333334444444455000000d10000000000000d20"), 0x014041), + (hex!("0122222222333333334444444455000000d10000000000007a00"), 0x0140e1), + (hex!("0122222222333333334444444455000000d20000000000000d30"), 0x014181), + (hex!("0122222222333333334444444455000000d30000000000000d40"), 0x014221), + (hex!("0122222222333333334444444455000000d40000000000000d50"), 0x0142c1), + (hex!("0122222222333333334444444455000000d50000000000000d60"), 0x014361), + (hex!("0122222222333333334444444455000000d50000000000004960"), 0x014401), + (hex!("0122222222333333334444444455000000d500000000000055d0"), 0x0144a1), + (hex!("0122222222333333334444444455000000d500000000000067d0"), 0x014541), + (hex!("0122222222333333334444444455000000d60000000000000d70"), 0x0145e1), + (hex!("0122222222333333334444444455000000d70000000000000d80"), 0x014681), + (hex!("0122222222333333334444444455000000d80000000000000d90"), 0x014721), + (hex!("0122222222333333334444444455000000d800000000000065f0"), 0x0147c1), + (hex!("0122222222333333334444444455000000d90000000000000da0"), 0x014861), + (hex!("0122222222333333334444444455000000d90000000000004980"), 0x014901), + (hex!("0122222222333333334444444455000000da0000000000000db0"), 0x0149a1), + (hex!("0122222222333333334444444455000000da00000000000048c0"), 0x014a41), + (hex!("0122222222333333334444444455000000da00000000000072c0"), 0x014ae1), + (hex!("0122222222333333334444444455000000da00000000000076b0"), 0x014b81), + (hex!("0122222222333333334444444455000000db0000000000000dc0"), 0x014c21), + (hex!("0122222222333333334444444455000000dc0000000000000dd0"), 0x014cc1), + (hex!("0122222222333333334444444455000000dc00000000000040a0"), 0x014d61), + (hex!("0122222222333333334444444455000000dc00000000000074c0"), 0x014e01), + (hex!("0122222222333333334444444455000000dd0000000000000de0"), 0x014ea1), + (hex!("0122222222333333334444444455000000dd0000000000004e50"), 0x014f41), + (hex!("0122222222333333334444444455000000dd0000000000007270"), 0x014fe1), + (hex!("0122222222333333334444444455000000de0000000000000df0"), 0x015081), + (hex!("0122222222333333334444444455000000de00000000000078d0"), 0x015121), + (hex!("0122222222333333334444444455000000df0000000000000e00"), 0x0151c1), + (hex!("0122222222333333334444444455000000df0000000000004d30"), 0x015261), + (hex!("0122222222333333334444444455000000df0000000000006c30"), 0x015301), + (hex!("0122222222333333334444444455000000e00000000000000e10"), 0x0153a1), + (hex!("0122222222333333334444444455000000e00000000000005d30"), 0x015441), + (hex!("0122222222333333334444444455000000e10000000000000e20"), 0x0154e1), + (hex!("0122222222333333334444444455000000e10000000000004610"), 0x015581), + (hex!("0122222222333333334444444455000000e100000000000051d0"), 0x015621), + (hex!("0122222222333333334444444455000000e10000000000005f10"), 0x0156c1), + (hex!("0122222222333333334444444455000000e20000000000000e30"), 0x015761), + (hex!("0122222222333333334444444455000000e20000000000007a90"), 0x015801), + (hex!("0122222222333333334444444455000000e30000000000000e40"), 0x0158a1), + (hex!("0122222222333333334444444455000000e30000000000005ae0"), 0x015941), + (hex!("0122222222333333334444444455000000e40000000000000e50"), 0x0159e1), + (hex!("0122222222333333334444444455000000e50000000000000e60"), 0x015a81), + (hex!("0122222222333333334444444455000000e50000000000004700"), 0x015b21), + (hex!("0122222222333333334444444455000000e500000000000065d0"), 0x015bc1), + (hex!("0122222222333333334444444455000000e60000000000000e70"), 0x015c61), + (hex!("0122222222333333334444444455000000e60000000000004fd0"), 0x015d01), + (hex!("0122222222333333334444444455000000e70000000000000e80"), 0x015da1), + (hex!("0122222222333333334444444455000000e70000000000005150"), 0x015e41), + (hex!("0122222222333333334444444455000000e70000000000005920"), 0x015ee1), + (hex!("0122222222333333334444444455000000e80000000000000e90"), 0x015f81), + (hex!("0122222222333333334444444455000000e80000000000004320"), 0x016021), + (hex!("0122222222333333334444444455000000e80000000000005ec0"), 0x0160c1), + (hex!("0122222222333333334444444455000000e90000000000000ea0"), 0x016161), + (hex!("0122222222333333334444444455000000e900000000000043b0"), 0x016201), + (hex!("0122222222333333334444444455000000ea0000000000000eb0"), 0x0162a1), + (hex!("0122222222333333334444444455000000ea0000000000003ea0"), 0x016341), + (hex!("0122222222333333334444444455000000ea0000000000004f50"), 0x0163e1), + (hex!("0122222222333333334444444455000000ea0000000000007520"), 0x016481), + (hex!("0122222222333333334444444455000000eb0000000000000ec0"), 0x016521), + (hex!("0122222222333333334444444455000000ec0000000000000ed0"), 0x0165c1), + (hex!("0122222222333333334444444455000000ec0000000000006670"), 0x016661), + (hex!("0122222222333333334444444455000000ed0000000000000ee0"), 0x016701), + (hex!("0122222222333333334444444455000000ee0000000000000ef0"), 0x0167a1), + (hex!("0122222222333333334444444455000000ee0000000000004d10"), 0x016841), + (hex!("0122222222333333334444444455000000ef0000000000000f00"), 0x0168e1), + (hex!("0122222222333333334444444455000000f00000000000000f10"), 0x016981), + (hex!("0122222222333333334444444455000000f00000000000007220"), 0x016a21), + (hex!("0122222222333333334444444455000000f00000000000007540"), 0x016ac1), + (hex!("0122222222333333334444444455000000f10000000000000f20"), 0x016b61), + (hex!("0122222222333333334444444455000000f100000000000066f0"), 0x016c01), + (hex!("0122222222333333334444444455000000f20000000000000f30"), 0x016ca1), + (hex!("0122222222333333334444444455000000f20000000000007810"), 0x016d41), + (hex!("0122222222333333334444444455000000f30000000000000f40"), 0x016de1), + (hex!("0122222222333333334444444455000000f30000000000007b70"), 0x016e81), + (hex!("0122222222333333334444444455000000f40000000000000f50"), 0x016f21), + (hex!("0122222222333333334444444455000000f400000000000059c0"), 0x016fc1), + (hex!("0122222222333333334444444455000000f50000000000000f60"), 0x017061), + (hex!("0122222222333333334444444455000000f50000000000003fb0"), 0x017101), + (hex!("0122222222333333334444444455000000f50000000000005740"), 0x0171a1), + (hex!("0122222222333333334444444455000000f500000000000064d0"), 0x017241), + (hex!("0122222222333333334444444455000000f50000000000006960"), 0x0172e1), + (hex!("0122222222333333334444444455000000f60000000000000f70"), 0x017381), + (hex!("0122222222333333334444444455000000f60000000000006d00"), 0x017421), + (hex!("0122222222333333334444444455000000f70000000000000f80"), 0x0174c1), + (hex!("0122222222333333334444444455000000f80000000000000f90"), 0x017561), + (hex!("0122222222333333334444444455000000f90000000000000fa0"), 0x017601), + (hex!("0122222222333333334444444455000000fa0000000000000fb0"), 0x0176a1), + (hex!("0122222222333333334444444455000000fa00000000000067b0"), 0x017741), + (hex!("0122222222333333334444444455000000fb0000000000000fc0"), 0x0177e1), + (hex!("0122222222333333334444444455000000fb0000000000004eb0"), 0x017881), + (hex!("0122222222333333334444444455000000fb0000000000006ef0"), 0x017921), + (hex!("0122222222333333334444444455000000fc0000000000000fd0"), 0x0179c1), + (hex!("0122222222333333334444444455000000fc0000000000004470"), 0x017a61), + (hex!("0122222222333333334444444455000000fc0000000000005940"), 0x017b01), + (hex!("0122222222333333334444444455000000fd0000000000000fe0"), 0x017ba1), + (hex!("0122222222333333334444444455000000fe0000000000000ff0"), 0x017c41), + (hex!("0122222222333333334444444455000000ff0000000000001000"), 0x017ce1), + (hex!("0122222222333333334444444455000000ff0000000000005690"), 0x017d81), + (hex!("0122222222333333334444444455000001000000000000001010"), 0x017e21), + (hex!("0122222222333333334444444455000001000000000000005210"), 0x017ec1), + (hex!("01222222223333333344444444550000010000000000000070a0"), 0x017f61), + (hex!("0122222222333333334444444455000001010000000000001020"), 0x018001), + (hex!("0122222222333333334444444455000001010000000000006b80"), 0x0180a1), + (hex!("0122222222333333334444444455000001020000000000001030"), 0x018141), + (hex!("0122222222333333334444444455000001030000000000001040"), 0x0181e1), + (hex!("0122222222333333334444444455000001030000000000004c80"), 0x018281), + (hex!("0122222222333333334444444455000001040000000000001050"), 0x018321), + (hex!("0122222222333333334444444455000001040000000000004850"), 0x0183c1), + (hex!("01222222223333333344444444550000010400000000000057b0"), 0x018461), + (hex!("0122222222333333334444444455000001050000000000001060"), 0x018501), + (hex!("01222222223333333344444444550000010500000000000048d0"), 0x0185a1), + (hex!("0122222222333333334444444455000001050000000000007870"), 0x018641), + (hex!("0122222222333333334444444455000001060000000000001070"), 0x0186e1), + (hex!("0122222222333333334444444455000001060000000000004f90"), 0x018781), + (hex!("0122222222333333334444444455000001060000000000006270"), 0x018821), + (hex!("0122222222333333334444444455000001070000000000001080"), 0x0188c1), + (hex!("01222222223333333344444444550000010700000000000063b0"), 0x018961), + (hex!("0122222222333333334444444455000001080000000000001090"), 0x018a01), + (hex!("01222222223333333344444444550000010900000000000010a0"), 0x018aa1), + (hex!("0122222222333333334444444455000001090000000000006f40"), 0x018b41), + (hex!("01222222223333333344444444550000010a00000000000010b0"), 0x018be1), + (hex!("01222222223333333344444444550000010a0000000000006640"), 0x018c81), + (hex!("01222222223333333344444444550000010b00000000000010c0"), 0x018d21), + (hex!("01222222223333333344444444550000010c00000000000010d0"), 0x018dc1), + (hex!("01222222223333333344444444550000010d00000000000010e0"), 0x018e61), + (hex!("01222222223333333344444444550000010e00000000000010f0"), 0x018f01), + (hex!("01222222223333333344444444550000010e0000000000005c40"), 0x018fa1), + (hex!("01222222223333333344444444550000010e0000000000007ba0"), 0x019041), + (hex!("01222222223333333344444444550000010f0000000000001100"), 0x0190e1), + (hex!("01222222223333333344444444550000010f0000000000005c30"), 0x019181), + (hex!("0122222222333333334444444455000001100000000000001110"), 0x019221), + (hex!("0122222222333333334444444455000001100000000000007640"), 0x0192c1), + (hex!("0122222222333333334444444455000001110000000000001120"), 0x019361), + (hex!("01222222223333333344444444550000011100000000000052c0"), 0x019401), + (hex!("0122222222333333334444444455000001110000000000005710"), 0x0194a1), + (hex!("0122222222333333334444444455000001110000000000006a00"), 0x019541), + (hex!("0122222222333333334444444455000001120000000000001130"), 0x0195e1), + (hex!("0122222222333333334444444455000001130000000000001140"), 0x019681), + (hex!("0122222222333333334444444455000001140000000000001150"), 0x019721), + (hex!("0122222222333333334444444455000001140000000000003fa0"), 0x0197c1), + (hex!("01222222223333333344444444550000011400000000000054b0"), 0x019861), + (hex!("0122222222333333334444444455000001140000000000006070"), 0x019901), + (hex!("0122222222333333334444444455000001150000000000001160"), 0x0199a1), + (hex!("0122222222333333334444444455000001150000000000005320"), 0x019a41), + (hex!("0122222222333333334444444455000001150000000000006600"), 0x019ae1), + (hex!("0122222222333333334444444455000001150000000000006df0"), 0x019b81), + (hex!("01222222223333333344444444550000011500000000000079c0"), 0x019c21), + (hex!("0122222222333333334444444455000001160000000000001170"), 0x019cc1), + (hex!("0122222222333333334444444455000001170000000000001180"), 0x019d61), + (hex!("0122222222333333334444444455000001170000000000004a60"), 0x019e01), + (hex!("01222222223333333344444444550000011700000000000063c0"), 0x019ea1), + (hex!("0122222222333333334444444455000001180000000000001190"), 0x019f41), + (hex!("0122222222333333334444444455000001180000000000004530"), 0x019fe1), + (hex!("01222222223333333344444444550000011800000000000077c0"), 0x01a081), + (hex!("01222222223333333344444444550000011900000000000011a0"), 0x01a121), + (hex!("01222222223333333344444444550000011a00000000000011b0"), 0x01a1c1), + (hex!("01222222223333333344444444550000011a00000000000041c0"), 0x01a261), + (hex!("01222222223333333344444444550000011a00000000000061e0"), 0x01a301), + (hex!("01222222223333333344444444550000011b00000000000011c0"), 0x01a3a1), + (hex!("01222222223333333344444444550000011c00000000000011d0"), 0x01a441), + (hex!("01222222223333333344444444550000011c0000000000005f90"), 0x01a4e1), + (hex!("01222222223333333344444444550000011d00000000000011e0"), 0x01a581), + (hex!("01222222223333333344444444550000011d0000000000004160"), 0x01a621), + (hex!("01222222223333333344444444550000011e00000000000011f0"), 0x01a6c1), + (hex!("01222222223333333344444444550000011e00000000000056d0"), 0x01a761), + (hex!("01222222223333333344444444550000011f0000000000001200"), 0x01a801), + (hex!("01222222223333333344444444550000011f0000000000004510"), 0x01a8a1), + (hex!("0122222222333333334444444455000001200000000000001210"), 0x01a941), + (hex!("0122222222333333334444444455000001210000000000001220"), 0x01a9e1), + (hex!("0122222222333333334444444455000001210000000000005140"), 0x01aa81), + (hex!("0122222222333333334444444455000001210000000000006710"), 0x01ab21), + (hex!("0122222222333333334444444455000001210000000000006f50"), 0x01abc1), + (hex!("0122222222333333334444444455000001220000000000001230"), 0x01ac61), + (hex!("0122222222333333334444444455000001220000000000005570"), 0x01ad01), + (hex!("0122222222333333334444444455000001220000000000007ac0"), 0x01ada1), + (hex!("0122222222333333334444444455000001230000000000001240"), 0x01ae41), + (hex!("0122222222333333334444444455000001240000000000001250"), 0x01aee1), + (hex!("0122222222333333334444444455000001240000000000006cd0"), 0x01af81), + (hex!("0122222222333333334444444455000001250000000000001260"), 0x01b021), + (hex!("01222222223333333344444444550000012500000000000046b0"), 0x01b0c1), + (hex!("0122222222333333334444444455000001250000000000005eb0"), 0x01b161), + (hex!("0122222222333333334444444455000001260000000000001270"), 0x01b201), + (hex!("0122222222333333334444444455000001260000000000004630"), 0x01b2a1), + (hex!("0122222222333333334444444455000001270000000000001280"), 0x01b341), + (hex!("0122222222333333334444444455000001270000000000004ff0"), 0x01b3e1), + (hex!("0122222222333333334444444455000001270000000000006ec0"), 0x01b481), + (hex!("0122222222333333334444444455000001280000000000001290"), 0x01b521), + (hex!("01222222223333333344444444550000012900000000000012a0"), 0x01b5c1), + (hex!("0122222222333333334444444455000001290000000000005f60"), 0x01b661), + (hex!("01222222223333333344444444550000012a00000000000012b0"), 0x01b701), + (hex!("01222222223333333344444444550000012a0000000000005480"), 0x01b7a1), + (hex!("01222222223333333344444444550000012b00000000000012c0"), 0x01b841), + (hex!("01222222223333333344444444550000012b00000000000065a0"), 0x01b8e1), + (hex!("01222222223333333344444444550000012b00000000000066c0"), 0x01b981), + (hex!("01222222223333333344444444550000012c00000000000012d0"), 0x01ba21), + (hex!("01222222223333333344444444550000012c00000000000064b0"), 0x01bac1), + (hex!("01222222223333333344444444550000012d00000000000012e0"), 0x01bb61), + (hex!("01222222223333333344444444550000012d00000000000049c0"), 0x01bc01), + (hex!("01222222223333333344444444550000012d0000000000004bf0"), 0x01bca1), + (hex!("01222222223333333344444444550000012e00000000000012f0"), 0x01bd41), + (hex!("01222222223333333344444444550000012e0000000000005ed0"), 0x01bde1), + (hex!("01222222223333333344444444550000012f0000000000001300"), 0x01be81), + (hex!("01222222223333333344444444550000012f00000000000049a0"), 0x01bf21), + (hex!("0122222222333333334444444455000001300000000000001310"), 0x01bfc1), + (hex!("0122222222333333334444444455000001300000000000007840"), 0x01c061), + (hex!("0122222222333333334444444455000001310000000000001320"), 0x01c101), + (hex!("0122222222333333334444444455000001310000000000005f70"), 0x01c1a1), + (hex!("0122222222333333334444444455000001320000000000001330"), 0x01c241), + (hex!("0122222222333333334444444455000001320000000000005a00"), 0x01c2e1), + (hex!("0122222222333333334444444455000001330000000000001340"), 0x01c381), + (hex!("0122222222333333334444444455000001330000000000006c70"), 0x01c421), + (hex!("0122222222333333334444444455000001340000000000001350"), 0x01c4c1), + (hex!("0122222222333333334444444455000001340000000000005c60"), 0x01c561), + (hex!("0122222222333333334444444455000001350000000000001360"), 0x01c601), + (hex!("0122222222333333334444444455000001350000000000004f10"), 0x01c6a1), + (hex!("0122222222333333334444444455000001360000000000001370"), 0x01c741), + (hex!("0122222222333333334444444455000001360000000000004c60"), 0x01c7e1), + (hex!("0122222222333333334444444455000001370000000000001380"), 0x01c881), + (hex!("0122222222333333334444444455000001380000000000001390"), 0x01c921), + (hex!("01222222223333333344444444550000013900000000000013a0"), 0x01c9c1), + (hex!("0122222222333333334444444455000001390000000000004ea0"), 0x01ca61), + (hex!("01222222223333333344444444550000013a00000000000013b0"), 0x01cb01), + (hex!("01222222223333333344444444550000013a0000000000007350"), 0x01cba1), + (hex!("01222222223333333344444444550000013b00000000000013c0"), 0x01cc41), + (hex!("01222222223333333344444444550000013c00000000000013d0"), 0x01cce1), + (hex!("01222222223333333344444444550000013c0000000000007050"), 0x01cd81), + (hex!("01222222223333333344444444550000013d00000000000013e0"), 0x01ce21), + (hex!("01222222223333333344444444550000013d0000000000006bd0"), 0x01cec1), + (hex!("01222222223333333344444444550000013e00000000000013f0"), 0x01cf61), + (hex!("01222222223333333344444444550000013e00000000000058e0"), 0x01d001), + (hex!("01222222223333333344444444550000013f0000000000001400"), 0x01d0a1), + (hex!("01222222223333333344444444550000013f0000000000004740"), 0x01d141), + (hex!("0122222222333333334444444455000001400000000000001410"), 0x01d1e1), + (hex!("0122222222333333334444444455000001400000000000003f10"), 0x01d281), + (hex!("0122222222333333334444444455000001400000000000006d40"), 0x01d321), + (hex!("01222222223333333344444444550000014000000000000072d0"), 0x01d3c1), + (hex!("0122222222333333334444444455000001410000000000001420"), 0x01d461), + (hex!("0122222222333333334444444455000001420000000000001430"), 0x01d501), + (hex!("0122222222333333334444444455000001430000000000001440"), 0x01d5a1), + (hex!("0122222222333333334444444455000001440000000000001450"), 0x01d641), + (hex!("0122222222333333334444444455000001450000000000001460"), 0x01d6e1), + (hex!("0122222222333333334444444455000001460000000000001470"), 0x01d781), + (hex!("01222222223333333344444444550000014600000000000055c0"), 0x01d821), + (hex!("0122222222333333334444444455000001470000000000001480"), 0x01d8c1), + (hex!("0122222222333333334444444455000001470000000000004570"), 0x01d961), + (hex!("0122222222333333334444444455000001470000000000004be0"), 0x01da01), + (hex!("0122222222333333334444444455000001480000000000001490"), 0x01daa1), + (hex!("0122222222333333334444444455000001480000000000005360"), 0x01db41), + (hex!("01222222223333333344444444550000014900000000000014a0"), 0x01dbe1), + (hex!("01222222223333333344444444550000014a00000000000014b0"), 0x01dc81), + (hex!("01222222223333333344444444550000014a00000000000053d0"), 0x01dd21), + (hex!("01222222223333333344444444550000014b00000000000014c0"), 0x01ddc1), + (hex!("01222222223333333344444444550000014b0000000000005950"), 0x01de61), + (hex!("01222222223333333344444444550000014c00000000000014d0"), 0x01df01), + (hex!("01222222223333333344444444550000014c0000000000004f60"), 0x01dfa1), + (hex!("01222222223333333344444444550000014d00000000000014e0"), 0x01e041), + (hex!("01222222223333333344444444550000014d0000000000004520"), 0x01e0e1), + (hex!("01222222223333333344444444550000014d0000000000005200"), 0x01e181), + (hex!("01222222223333333344444444550000014e00000000000014f0"), 0x01e221), + (hex!("01222222223333333344444444550000014e0000000000005bd0"), 0x01e2c1), + (hex!("01222222223333333344444444550000014f0000000000001500"), 0x01e361), + (hex!("01222222223333333344444444550000014f00000000000060d0"), 0x01e401), + (hex!("0122222222333333334444444455000001500000000000001510"), 0x01e4a1), + (hex!("01222222223333333344444444550000015000000000000075e0"), 0x01e541), + (hex!("0122222222333333334444444455000001510000000000001520"), 0x01e5e1), + (hex!("0122222222333333334444444455000001510000000000005c00"), 0x01e681), + (hex!("0122222222333333334444444455000001510000000000006af0"), 0x01e721), + (hex!("0122222222333333334444444455000001510000000000007b80"), 0x01e7c1), + (hex!("0122222222333333334444444455000001520000000000001530"), 0x01e861), + (hex!("0122222222333333334444444455000001520000000000004c70"), 0x01e901), + (hex!("0122222222333333334444444455000001530000000000001540"), 0x01e9a1), + (hex!("0122222222333333334444444455000001540000000000001550"), 0x01ea41), + (hex!("0122222222333333334444444455000001540000000000007cd0"), 0x01eae1), + (hex!("0122222222333333334444444455000001550000000000001560"), 0x01eb81), + (hex!("0122222222333333334444444455000001550000000000004ae0"), 0x01ec21), + (hex!("01222222223333333344444444550000015500000000000068c0"), 0x01ecc1), + (hex!("0122222222333333334444444455000001560000000000001570"), 0x01ed61), + (hex!("01222222223333333344444444550000015600000000000064a0"), 0x01ee01), + (hex!("0122222222333333334444444455000001570000000000001580"), 0x01eea1), + (hex!("0122222222333333334444444455000001580000000000001590"), 0x01ef41), + (hex!("0122222222333333334444444455000001580000000000006d30"), 0x01efe1), + (hex!("01222222223333333344444444550000015800000000000074f0"), 0x01f081), + (hex!("01222222223333333344444444550000015900000000000015a0"), 0x01f121), + (hex!("01222222223333333344444444550000015900000000000053a0"), 0x01f1c1), + (hex!("01222222223333333344444444550000015900000000000055e0"), 0x01f261), + (hex!("0122222222333333334444444455000001590000000000006210"), 0x01f301), + (hex!("01222222223333333344444444550000015900000000000067c0"), 0x01f3a1), + (hex!("01222222223333333344444444550000015a00000000000015b0"), 0x01f441), + (hex!("01222222223333333344444444550000015b00000000000015c0"), 0x01f4e1), + (hex!("01222222223333333344444444550000015c00000000000015d0"), 0x01f581), + (hex!("01222222223333333344444444550000015c0000000000004d80"), 0x01f621), + (hex!("01222222223333333344444444550000015c00000000000073f0"), 0x01f6c1), + (hex!("01222222223333333344444444550000015d00000000000015e0"), 0x01f761), + (hex!("01222222223333333344444444550000015e00000000000015f0"), 0x01f801), + (hex!("01222222223333333344444444550000015e0000000000004120"), 0x01f8a1), + (hex!("01222222223333333344444444550000015e0000000000004350"), 0x01f941), + (hex!("01222222223333333344444444550000015e0000000000007c50"), 0x01f9e1), + (hex!("01222222223333333344444444550000015f0000000000001600"), 0x01fa81), + (hex!("0122222222333333334444444455000001600000000000001610"), 0x01fb21), + (hex!("0122222222333333334444444455000001600000000000004840"), 0x01fbc1), + (hex!("0122222222333333334444444455000001600000000000004b10"), 0x01fc61), + (hex!("0122222222333333334444444455000001600000000000007060"), 0x01fd01), + (hex!("0122222222333333334444444455000001610000000000001620"), 0x01fda1), + (hex!("0122222222333333334444444455000001610000000000005300"), 0x01fe41), + (hex!("0122222222333333334444444455000001620000000000001630"), 0x01fee1), + (hex!("0122222222333333334444444455000001620000000000006530"), 0x01ff81), + (hex!("0122222222333333334444444455000001630000000000001640"), 0x020021), + (hex!("0122222222333333334444444455000001640000000000001650"), 0x0200c1), + (hex!("0122222222333333334444444455000001650000000000001660"), 0x020161), + (hex!("0122222222333333334444444455000001660000000000001670"), 0x020201), + (hex!("0122222222333333334444444455000001670000000000001680"), 0x0202a1), + (hex!("0122222222333333334444444455000001670000000000007310"), 0x020341), + (hex!("0122222222333333334444444455000001680000000000001690"), 0x0203e1), + (hex!("0122222222333333334444444455000001680000000000007b50"), 0x020481), + (hex!("01222222223333333344444444550000016900000000000016a0"), 0x020521), + (hex!("01222222223333333344444444550000016900000000000049d0"), 0x0205c1), + (hex!("01222222223333333344444444550000016a00000000000016b0"), 0x020661), + (hex!("01222222223333333344444444550000016a00000000000078b0"), 0x020701), + (hex!("01222222223333333344444444550000016b00000000000016c0"), 0x0207a1), + (hex!("01222222223333333344444444550000016b0000000000004100"), 0x020841), + (hex!("01222222223333333344444444550000016c00000000000016d0"), 0x0208e1), + (hex!("01222222223333333344444444550000016c0000000000006e00"), 0x020981), + (hex!("01222222223333333344444444550000016d00000000000016e0"), 0x020a21), + (hex!("01222222223333333344444444550000016e00000000000016f0"), 0x020ac1), + (hex!("01222222223333333344444444550000016e0000000000004ac0"), 0x020b61), + (hex!("01222222223333333344444444550000016e0000000000007820"), 0x020c01), + (hex!("01222222223333333344444444550000016f0000000000001700"), 0x020ca1), + (hex!("0122222222333333334444444455000001700000000000001710"), 0x020d41), + (hex!("0122222222333333334444444455000001700000000000005830"), 0x020de1), + (hex!("0122222222333333334444444455000001710000000000001720"), 0x020e81), + (hex!("01222222223333333344444444550000017100000000000072f0"), 0x020f21), + (hex!("0122222222333333334444444455000001720000000000001730"), 0x020fc1), + (hex!("0122222222333333334444444455000001720000000000004870"), 0x021061), + (hex!("01222222223333333344444444550000017200000000000070b0"), 0x021101), + (hex!("0122222222333333334444444455000001730000000000001740"), 0x0211a1), + (hex!("0122222222333333334444444455000001740000000000001750"), 0x021241), + (hex!("0122222222333333334444444455000001750000000000001760"), 0x0212e1), + (hex!("0122222222333333334444444455000001750000000000005670"), 0x021381), + (hex!("0122222222333333334444444455000001750000000000005870"), 0x021421), + (hex!("0122222222333333334444444455000001760000000000001770"), 0x0214c1), + (hex!("0122222222333333334444444455000001770000000000001780"), 0x021561), + (hex!("0122222222333333334444444455000001770000000000005000"), 0x021601), + (hex!("0122222222333333334444444455000001770000000000007090"), 0x0216a1), + (hex!("0122222222333333334444444455000001780000000000001790"), 0x021741), + (hex!("01222222223333333344444444550000017800000000000048a0"), 0x0217e1), + (hex!("0122222222333333334444444455000001780000000000006bf0"), 0x021881), + (hex!("01222222223333333344444444550000017900000000000017a0"), 0x021921), + (hex!("01222222223333333344444444550000017900000000000057d0"), 0x0219c1), + (hex!("0122222222333333334444444455000001790000000000006660"), 0x021a61), + (hex!("01222222223333333344444444550000017a00000000000017b0"), 0x021b01), + (hex!("01222222223333333344444444550000017a0000000000004970"), 0x021ba1), + (hex!("01222222223333333344444444550000017a0000000000005dc0"), 0x021c41), + (hex!("01222222223333333344444444550000017b00000000000017c0"), 0x021ce1), + (hex!("01222222223333333344444444550000017b0000000000004ee0"), 0x021d81), + (hex!("01222222223333333344444444550000017b00000000000054c0"), 0x021e21), + (hex!("01222222223333333344444444550000017c00000000000017d0"), 0x021ec1), + (hex!("01222222223333333344444444550000017c0000000000003fc0"), 0x021f61), + (hex!("01222222223333333344444444550000017c00000000000063e0"), 0x022001), + (hex!("01222222223333333344444444550000017c0000000000006520"), 0x0220a1), + (hex!("01222222223333333344444444550000017d00000000000017e0"), 0x022141), + (hex!("01222222223333333344444444550000017d0000000000006220"), 0x0221e1), + (hex!("01222222223333333344444444550000017d0000000000007120"), 0x022281), + (hex!("01222222223333333344444444550000017e00000000000017f0"), 0x022321), + (hex!("01222222223333333344444444550000017f0000000000001800"), 0x0223c1), + (hex!("0122222222333333334444444455000001800000000000001810"), 0x022461), + (hex!("0122222222333333334444444455000001810000000000001820"), 0x022501), + (hex!("01222222223333333344444444550000018100000000000041f0"), 0x0225a1), + (hex!("0122222222333333334444444455000001810000000000007590"), 0x022641), + (hex!("0122222222333333334444444455000001820000000000001830"), 0x0226e1), + (hex!("0122222222333333334444444455000001820000000000004ce0"), 0x022781), + (hex!("0122222222333333334444444455000001830000000000001840"), 0x022821), + (hex!("01222222223333333344444444550000018300000000000042c0"), 0x0228c1), + (hex!("0122222222333333334444444455000001840000000000001850"), 0x022961), + (hex!("0122222222333333334444444455000001840000000000004f70"), 0x022a01), + (hex!("0122222222333333334444444455000001850000000000001860"), 0x022aa1), + (hex!("0122222222333333334444444455000001850000000000006470"), 0x022b41), + (hex!("0122222222333333334444444455000001850000000000007500"), 0x022be1), + (hex!("0122222222333333334444444455000001860000000000001870"), 0x022c81), + (hex!("0122222222333333334444444455000001860000000000004770"), 0x022d21), + (hex!("0122222222333333334444444455000001870000000000001880"), 0x022dc1), + (hex!("0122222222333333334444444455000001870000000000006a30"), 0x022e61), + (hex!("0122222222333333334444444455000001880000000000001890"), 0x022f01), + (hex!("0122222222333333334444444455000001880000000000007410"), 0x022fa1), + (hex!("01222222223333333344444444550000018900000000000018a0"), 0x023041), + (hex!("01222222223333333344444444550000018900000000000044d0"), 0x0230e1), + (hex!("0122222222333333334444444455000001890000000000005ac0"), 0x023181), + (hex!("01222222223333333344444444550000018a00000000000018b0"), 0x023221), + (hex!("01222222223333333344444444550000018a0000000000006260"), 0x0232c1), + (hex!("01222222223333333344444444550000018a0000000000006d70"), 0x023361), + (hex!("01222222223333333344444444550000018b00000000000018c0"), 0x023401), + (hex!("01222222223333333344444444550000018b0000000000004aa0"), 0x0234a1), + (hex!("01222222223333333344444444550000018b0000000000006fd0"), 0x023541), + (hex!("01222222223333333344444444550000018c00000000000018d0"), 0x0235e1), + (hex!("01222222223333333344444444550000018c00000000000051b0"), 0x023681), + (hex!("01222222223333333344444444550000018c0000000000006650"), 0x023721), + (hex!("01222222223333333344444444550000018d00000000000018e0"), 0x0237c1), + (hex!("01222222223333333344444444550000018e00000000000018f0"), 0x023861), + (hex!("01222222223333333344444444550000018e00000000000041d0"), 0x023901), + (hex!("01222222223333333344444444550000018f0000000000001900"), 0x0239a1), + (hex!("01222222223333333344444444550000018f0000000000007600"), 0x023a41), + (hex!("0122222222333333334444444455000001900000000000001910"), 0x023ae1), + (hex!("0122222222333333334444444455000001900000000000005410"), 0x023b81), + (hex!("0122222222333333334444444455000001900000000000006760"), 0x023c21), + (hex!("0122222222333333334444444455000001910000000000001920"), 0x023cc1), + (hex!("0122222222333333334444444455000001920000000000001930"), 0x023d61), + (hex!("0122222222333333334444444455000001920000000000004ca0"), 0x023e01), + (hex!("0122222222333333334444444455000001920000000000005d80"), 0x023ea1), + (hex!("0122222222333333334444444455000001920000000000005fd0"), 0x023f41), + (hex!("01222222223333333344444444550000019200000000000070d0"), 0x023fe1), + (hex!("0122222222333333334444444455000001930000000000001940"), 0x024081), + (hex!("0122222222333333334444444455000001930000000000004010"), 0x024121), + (hex!("0122222222333333334444444455000001930000000000007ca0"), 0x0241c1), + (hex!("0122222222333333334444444455000001940000000000001950"), 0x024261), + (hex!("0122222222333333334444444455000001950000000000001960"), 0x024301), + (hex!("0122222222333333334444444455000001950000000000005380"), 0x0243a1), + (hex!("0122222222333333334444444455000001960000000000001970"), 0x024441), + (hex!("0122222222333333334444444455000001960000000000006de0"), 0x0244e1), + (hex!("0122222222333333334444444455000001970000000000001980"), 0x024581), + (hex!("01222222223333333344444444550000019700000000000048f0"), 0x024621), + (hex!("0122222222333333334444444455000001980000000000001990"), 0x0246c1), + (hex!("0122222222333333334444444455000001980000000000006510"), 0x024761), + (hex!("01222222223333333344444444550000019900000000000019a0"), 0x024801), + (hex!("0122222222333333334444444455000001990000000000007570"), 0x0248a1), + (hex!("0122222222333333334444444455000001990000000000007580"), 0x024941), + (hex!("01222222223333333344444444550000019a00000000000019b0"), 0x0249e1), + (hex!("01222222223333333344444444550000019a0000000000004050"), 0x024a81), + (hex!("01222222223333333344444444550000019a0000000000004ba0"), 0x024b21), + (hex!("01222222223333333344444444550000019a0000000000005540"), 0x024bc1), + (hex!("01222222223333333344444444550000019a00000000000061c0"), 0x024c61), + (hex!("01222222223333333344444444550000019a0000000000007c60"), 0x024d01), + (hex!("01222222223333333344444444550000019b00000000000019c0"), 0x024da1), + (hex!("01222222223333333344444444550000019b0000000000006240"), 0x024e41), + (hex!("01222222223333333344444444550000019c00000000000019d0"), 0x024ee1), + (hex!("01222222223333333344444444550000019d00000000000019e0"), 0x024f81), + (hex!("01222222223333333344444444550000019d0000000000004640"), 0x025021), + (hex!("01222222223333333344444444550000019d00000000000052a0"), 0x0250c1), + (hex!("01222222223333333344444444550000019d00000000000052b0"), 0x025161), + (hex!("01222222223333333344444444550000019e00000000000019f0"), 0x025201), + (hex!("01222222223333333344444444550000019f0000000000001a00"), 0x0252a1), + (hex!("01222222223333333344444444550000019f0000000000006b20"), 0x025341), + (hex!("0122222222333333334444444455000001a00000000000001a10"), 0x0253e1), + (hex!("0122222222333333334444444455000001a10000000000001a20"), 0x025481), + (hex!("0122222222333333334444444455000001a10000000000005460"), 0x025521), + (hex!("0122222222333333334444444455000001a10000000000005d20"), 0x0255c1), + (hex!("0122222222333333334444444455000001a100000000000068f0"), 0x025661), + (hex!("0122222222333333334444444455000001a20000000000001a30"), 0x025701), + (hex!("0122222222333333334444444455000001a20000000000007170"), 0x0257a1), + (hex!("0122222222333333334444444455000001a30000000000001a40"), 0x025841), + (hex!("0122222222333333334444444455000001a40000000000001a50"), 0x0258e1), + (hex!("0122222222333333334444444455000001a50000000000001a60"), 0x025981), + (hex!("0122222222333333334444444455000001a60000000000001a70"), 0x025a21), + (hex!("0122222222333333334444444455000001a70000000000001a80"), 0x025ac1), + (hex!("0122222222333333334444444455000001a70000000000005a90"), 0x025b61), + (hex!("0122222222333333334444444455000001a70000000000006440"), 0x025c01), + (hex!("0122222222333333334444444455000001a80000000000001a90"), 0x025ca1), + (hex!("0122222222333333334444444455000001a80000000000004800"), 0x025d41), + (hex!("0122222222333333334444444455000001a90000000000001aa0"), 0x025de1), + (hex!("0122222222333333334444444455000001aa0000000000001ab0"), 0x025e81), + (hex!("0122222222333333334444444455000001aa0000000000005b60"), 0x025f21), + (hex!("0122222222333333334444444455000001ab0000000000001ac0"), 0x025fc1), + (hex!("0122222222333333334444444455000001ab0000000000006700"), 0x026061), + (hex!("0122222222333333334444444455000001ab00000000000071d0"), 0x026101), + (hex!("0122222222333333334444444455000001ac0000000000001ad0"), 0x0261a1), + (hex!("0122222222333333334444444455000001ac0000000000007380"), 0x026241), + (hex!("0122222222333333334444444455000001ad0000000000001ae0"), 0x0262e1), + (hex!("0122222222333333334444444455000001ad0000000000006350"), 0x026381), + (hex!("0122222222333333334444444455000001ae0000000000001af0"), 0x026421), + (hex!("0122222222333333334444444455000001af0000000000001b00"), 0x0264c1), + (hex!("0122222222333333334444444455000001af0000000000007390"), 0x026561), + (hex!("0122222222333333334444444455000001b00000000000001b10"), 0x026601), + (hex!("0122222222333333334444444455000001b10000000000001b20"), 0x0266a1), + (hex!("0122222222333333334444444455000001b10000000000005cc0"), 0x026741), + (hex!("0122222222333333334444444455000001b20000000000001b30"), 0x0267e1), + (hex!("0122222222333333334444444455000001b20000000000004fb0"), 0x026881), + (hex!("0122222222333333334444444455000001b30000000000001b40"), 0x026921), + (hex!("0122222222333333334444444455000001b40000000000001b50"), 0x0269c1), + (hex!("0122222222333333334444444455000001b50000000000001b60"), 0x026a61), + (hex!("0122222222333333334444444455000001b60000000000001b70"), 0x026b01), + (hex!("0122222222333333334444444455000001b600000000000048e0"), 0x026ba1), + (hex!("0122222222333333334444444455000001b70000000000001b80"), 0x026c41), + (hex!("0122222222333333334444444455000001b70000000000005ca0"), 0x026ce1), + (hex!("0122222222333333334444444455000001b70000000000007900"), 0x026d81), + (hex!("0122222222333333334444444455000001b80000000000001b90"), 0x026e21), + (hex!("0122222222333333334444444455000001b80000000000004d90"), 0x026ec1), + (hex!("0122222222333333334444444455000001b90000000000001ba0"), 0x026f61), + (hex!("0122222222333333334444444455000001b90000000000003f40"), 0x027001), + (hex!("0122222222333333334444444455000001ba0000000000001bb0"), 0x0270a1), + (hex!("0122222222333333334444444455000001ba00000000000042a0"), 0x027141), + (hex!("0122222222333333334444444455000001ba00000000000067f0"), 0x0271e1), + (hex!("0122222222333333334444444455000001ba00000000000073a0"), 0x027281), + (hex!("0122222222333333334444444455000001bb0000000000001bc0"), 0x027321), + (hex!("0122222222333333334444444455000001bb0000000000004a00"), 0x0273c1), + (hex!("0122222222333333334444444455000001bb0000000000005e00"), 0x027461), + (hex!("0122222222333333334444444455000001bc0000000000001bd0"), 0x027501), + (hex!("0122222222333333334444444455000001bc0000000000004230"), 0x0275a1), + (hex!("0122222222333333334444444455000001bc0000000000005860"), 0x027641), + (hex!("0122222222333333334444444455000001bd0000000000001be0"), 0x0276e1), + (hex!("0122222222333333334444444455000001bd0000000000007c70"), 0x027781), + (hex!("0122222222333333334444444455000001be0000000000001bf0"), 0x027821), + (hex!("0122222222333333334444444455000001be0000000000007770"), 0x0278c1), + (hex!("0122222222333333334444444455000001be0000000000007cf0"), 0x027961), + (hex!("0122222222333333334444444455000001bf0000000000001c00"), 0x027a01), + (hex!("0122222222333333334444444455000001bf0000000000006490"), 0x027aa1), + (hex!("0122222222333333334444444455000001c00000000000001c10"), 0x027b41), + (hex!("0122222222333333334444444455000001c10000000000001c20"), 0x027be1), + (hex!("0122222222333333334444444455000001c10000000000004600"), 0x027c81), + (hex!("0122222222333333334444444455000001c20000000000001c30"), 0x027d21), + (hex!("0122222222333333334444444455000001c20000000000006e30"), 0x027dc1), + (hex!("0122222222333333334444444455000001c30000000000001c40"), 0x027e61), + (hex!("0122222222333333334444444455000001c40000000000001c50"), 0x027f01), + (hex!("0122222222333333334444444455000001c50000000000001c60"), 0x027fa1), + (hex!("0122222222333333334444444455000001c60000000000001c70"), 0x028041), + (hex!("0122222222333333334444444455000001c60000000000004240"), 0x0280e1), + (hex!("0122222222333333334444444455000001c60000000000005bb0"), 0x028181), + (hex!("0122222222333333334444444455000001c70000000000001c80"), 0x028221), + (hex!("0122222222333333334444444455000001c80000000000001c90"), 0x0282c1), + (hex!("0122222222333333334444444455000001c90000000000001ca0"), 0x028361), + (hex!("0122222222333333334444444455000001c90000000000006730"), 0x028401), + (hex!("0122222222333333334444444455000001ca0000000000001cb0"), 0x0284a1), + (hex!("0122222222333333334444444455000001ca00000000000070f0"), 0x028541), + (hex!("0122222222333333334444444455000001cb0000000000001cc0"), 0x0285e1), + (hex!("0122222222333333334444444455000001cb00000000000071a0"), 0x028681), + (hex!("0122222222333333334444444455000001cc0000000000001cd0"), 0x028721), + (hex!("0122222222333333334444444455000001cc0000000000005280"), 0x0287c1), + (hex!("0122222222333333334444444455000001cc0000000000005d90"), 0x028861), + (hex!("0122222222333333334444444455000001cd0000000000001ce0"), 0x028901), + (hex!("0122222222333333334444444455000001cd00000000000069b0"), 0x0289a1), + (hex!("0122222222333333334444444455000001ce0000000000001cf0"), 0x028a41), + (hex!("0122222222333333334444444455000001ce0000000000004540"), 0x028ae1), + (hex!("0122222222333333334444444455000001cf0000000000001d00"), 0x028b81), + (hex!("0122222222333333334444444455000001cf00000000000076a0"), 0x028c21), + (hex!("0122222222333333334444444455000001d00000000000001d10"), 0x028cc1), + (hex!("0122222222333333334444444455000001d000000000000060a0"), 0x028d61), + (hex!("0122222222333333334444444455000001d10000000000001d20"), 0x028e01), + (hex!("0122222222333333334444444455000001d20000000000001d30"), 0x028ea1), + (hex!("0122222222333333334444444455000001d30000000000001d40"), 0x028f41), + (hex!("0122222222333333334444444455000001d30000000000004000"), 0x028fe1), + (hex!("0122222222333333334444444455000001d30000000000004140"), 0x029081), + (hex!("0122222222333333334444444455000001d30000000000006790"), 0x029121), + (hex!("0122222222333333334444444455000001d40000000000001d50"), 0x0291c1), + (hex!("0122222222333333334444444455000001d50000000000001d60"), 0x029261), + (hex!("0122222222333333334444444455000001d60000000000001d70"), 0x029301), + (hex!("0122222222333333334444444455000001d60000000000004b50"), 0x0293a1), + (hex!("0122222222333333334444444455000001d60000000000007430"), 0x029441), + (hex!("0122222222333333334444444455000001d70000000000001d80"), 0x0294e1), + (hex!("0122222222333333334444444455000001d70000000000006920"), 0x029581), + (hex!("0122222222333333334444444455000001d80000000000001d90"), 0x029621), + (hex!("0122222222333333334444444455000001d80000000000005b30"), 0x0296c1), + (hex!("0122222222333333334444444455000001d90000000000001da0"), 0x029761), + (hex!("0122222222333333334444444455000001da0000000000001db0"), 0x029801), + (hex!("0122222222333333334444444455000001da0000000000004af0"), 0x0298a1), + (hex!("0122222222333333334444444455000001da0000000000007240"), 0x029941), + (hex!("0122222222333333334444444455000001da0000000000007470"), 0x0299e1), + (hex!("0122222222333333334444444455000001db0000000000001dc0"), 0x029a81), + (hex!("0122222222333333334444444455000001db00000000000045d0"), 0x029b21), + (hex!("0122222222333333334444444455000001dc0000000000001dd0"), 0x029bc1), + (hex!("0122222222333333334444444455000001dd0000000000001de0"), 0x029c61), + (hex!("0122222222333333334444444455000001dd0000000000004bb0"), 0x029d01), + (hex!("0122222222333333334444444455000001dd0000000000004cd0"), 0x029da1), + (hex!("0122222222333333334444444455000001dd0000000000006100"), 0x029e41), + (hex!("0122222222333333334444444455000001dd0000000000007bb0"), 0x029ee1), + (hex!("0122222222333333334444444455000001de0000000000001df0"), 0x029f81), + (hex!("0122222222333333334444444455000001de0000000000004260"), 0x02a021), + (hex!("0122222222333333334444444455000001de0000000000006040"), 0x02a0c1), + (hex!("0122222222333333334444444455000001df0000000000001e00"), 0x02a161), + (hex!("0122222222333333334444444455000001df0000000000005fa0"), 0x02a201), + (hex!("0122222222333333334444444455000001df0000000000006a70"), 0x02a2a1), + (hex!("0122222222333333334444444455000001df0000000000006dc0"), 0x02a341), + (hex!("0122222222333333334444444455000001e00000000000001e10"), 0x02a3e1), + (hex!("0122222222333333334444444455000001e00000000000007010"), 0x02a481), + (hex!("0122222222333333334444444455000001e10000000000001e20"), 0x02a521), + (hex!("0122222222333333334444444455000001e10000000000005720"), 0x02a5c1), + (hex!("0122222222333333334444444455000001e10000000000006830"), 0x02a661), + (hex!("0122222222333333334444444455000001e20000000000001e30"), 0x02a701), + (hex!("0122222222333333334444444455000001e20000000000005100"), 0x02a7a1), + (hex!("0122222222333333334444444455000001e30000000000001e40"), 0x02a841), + (hex!("0122222222333333334444444455000001e40000000000001e50"), 0x02a8e1), + (hex!("0122222222333333334444444455000001e40000000000003f30"), 0x02a981), + (hex!("0122222222333333334444444455000001e40000000000005220"), 0x02aa21), + (hex!("0122222222333333334444444455000001e50000000000001e60"), 0x02aac1), + (hex!("0122222222333333334444444455000001e50000000000006f60"), 0x02ab61), + (hex!("0122222222333333334444444455000001e60000000000001e70"), 0x02ac01), + (hex!("0122222222333333334444444455000001e60000000000006c80"), 0x02aca1), + (hex!("0122222222333333334444444455000001e70000000000001e80"), 0x02ad41), + (hex!("0122222222333333334444444455000001e80000000000001e90"), 0x02ade1), + (hex!("0122222222333333334444444455000001e80000000000004e30"), 0x02ae81), + (hex!("0122222222333333334444444455000001e90000000000001ea0"), 0x02af21), + (hex!("0122222222333333334444444455000001e90000000000005470"), 0x02afc1), + (hex!("0122222222333333334444444455000001ea0000000000001eb0"), 0x02b061), + (hex!("0122222222333333334444444455000001ea0000000000007980"), 0x02b101), + (hex!("0122222222333333334444444455000001eb0000000000001ec0"), 0x02b1a1), + (hex!("0122222222333333334444444455000001eb0000000000004390"), 0x02b241), + (hex!("0122222222333333334444444455000001eb0000000000005970"), 0x02b2e1), + (hex!("0122222222333333334444444455000001ec0000000000001ed0"), 0x02b381), + (hex!("0122222222333333334444444455000001ec0000000000005d50"), 0x02b421), + (hex!("0122222222333333334444444455000001ec00000000000076e0"), 0x02b4c1), + (hex!("0122222222333333334444444455000001ed0000000000001ee0"), 0x02b561), + (hex!("0122222222333333334444444455000001ed0000000000006190"), 0x02b601), + (hex!("0122222222333333334444444455000001ee0000000000001ef0"), 0x02b6a1), + (hex!("0122222222333333334444444455000001ee0000000000004900"), 0x02b741), + (hex!("0122222222333333334444444455000001ef0000000000001f00"), 0x02b7e1), + (hex!("0122222222333333334444444455000001ef0000000000006c60"), 0x02b881), + (hex!("0122222222333333334444444455000001f00000000000001f10"), 0x02b921), + (hex!("0122222222333333334444444455000001f00000000000006950"), 0x02b9c1), + (hex!("0122222222333333334444444455000001f10000000000001f20"), 0x02ba61), + (hex!("0122222222333333334444444455000001f10000000000006400"), 0x02bb01), + (hex!("0122222222333333334444444455000001f20000000000001f30"), 0x02bba1), + (hex!("0122222222333333334444444455000001f20000000000006f00"), 0x02bc41), + (hex!("0122222222333333334444444455000001f20000000000007b10"), 0x02bce1), + (hex!("0122222222333333334444444455000001f30000000000001f40"), 0x02bd81), + (hex!("0122222222333333334444444455000001f40000000000001f50"), 0x02be21), + (hex!("0122222222333333334444444455000001f50000000000001f60"), 0x02bec1), + (hex!("0122222222333333334444444455000001f500000000000044f0"), 0x02bf61), + (hex!("0122222222333333334444444455000001f60000000000001f70"), 0x02c001), + (hex!("0122222222333333334444444455000001f70000000000001f80"), 0x02c0a1), + (hex!("0122222222333333334444444455000001f70000000000004ad0"), 0x02c141), + (hex!("0122222222333333334444444455000001f80000000000001f90"), 0x02c1e1), + (hex!("0122222222333333334444444455000001f90000000000001fa0"), 0x02c281), + (hex!("0122222222333333334444444455000001f90000000000003f60"), 0x02c321), + (hex!("0122222222333333334444444455000001f90000000000004a80"), 0x02c3c1), + (hex!("0122222222333333334444444455000001fa0000000000001fb0"), 0x02c461), + (hex!("0122222222333333334444444455000001fa0000000000006f90"), 0x02c501), + (hex!("0122222222333333334444444455000001fb0000000000001fc0"), 0x02c5a1), + (hex!("0122222222333333334444444455000001fc0000000000001fd0"), 0x02c641), + (hex!("0122222222333333334444444455000001fc0000000000004a90"), 0x02c6e1), + (hex!("0122222222333333334444444455000001fd0000000000001fe0"), 0x02c781), + (hex!("0122222222333333334444444455000001fd0000000000005f50"), 0x02c821), + (hex!("0122222222333333334444444455000001fe0000000000001ff0"), 0x02c8c1), + (hex!("0122222222333333334444444455000001ff0000000000002000"), 0x02c961), + (hex!("0122222222333333334444444455000002000000000000002010"), 0x02ca01), + (hex!("0122222222333333334444444455000002000000000000005f00"), 0x02caa1), + (hex!("0122222222333333334444444455000002000000000000006840"), 0x02cb41), + (hex!("0122222222333333334444444455000002010000000000002020"), 0x02cbe1), + (hex!("0122222222333333334444444455000002020000000000002030"), 0x02cc81), + (hex!("0122222222333333334444444455000002030000000000002040"), 0x02cd21), + (hex!("0122222222333333334444444455000002040000000000002050"), 0x02cdc1), + (hex!("01222222223333333344444444550000020400000000000051f0"), 0x02ce61), + (hex!("0122222222333333334444444455000002050000000000002060"), 0x02cf01), + (hex!("0122222222333333334444444455000002060000000000002070"), 0x02cfa1), + (hex!("0122222222333333334444444455000002060000000000005c80"), 0x02d041), + (hex!("01222222223333333344444444550000020600000000000061d0"), 0x02d0e1), + (hex!("01222222223333333344444444550000020600000000000078c0"), 0x02d181), + (hex!("0122222222333333334444444455000002070000000000002080"), 0x02d221), + (hex!("0122222222333333334444444455000002070000000000006ba0"), 0x02d2c1), + (hex!("0122222222333333334444444455000002080000000000002090"), 0x02d361), + (hex!("01222222223333333344444444550000020900000000000020a0"), 0x02d401), + (hex!("01222222223333333344444444550000020900000000000067a0"), 0x02d4a1), + (hex!("01222222223333333344444444550000020a00000000000020b0"), 0x02d541), + (hex!("01222222223333333344444444550000020a0000000000004950"), 0x02d5e1), + (hex!("01222222223333333344444444550000020a0000000000004de0"), 0x02d681), + (hex!("01222222223333333344444444550000020b00000000000020c0"), 0x02d721), + (hex!("01222222223333333344444444550000020b0000000000004b00"), 0x02d7c1), + (hex!("01222222223333333344444444550000020c00000000000020d0"), 0x02d861), + (hex!("01222222223333333344444444550000020d00000000000020e0"), 0x02d901), + (hex!("01222222223333333344444444550000020e00000000000020f0"), 0x02d9a1), + (hex!("01222222223333333344444444550000020f0000000000002100"), 0x02da41), + (hex!("0122222222333333334444444455000002100000000000002110"), 0x02dae1), + (hex!("0122222222333333334444444455000002110000000000002120"), 0x02db81), + (hex!("0122222222333333334444444455000002110000000000004490"), 0x02dc21), + (hex!("0122222222333333334444444455000002120000000000002130"), 0x02dcc1), + (hex!("0122222222333333334444444455000002130000000000002140"), 0x02dd61), + (hex!("01222222223333333344444444550000021300000000000046d0"), 0x02de01), + (hex!("01222222223333333344444444550000021300000000000046e0"), 0x02dea1), + (hex!("0122222222333333334444444455000002130000000000004b70"), 0x02df41), + (hex!("0122222222333333334444444455000002140000000000002150"), 0x02dfe1), + (hex!("0122222222333333334444444455000002140000000000006c50"), 0x02e081), + (hex!("0122222222333333334444444455000002150000000000002160"), 0x02e121), + (hex!("01222222223333333344444444550000021500000000000043c0"), 0x02e1c1), + (hex!("0122222222333333334444444455000002160000000000002170"), 0x02e261), + (hex!("01222222223333333344444444550000021600000000000055b0"), 0x02e301), + (hex!("0122222222333333334444444455000002160000000000006150"), 0x02e3a1), + (hex!("0122222222333333334444444455000002170000000000002180"), 0x02e441), + (hex!("01222222223333333344444444550000021700000000000053b0"), 0x02e4e1), + (hex!("0122222222333333334444444455000002170000000000007460"), 0x02e581), + (hex!("0122222222333333334444444455000002180000000000002190"), 0x02e621), + (hex!("01222222223333333344444444550000021900000000000021a0"), 0x02e6c1), + (hex!("01222222223333333344444444550000021a00000000000021b0"), 0x02e761), + (hex!("01222222223333333344444444550000021a0000000000007650"), 0x02e801), + (hex!("01222222223333333344444444550000021b00000000000021c0"), 0x02e8a1), + (hex!("01222222223333333344444444550000021b0000000000004b20"), 0x02e941), + (hex!("01222222223333333344444444550000021c00000000000021d0"), 0x02e9e1), + (hex!("01222222223333333344444444550000021c0000000000007610"), 0x02ea81), + (hex!("01222222223333333344444444550000021d00000000000021e0"), 0x02eb21), + (hex!("01222222223333333344444444550000021d0000000000005f40"), 0x02ebc1), + (hex!("01222222223333333344444444550000021e00000000000021f0"), 0x02ec61), + (hex!("01222222223333333344444444550000021e0000000000005a50"), 0x02ed01), + (hex!("01222222223333333344444444550000021e0000000000005ff0"), 0x02eda1), + (hex!("01222222223333333344444444550000021f0000000000002200"), 0x02ee41), + (hex!("01222222223333333344444444550000021f00000000000043a0"), 0x02eee1), + (hex!("01222222223333333344444444550000021f0000000000004cb0"), 0x02ef81), + (hex!("01222222223333333344444444550000021f0000000000004e00"), 0x02f021), + (hex!("0122222222333333334444444455000002200000000000002210"), 0x02f0c1), + (hex!("0122222222333333334444444455000002210000000000002220"), 0x02f161), + (hex!("0122222222333333334444444455000002210000000000006290"), 0x02f201), + (hex!("0122222222333333334444444455000002210000000000007230"), 0x02f2a1), + (hex!("0122222222333333334444444455000002220000000000002230"), 0x02f341), + (hex!("0122222222333333334444444455000002220000000000006ea0"), 0x02f3e1), + (hex!("0122222222333333334444444455000002230000000000002240"), 0x02f481), + (hex!("0122222222333333334444444455000002230000000000004710"), 0x02f521), + (hex!("0122222222333333334444444455000002240000000000002250"), 0x02f5c1), + (hex!("0122222222333333334444444455000002250000000000002260"), 0x02f661), + (hex!("0122222222333333334444444455000002260000000000002270"), 0x02f701), + (hex!("0122222222333333334444444455000002260000000000005b40"), 0x02f7a1), + (hex!("0122222222333333334444444455000002260000000000006300"), 0x02f841), + (hex!("0122222222333333334444444455000002270000000000002280"), 0x02f8e1), + (hex!("0122222222333333334444444455000002270000000000005b80"), 0x02f981), + (hex!("0122222222333333334444444455000002280000000000002290"), 0x02fa21), + (hex!("0122222222333333334444444455000002280000000000003ed0"), 0x02fac1), + (hex!("0122222222333333334444444455000002280000000000004550"), 0x02fb61), + (hex!("01222222223333333344444444550000022800000000000077d0"), 0x02fc01), + (hex!("01222222223333333344444444550000022900000000000022a0"), 0x02fca1), + (hex!("0122222222333333334444444455000002290000000000006480"), 0x02fd41), + (hex!("01222222223333333344444444550000022a00000000000022b0"), 0x02fde1), + (hex!("01222222223333333344444444550000022a0000000000005450"), 0x02fe81), + (hex!("01222222223333333344444444550000022b00000000000022c0"), 0x02ff21), + (hex!("01222222223333333344444444550000022b0000000000006dd0"), 0x02ffc1), + (hex!("01222222223333333344444444550000022c00000000000022d0"), 0x030061), + (hex!("01222222223333333344444444550000022c0000000000006890"), 0x030101), + (hex!("01222222223333333344444444550000022d00000000000022e0"), 0x0301a1), + (hex!("01222222223333333344444444550000022e00000000000022f0"), 0x030241), + (hex!("01222222223333333344444444550000022e0000000000004f20"), 0x0302e1), + (hex!("01222222223333333344444444550000022f0000000000002300"), 0x030381), + (hex!("01222222223333333344444444550000022f0000000000005260"), 0x030421), + (hex!("01222222223333333344444444550000022f00000000000053f0"), 0x0304c1), + (hex!("0122222222333333334444444455000002300000000000002310"), 0x030561), + (hex!("01222222223333333344444444550000023000000000000050e0"), 0x030601), + (hex!("0122222222333333334444444455000002310000000000002320"), 0x0306a1), + (hex!("0122222222333333334444444455000002310000000000007800"), 0x030741), + (hex!("0122222222333333334444444455000002320000000000002330"), 0x0307e1), + (hex!("0122222222333333334444444455000002330000000000002340"), 0x030881), + (hex!("0122222222333333334444444455000002330000000000004d70"), 0x030921), + (hex!("0122222222333333334444444455000002330000000000005cf0"), 0x0309c1), + (hex!("0122222222333333334444444455000002340000000000002350"), 0x030a61), + (hex!("0122222222333333334444444455000002350000000000002360"), 0x030b01), + (hex!("0122222222333333334444444455000002350000000000006970"), 0x030ba1), + (hex!("0122222222333333334444444455000002360000000000002370"), 0x030c41), + (hex!("0122222222333333334444444455000002360000000000005270"), 0x030ce1), + (hex!("0122222222333333334444444455000002370000000000002380"), 0x030d81), + (hex!("0122222222333333334444444455000002370000000000005d70"), 0x030e21), + (hex!("0122222222333333334444444455000002380000000000002390"), 0x030ec1), + (hex!("01222222223333333344444444550000023800000000000069a0"), 0x030f61), + (hex!("01222222223333333344444444550000023900000000000023a0"), 0x031001), + (hex!("01222222223333333344444444550000023900000000000052e0"), 0x0310a1), + (hex!("0122222222333333334444444455000002390000000000005a10"), 0x031141), + (hex!("0122222222333333334444444455000002390000000000007440"), 0x0311e1), + (hex!("01222222223333333344444444550000023a00000000000023b0"), 0x031281), + (hex!("01222222223333333344444444550000023a0000000000003f00"), 0x031321), + (hex!("01222222223333333344444444550000023a0000000000004430"), 0x0313c1), + (hex!("01222222223333333344444444550000023a0000000000007070"), 0x031461), + (hex!("01222222223333333344444444550000023a00000000000074a0"), 0x031501), + (hex!("01222222223333333344444444550000023b00000000000023c0"), 0x0315a1), + (hex!("01222222223333333344444444550000023b0000000000004730"), 0x031641), + (hex!("01222222223333333344444444550000023b00000000000068b0"), 0x0316e1), + (hex!("01222222223333333344444444550000023c00000000000023d0"), 0x031781), + (hex!("01222222223333333344444444550000023c0000000000004680"), 0x031821), + (hex!("01222222223333333344444444550000023d00000000000023e0"), 0x0318c1), + (hex!("01222222223333333344444444550000023d00000000000059a0"), 0x031961), + (hex!("01222222223333333344444444550000023e00000000000023f0"), 0x031a01), + (hex!("01222222223333333344444444550000023f0000000000002400"), 0x031aa1), + (hex!("0122222222333333334444444455000002400000000000002410"), 0x031b41), + (hex!("0122222222333333334444444455000002400000000000004920"), 0x031be1), + (hex!("01222222223333333344444444550000024000000000000066e0"), 0x031c81), + (hex!("01222222223333333344444444550000024000000000000076f0"), 0x031d21), + (hex!("01222222223333333344444444550000024000000000000078e0"), 0x031dc1), + (hex!("0122222222333333334444444455000002410000000000002420"), 0x031e61), + (hex!("0122222222333333334444444455000002420000000000002430"), 0x031f01), + (hex!("0122222222333333334444444455000002420000000000006590"), 0x031fa1), + (hex!("0122222222333333334444444455000002430000000000002440"), 0x032041), + (hex!("0122222222333333334444444455000002430000000000004d00"), 0x0320e1), + (hex!("0122222222333333334444444455000002440000000000002450"), 0x032181), + (hex!("0122222222333333334444444455000002440000000000005f80"), 0x032221), + (hex!("0122222222333333334444444455000002450000000000002460"), 0x0322c1), + (hex!("0122222222333333334444444455000002450000000000004940"), 0x032361), + (hex!("0122222222333333334444444455000002460000000000002470"), 0x032401), + (hex!("0122222222333333334444444455000002470000000000002480"), 0x0324a1), + (hex!("0122222222333333334444444455000002470000000000004dd0"), 0x032541), + (hex!("0122222222333333334444444455000002470000000000005930"), 0x0325e1), + (hex!("01222222223333333344444444550000024700000000000061b0"), 0x032681), + (hex!("0122222222333333334444444455000002470000000000007740"), 0x032721), + (hex!("0122222222333333334444444455000002480000000000002490"), 0x0327c1), + (hex!("0122222222333333334444444455000002480000000000004890"), 0x032861), + (hex!("01222222223333333344444444550000024900000000000024a0"), 0x032901), + (hex!("01222222223333333344444444550000024a00000000000024b0"), 0x0329a1), + (hex!("01222222223333333344444444550000024b00000000000024c0"), 0x032a41), + (hex!("01222222223333333344444444550000024c00000000000024d0"), 0x032ae1), + (hex!("01222222223333333344444444550000024d00000000000024e0"), 0x032b81), + (hex!("01222222223333333344444444550000024d0000000000004070"), 0x032c21), + (hex!("01222222223333333344444444550000024e00000000000024f0"), 0x032cc1), + (hex!("01222222223333333344444444550000024e00000000000066a0"), 0x032d61), + (hex!("01222222223333333344444444550000024e0000000000006ab0"), 0x032e01), + (hex!("01222222223333333344444444550000024f0000000000002500"), 0x032ea1), + (hex!("0122222222333333334444444455000002500000000000002510"), 0x032f41), + (hex!("0122222222333333334444444455000002510000000000002520"), 0x032fe1), + (hex!("0122222222333333334444444455000002510000000000007320"), 0x033081), + (hex!("0122222222333333334444444455000002520000000000002530"), 0x033121), + (hex!("0122222222333333334444444455000002520000000000006410"), 0x0331c1), + (hex!("0122222222333333334444444455000002530000000000002540"), 0x033261), + (hex!("0122222222333333334444444455000002530000000000005110"), 0x033301), + (hex!("0122222222333333334444444455000002540000000000002550"), 0x0333a1), + (hex!("01222222223333333344444444550000025400000000000040c0"), 0x033441), + (hex!("0122222222333333334444444455000002540000000000006a40"), 0x0334e1), + (hex!("0122222222333333334444444455000002550000000000002560"), 0x033581), + (hex!("0122222222333333334444444455000002550000000000005190"), 0x033621), + (hex!("0122222222333333334444444455000002560000000000002570"), 0x0336c1), + (hex!("01222222223333333344444444550000025600000000000061f0"), 0x033761), + (hex!("0122222222333333334444444455000002570000000000002580"), 0x033801), + (hex!("0122222222333333334444444455000002580000000000002590"), 0x0338a1), + (hex!("01222222223333333344444444550000025800000000000043d0"), 0x033941), + (hex!("01222222223333333344444444550000025900000000000025a0"), 0x0339e1), + (hex!("0122222222333333334444444455000002590000000000006bb0"), 0x033a81), + (hex!("01222222223333333344444444550000025a00000000000025b0"), 0x033b21), + (hex!("01222222223333333344444444550000025a0000000000005fb0"), 0x033bc1), + (hex!("01222222223333333344444444550000025a00000000000064c0"), 0x033c61), + (hex!("01222222223333333344444444550000025b00000000000025c0"), 0x033d01), + (hex!("01222222223333333344444444550000025b0000000000005c10"), 0x033da1), + (hex!("01222222223333333344444444550000025c00000000000025d0"), 0x033e41), + (hex!("01222222223333333344444444550000025c0000000000007d00"), 0x033ee1), + (hex!("01222222223333333344444444550000025d00000000000025e0"), 0x033f81), + (hex!("01222222223333333344444444550000025e00000000000025f0"), 0x034021), + (hex!("01222222223333333344444444550000025e00000000000045e0"), 0x0340c1), + (hex!("01222222223333333344444444550000025e0000000000006ee0"), 0x034161), + (hex!("01222222223333333344444444550000025f0000000000002600"), 0x034201), + (hex!("01222222223333333344444444550000025f00000000000050b0"), 0x0342a1), + (hex!("01222222223333333344444444550000025f0000000000007690"), 0x034341), + (hex!("0122222222333333334444444455000002600000000000002610"), 0x0343e1), + (hex!("0122222222333333334444444455000002600000000000007b60"), 0x034481), + (hex!("0122222222333333334444444455000002610000000000002620"), 0x034521), + (hex!("0122222222333333334444444455000002620000000000002630"), 0x0345c1), + (hex!("0122222222333333334444444455000002630000000000002640"), 0x034661), + (hex!("0122222222333333334444444455000002640000000000002650"), 0x034701), + (hex!("0122222222333333334444444455000002650000000000002660"), 0x0347a1), + (hex!("0122222222333333334444444455000002650000000000006180"), 0x034841), + (hex!("0122222222333333334444444455000002660000000000002670"), 0x0348e1), + (hex!("0122222222333333334444444455000002660000000000005430"), 0x034981), + (hex!("0122222222333333334444444455000002660000000000007a60"), 0x034a21), + (hex!("0122222222333333334444444455000002670000000000002680"), 0x034ac1), + (hex!("01222222223333333344444444550000026700000000000077f0"), 0x034b61), + (hex!("0122222222333333334444444455000002680000000000002690"), 0x034c01), + (hex!("01222222223333333344444444550000026900000000000026a0"), 0x034ca1), + (hex!("01222222223333333344444444550000026a00000000000026b0"), 0x034d41), + (hex!("01222222223333333344444444550000026a0000000000007530"), 0x034de1), + (hex!("01222222223333333344444444550000026b00000000000026c0"), 0x034e81), + (hex!("01222222223333333344444444550000026b00000000000058b0"), 0x034f21), + (hex!("01222222223333333344444444550000026b00000000000066b0"), 0x034fc1), + (hex!("01222222223333333344444444550000026b0000000000006b10"), 0x035061), + (hex!("01222222223333333344444444550000026c00000000000026d0"), 0x035101), + (hex!("01222222223333333344444444550000026d00000000000026e0"), 0x0351a1), + (hex!("01222222223333333344444444550000026d0000000000004210"), 0x035241), + (hex!("01222222223333333344444444550000026d0000000000005490"), 0x0352e1), + (hex!("01222222223333333344444444550000026d0000000000005e60"), 0x035381), + (hex!("01222222223333333344444444550000026d00000000000068e0"), 0x035421), + (hex!("01222222223333333344444444550000026d0000000000007020"), 0x0354c1), + (hex!("01222222223333333344444444550000026d0000000000007300"), 0x035561), + (hex!("01222222223333333344444444550000026e00000000000026f0"), 0x035601), + (hex!("01222222223333333344444444550000026f0000000000002700"), 0x0356a1), + (hex!("01222222223333333344444444550000026f0000000000004910"), 0x035741), + (hex!("0122222222333333334444444455000002700000000000002710"), 0x0357e1), + (hex!("0122222222333333334444444455000002710000000000002720"), 0x035881), + (hex!("01222222223333333344444444550000027100000000000050c0"), 0x035921), + (hex!("0122222222333333334444444455000002720000000000002730"), 0x0359c1), + (hex!("0122222222333333334444444455000002730000000000002740"), 0x035a61), + (hex!("0122222222333333334444444455000002740000000000002750"), 0x035b01), + (hex!("0122222222333333334444444455000002740000000000007490"), 0x035ba1), + (hex!("0122222222333333334444444455000002750000000000002760"), 0x035c41), + (hex!("0122222222333333334444444455000002760000000000002770"), 0x035ce1), + (hex!("0122222222333333334444444455000002760000000000004790"), 0x035d81), + (hex!("0122222222333333334444444455000002770000000000002780"), 0x035e21), + (hex!("01222222223333333344444444550000027700000000000050a0"), 0x035ec1), + (hex!("0122222222333333334444444455000002780000000000002790"), 0x035f61), + (hex!("0122222222333333334444444455000002780000000000004330"), 0x036001), + (hex!("0122222222333333334444444455000002780000000000006b00"), 0x0360a1), + (hex!("01222222223333333344444444550000027900000000000027a0"), 0x036141), + (hex!("01222222223333333344444444550000027a00000000000027b0"), 0x0361e1), + (hex!("01222222223333333344444444550000027b00000000000027c0"), 0x036281), + (hex!("01222222223333333344444444550000027b0000000000004930"), 0x036321), + (hex!("01222222223333333344444444550000027b0000000000006250"), 0x0363c1), + (hex!("01222222223333333344444444550000027c00000000000027d0"), 0x036461), + (hex!("01222222223333333344444444550000027d00000000000027e0"), 0x036501), + (hex!("01222222223333333344444444550000027d0000000000005ce0"), 0x0365a1), + (hex!("01222222223333333344444444550000027d0000000000005fe0"), 0x036641), + (hex!("01222222223333333344444444550000027e00000000000027f0"), 0x0366e1), + (hex!("01222222223333333344444444550000027f0000000000002800"), 0x036781), + (hex!("01222222223333333344444444550000027f0000000000003e90"), 0x036821), + (hex!("01222222223333333344444444550000027f0000000000007910"), 0x0368c1), + (hex!("0122222222333333334444444455000002800000000000002810"), 0x036961), + (hex!("0122222222333333334444444455000002800000000000004990"), 0x036a01), + (hex!("0122222222333333334444444455000002800000000000006160"), 0x036aa1), + (hex!("0122222222333333334444444455000002800000000000006740"), 0x036b41), + (hex!("0122222222333333334444444455000002810000000000002820"), 0x036be1), + (hex!("0122222222333333334444444455000002820000000000002830"), 0x036c81), + (hex!("0122222222333333334444444455000002820000000000005170"), 0x036d21), + (hex!("0122222222333333334444444455000002830000000000002840"), 0x036dc1), + (hex!("0122222222333333334444444455000002840000000000002850"), 0x036e61), + (hex!("0122222222333333334444444455000002840000000000004810"), 0x036f01), + (hex!("0122222222333333334444444455000002840000000000006aa0"), 0x036fa1), + (hex!("0122222222333333334444444455000002850000000000002860"), 0x037041), + (hex!("0122222222333333334444444455000002860000000000002870"), 0x0370e1), + (hex!("0122222222333333334444444455000002860000000000005080"), 0x037181), + (hex!("0122222222333333334444444455000002870000000000002880"), 0x037221), + (hex!("0122222222333333334444444455000002870000000000004e60"), 0x0372c1), + (hex!("0122222222333333334444444455000002880000000000002890"), 0x037361), + (hex!("0122222222333333334444444455000002880000000000005060"), 0x037401), + (hex!("0122222222333333334444444455000002880000000000006f20"), 0x0374a1), + (hex!("01222222223333333344444444550000028900000000000028a0"), 0x037541), + (hex!("01222222223333333344444444550000028900000000000047e0"), 0x0375e1), + (hex!("01222222223333333344444444550000028a00000000000028b0"), 0x037681), + (hex!("01222222223333333344444444550000028a0000000000005ab0"), 0x037721), + (hex!("01222222223333333344444444550000028a0000000000007130"), 0x0377c1), + (hex!("01222222223333333344444444550000028a0000000000007660"), 0x037861), + (hex!("01222222223333333344444444550000028b00000000000028c0"), 0x037901), + (hex!("01222222223333333344444444550000028b00000000000054e0"), 0x0379a1), + (hex!("01222222223333333344444444550000028c00000000000028d0"), 0x037a41), + (hex!("01222222223333333344444444550000028c00000000000046f0"), 0x037ae1), + (hex!("01222222223333333344444444550000028c00000000000061a0"), 0x037b81), + (hex!("01222222223333333344444444550000028d00000000000028e0"), 0x037c21), + (hex!("01222222223333333344444444550000028e00000000000028f0"), 0x037cc1), + (hex!("01222222223333333344444444550000028e0000000000004130"), 0x037d61), + (hex!("01222222223333333344444444550000028f0000000000002900"), 0x037e01), + (hex!("01222222223333333344444444550000028f0000000000007510"), 0x037ea1), + (hex!("0122222222333333334444444455000002900000000000002910"), 0x037f41), + (hex!("0122222222333333334444444455000002900000000000004a40"), 0x037fe1), + (hex!("0122222222333333334444444455000002910000000000002920"), 0x038081), + (hex!("0122222222333333334444444455000002920000000000002930"), 0x038121), + (hex!("0122222222333333334444444455000002920000000000004e90"), 0x0381c1), + (hex!("0122222222333333334444444455000002930000000000002940"), 0x038261), + (hex!("0122222222333333334444444455000002930000000000006880"), 0x038301), + (hex!("0122222222333333334444444455000002940000000000002950"), 0x0383a1), + (hex!("0122222222333333334444444455000002940000000000007bc0"), 0x038441), + (hex!("0122222222333333334444444455000002950000000000002960"), 0x0384e1), + (hex!("0122222222333333334444444455000002960000000000002970"), 0x038581), + (hex!("01222222223333333344444444550000029600000000000059d0"), 0x038621), + (hex!("0122222222333333334444444455000002970000000000002980"), 0x0386c1), + (hex!("0122222222333333334444444455000002970000000000004a50"), 0x038761), + (hex!("0122222222333333334444444455000002970000000000005f20"), 0x038801), + (hex!("01222222223333333344444444550000029700000000000068d0"), 0x0388a1), + (hex!("0122222222333333334444444455000002980000000000002990"), 0x038941), + (hex!("0122222222333333334444444455000002980000000000004370"), 0x0389e1), + (hex!("0122222222333333334444444455000002980000000000004420"), 0x038a81), + (hex!("01222222223333333344444444550000029900000000000029a0"), 0x038b21), + (hex!("01222222223333333344444444550000029a00000000000029b0"), 0x038bc1), + (hex!("01222222223333333344444444550000029a0000000000006010"), 0x038c61), + (hex!("01222222223333333344444444550000029a0000000000006980"), 0x038d01), + (hex!("01222222223333333344444444550000029b00000000000029c0"), 0x038da1), + (hex!("01222222223333333344444444550000029c00000000000029d0"), 0x038e41), + (hex!("01222222223333333344444444550000029c0000000000007480"), 0x038ee1), + (hex!("01222222223333333344444444550000029d00000000000029e0"), 0x038f81), + (hex!("01222222223333333344444444550000029d0000000000005030"), 0x039021), + (hex!("01222222223333333344444444550000029d0000000000007780"), 0x0390c1), + (hex!("01222222223333333344444444550000029d0000000000007a50"), 0x039161), + (hex!("01222222223333333344444444550000029e00000000000029f0"), 0x039201), + (hex!("01222222223333333344444444550000029e00000000000074b0"), 0x0392a1), + (hex!("01222222223333333344444444550000029f0000000000002a00"), 0x039341), + (hex!("0122222222333333334444444455000002a00000000000002a10"), 0x0393e1), + (hex!("0122222222333333334444444455000002a10000000000002a20"), 0x039481), + (hex!("0122222222333333334444444455000002a20000000000002a30"), 0x039521), + (hex!("0122222222333333334444444455000002a20000000000004c50"), 0x0395c1), + (hex!("0122222222333333334444444455000002a20000000000006f10"), 0x039661), + (hex!("0122222222333333334444444455000002a30000000000002a40"), 0x039701), + (hex!("0122222222333333334444444455000002a40000000000002a50"), 0x0397a1), + (hex!("0122222222333333334444444455000002a40000000000005d60"), 0x039841), + (hex!("0122222222333333334444444455000002a50000000000002a60"), 0x0398e1), + (hex!("0122222222333333334444444455000002a50000000000005440"), 0x039981), + (hex!("0122222222333333334444444455000002a50000000000005890"), 0x039a21), + (hex!("0122222222333333334444444455000002a60000000000002a70"), 0x039ac1), + (hex!("0122222222333333334444444455000002a70000000000002a80"), 0x039b61), + (hex!("0122222222333333334444444455000002a700000000000054a0"), 0x039c01), + (hex!("0122222222333333334444444455000002a70000000000007280"), 0x039ca1), + (hex!("0122222222333333334444444455000002a80000000000002a90"), 0x039d41), + (hex!("0122222222333333334444444455000002a90000000000002aa0"), 0x039de1), + (hex!("0122222222333333334444444455000002aa0000000000002ab0"), 0x039e81), + (hex!("0122222222333333334444444455000002ab0000000000002ac0"), 0x039f21), + (hex!("0122222222333333334444444455000002ab0000000000006c90"), 0x039fc1), + (hex!("0122222222333333334444444455000002ac0000000000002ad0"), 0x03a061), + (hex!("0122222222333333334444444455000002ac0000000000006db0"), 0x03a101), + (hex!("0122222222333333334444444455000002ad0000000000002ae0"), 0x03a1a1), + (hex!("0122222222333333334444444455000002ad00000000000065e0"), 0x03a241), + (hex!("0122222222333333334444444455000002ad0000000000007b40"), 0x03a2e1), + (hex!("0122222222333333334444444455000002ae0000000000002af0"), 0x03a381), + (hex!("0122222222333333334444444455000002ae0000000000004d20"), 0x03a421), + (hex!("0122222222333333334444444455000002ae0000000000006f30"), 0x03a4c1), + (hex!("0122222222333333334444444455000002af0000000000002b00"), 0x03a561), + (hex!("0122222222333333334444444455000002b00000000000002b10"), 0x03a601), + (hex!("0122222222333333334444444455000002b00000000000004560"), 0x03a6a1), + (hex!("0122222222333333334444444455000002b00000000000005800"), 0x03a741), + (hex!("0122222222333333334444444455000002b00000000000005a60"), 0x03a7e1), + (hex!("0122222222333333334444444455000002b10000000000002b20"), 0x03a881), + (hex!("0122222222333333334444444455000002b10000000000007b30"), 0x03a921), + (hex!("0122222222333333334444444455000002b20000000000002b30"), 0x03a9c1), + (hex!("0122222222333333334444444455000002b20000000000004440"), 0x03aa61), + (hex!("0122222222333333334444444455000002b20000000000004f80"), 0x03ab01), + (hex!("0122222222333333334444444455000002b20000000000005020"), 0x03aba1), + (hex!("0122222222333333334444444455000002b30000000000002b40"), 0x03ac41), + (hex!("0122222222333333334444444455000002b40000000000002b50"), 0x03ace1), + (hex!("0122222222333333334444444455000002b50000000000002b60"), 0x03ad81), + (hex!("0122222222333333334444444455000002b500000000000059e0"), 0x03ae21), + (hex!("0122222222333333334444444455000002b60000000000002b70"), 0x03aec1), + (hex!("0122222222333333334444444455000002b70000000000002b80"), 0x03af61), + (hex!("0122222222333333334444444455000002b80000000000002b90"), 0x03b001), + (hex!("0122222222333333334444444455000002b80000000000004590"), 0x03b0a1), + (hex!("0122222222333333334444444455000002b800000000000047d0"), 0x03b141), + (hex!("0122222222333333334444444455000002b80000000000006030"), 0x03b1e1), + (hex!("0122222222333333334444444455000002b80000000000006a20"), 0x03b281), + (hex!("0122222222333333334444444455000002b80000000000006a90"), 0x03b321), + (hex!("0122222222333333334444444455000002b90000000000002ba0"), 0x03b3c1), + (hex!("0122222222333333334444444455000002ba0000000000002bb0"), 0x03b461), + (hex!("0122222222333333334444444455000002ba0000000000006e80"), 0x03b501), + (hex!("0122222222333333334444444455000002bb0000000000002bc0"), 0x03b5a1), + (hex!("0122222222333333334444444455000002bc0000000000002bd0"), 0x03b641), + (hex!("0122222222333333334444444455000002bc0000000000004b30"), 0x03b6e1), + (hex!("0122222222333333334444444455000002bd0000000000002be0"), 0x03b781), + (hex!("0122222222333333334444444455000002bd0000000000005e10"), 0x03b821), + (hex!("0122222222333333334444444455000002be0000000000002bf0"), 0x03b8c1), + (hex!("0122222222333333334444444455000002bf0000000000002c00"), 0x03b961), + (hex!("0122222222333333334444444455000002c00000000000002c10"), 0x03ba01), + (hex!("0122222222333333334444444455000002c10000000000002c20"), 0x03baa1), + (hex!("0122222222333333334444444455000002c10000000000003ef0"), 0x03bb41), + (hex!("0122222222333333334444444455000002c20000000000002c30"), 0x03bbe1), + (hex!("0122222222333333334444444455000002c200000000000056e0"), 0x03bc81), + (hex!("0122222222333333334444444455000002c30000000000002c40"), 0x03bd21), + (hex!("0122222222333333334444444455000002c30000000000004b60"), 0x03bdc1), + (hex!("0122222222333333334444444455000002c40000000000002c50"), 0x03be61), + (hex!("0122222222333333334444444455000002c400000000000045f0"), 0x03bf01), + (hex!("0122222222333333334444444455000002c40000000000005290"), 0x03bfa1), + (hex!("0122222222333333334444444455000002c50000000000002c60"), 0x03c041), + (hex!("0122222222333333334444444455000002c60000000000002c70"), 0x03c0e1), + (hex!("0122222222333333334444444455000002c60000000000006ae0"), 0x03c181), + (hex!("0122222222333333334444444455000002c70000000000002c80"), 0x03c221), + (hex!("0122222222333333334444444455000002c70000000000005680"), 0x03c2c1), + (hex!("0122222222333333334444444455000002c70000000000006e10"), 0x03c361), + (hex!("0122222222333333334444444455000002c80000000000002c90"), 0x03c401), + (hex!("0122222222333333334444444455000002c90000000000002ca0"), 0x03c4a1), + (hex!("0122222222333333334444444455000002ca0000000000002cb0"), 0x03c541), + (hex!("0122222222333333334444444455000002cb0000000000002cc0"), 0x03c5e1), + (hex!("0122222222333333334444444455000002cc0000000000002cd0"), 0x03c681), + (hex!("0122222222333333334444444455000002cc0000000000005b50"), 0x03c721), + (hex!("0122222222333333334444444455000002cd0000000000002ce0"), 0x03c7c1), + (hex!("0122222222333333334444444455000002ce0000000000002cf0"), 0x03c861), + (hex!("0122222222333333334444444455000002ce00000000000043f0"), 0x03c901), + (hex!("0122222222333333334444444455000002ce0000000000006420"), 0x03c9a1), + (hex!("0122222222333333334444444455000002cf0000000000002d00"), 0x03ca41), + (hex!("0122222222333333334444444455000002d00000000000002d10"), 0x03cae1), + (hex!("0122222222333333334444444455000002d10000000000002d20"), 0x03cb81), + (hex!("0122222222333333334444444455000002d10000000000005370"), 0x03cc21), + (hex!("0122222222333333334444444455000002d20000000000002d30"), 0x03ccc1), + (hex!("0122222222333333334444444455000002d20000000000005ef0"), 0x03cd61), + (hex!("0122222222333333334444444455000002d20000000000006570"), 0x03ce01), + (hex!("0122222222333333334444444455000002d30000000000002d40"), 0x03cea1), + (hex!("0122222222333333334444444455000002d30000000000007360"), 0x03cf41), + (hex!("0122222222333333334444444455000002d40000000000002d50"), 0x03cfe1), + (hex!("0122222222333333334444444455000002d400000000000079a0"), 0x03d081), + (hex!("0122222222333333334444444455000002d50000000000002d60"), 0x03d121), + (hex!("0122222222333333334444444455000002d50000000000004250"), 0x03d1c1), + (hex!("0122222222333333334444444455000002d50000000000006050"), 0x03d261), + (hex!("0122222222333333334444444455000002d60000000000002d70"), 0x03d301), + (hex!("0122222222333333334444444455000002d60000000000007080"), 0x03d3a1), + (hex!("0122222222333333334444444455000002d70000000000002d80"), 0x03d441), + (hex!("0122222222333333334444444455000002d80000000000002d90"), 0x03d4e1), + (hex!("0122222222333333334444444455000002d80000000000007110"), 0x03d581), + (hex!("0122222222333333334444444455000002d800000000000073c0"), 0x03d621), + (hex!("0122222222333333334444444455000002d800000000000075a0"), 0x03d6c1), + (hex!("0122222222333333334444444455000002d90000000000002da0"), 0x03d761), + (hex!("0122222222333333334444444455000002d90000000000004860"), 0x03d801), + (hex!("0122222222333333334444444455000002d90000000000006b60"), 0x03d8a1), + (hex!("0122222222333333334444444455000002da0000000000002db0"), 0x03d941), + (hex!("0122222222333333334444444455000002da0000000000006630"), 0x03d9e1), + (hex!("0122222222333333334444444455000002db0000000000002dc0"), 0x03da81), + (hex!("0122222222333333334444444455000002dc0000000000002dd0"), 0x03db21), + (hex!("0122222222333333334444444455000002dc0000000000004830"), 0x03dbc1), + (hex!("0122222222333333334444444455000002dd0000000000002de0"), 0x03dc61), + (hex!("0122222222333333334444444455000002de0000000000002df0"), 0x03dd01), + (hex!("0122222222333333334444444455000002de0000000000004f00"), 0x03dda1), + (hex!("0122222222333333334444444455000002df0000000000002e00"), 0x03de41), + (hex!("0122222222333333334444444455000002e00000000000002e10"), 0x03dee1), + (hex!("0122222222333333334444444455000002e10000000000002e20"), 0x03df81), + (hex!("0122222222333333334444444455000002e10000000000006e90"), 0x03e021), + (hex!("0122222222333333334444444455000002e20000000000002e30"), 0x03e0c1), + (hex!("0122222222333333334444444455000002e200000000000053e0"), 0x03e161), + (hex!("0122222222333333334444444455000002e30000000000002e40"), 0x03e201), + (hex!("0122222222333333334444444455000002e30000000000006020"), 0x03e2a1), + (hex!("0122222222333333334444444455000002e30000000000006540"), 0x03e341), + (hex!("0122222222333333334444444455000002e40000000000002e50"), 0x03e3e1), + (hex!("0122222222333333334444444455000002e50000000000002e60"), 0x03e481), + (hex!("0122222222333333334444444455000002e50000000000005180"), 0x03e521), + (hex!("0122222222333333334444444455000002e50000000000007bf0"), 0x03e5c1), + (hex!("0122222222333333334444444455000002e60000000000002e70"), 0x03e661), + (hex!("0122222222333333334444444455000002e60000000000005350"), 0x03e701), + (hex!("0122222222333333334444444455000002e60000000000007960"), 0x03e7a1), + (hex!("0122222222333333334444444455000002e70000000000002e80"), 0x03e841), + (hex!("0122222222333333334444444455000002e80000000000002e90"), 0x03e8e1), + (hex!("0122222222333333334444444455000002e90000000000002ea0"), 0x03e981), + (hex!("0122222222333333334444444455000002ea0000000000002eb0"), 0x03ea21), + (hex!("0122222222333333334444444455000002eb0000000000002ec0"), 0x03eac1), + (hex!("0122222222333333334444444455000002ec0000000000002ed0"), 0x03eb61), + (hex!("0122222222333333334444444455000002ec0000000000006c10"), 0x03ec01), + (hex!("0122222222333333334444444455000002ed0000000000002ee0"), 0x03eca1), + (hex!("0122222222333333334444444455000002ed0000000000005590"), 0x03ed41), + (hex!("0122222222333333334444444455000002ed0000000000005cd0"), 0x03ede1), + (hex!("0122222222333333334444444455000002ed0000000000006910"), 0x03ee81), + (hex!("0122222222333333334444444455000002ee0000000000002ef0"), 0x03ef21), + (hex!("0122222222333333334444444455000002ef0000000000002f00"), 0x03efc1), + (hex!("0122222222333333334444444455000002ef0000000000004ed0"), 0x03f061), + (hex!("0122222222333333334444444455000002f00000000000002f10"), 0x03f101), + (hex!("0122222222333333334444444455000002f00000000000004cf0"), 0x03f1a1), + (hex!("0122222222333333334444444455000002f00000000000005d10"), 0x03f241), + (hex!("0122222222333333334444444455000002f00000000000006860"), 0x03f2e1), + (hex!("0122222222333333334444444455000002f00000000000006b50"), 0x03f381), + (hex!("0122222222333333334444444455000002f00000000000007100"), 0x03f421), + (hex!("0122222222333333334444444455000002f00000000000007aa0"), 0x03f4c1), + (hex!("0122222222333333334444444455000002f10000000000002f20"), 0x03f561), + (hex!("0122222222333333334444444455000002f20000000000002f30"), 0x03f601), + (hex!("0122222222333333334444444455000002f200000000000044b0"), 0x03f6a1), + (hex!("0122222222333333334444444455000002f30000000000002f40"), 0x03f741), + (hex!("0122222222333333334444444455000002f300000000000075b0"), 0x03f7e1), + (hex!("0122222222333333334444444455000002f40000000000002f50"), 0x03f881), + (hex!("0122222222333333334444444455000002f400000000000060f0"), 0x03f921), + (hex!("0122222222333333334444444455000002f50000000000002f60"), 0x03f9c1), + (hex!("0122222222333333334444444455000002f50000000000007210"), 0x03fa61), + (hex!("0122222222333333334444444455000002f60000000000002f70"), 0x03fb01), + (hex!("0122222222333333334444444455000002f60000000000006610"), 0x03fba1), + (hex!("0122222222333333334444444455000002f70000000000002f80"), 0x03fc41), + (hex!("0122222222333333334444444455000002f70000000000007560"), 0x03fce1), + (hex!("0122222222333333334444444455000002f80000000000002f90"), 0x03fd81), + (hex!("0122222222333333334444444455000002f80000000000006320"), 0x03fe21), + (hex!("0122222222333333334444444455000002f90000000000002fa0"), 0x03fec1), + (hex!("0122222222333333334444444455000002f90000000000006e50"), 0x03ff61), + (hex!("0122222222333333334444444455000002fa0000000000002fb0"), 0x040001), + (hex!("0122222222333333334444444455000002fb0000000000002fc0"), 0x0400a1), + (hex!("0122222222333333334444444455000002fb0000000000004780"), 0x040141), + (hex!("0122222222333333334444444455000002fc0000000000002fd0"), 0x0401e1), + (hex!("0122222222333333334444444455000002fd0000000000002fe0"), 0x040281), + (hex!("0122222222333333334444444455000002fd0000000000005600"), 0x040321), + (hex!("0122222222333333334444444455000002fd0000000000006c00"), 0x0403c1), + (hex!("0122222222333333334444444455000002fe0000000000002ff0"), 0x040461), + (hex!("0122222222333333334444444455000002ff0000000000003000"), 0x040501), + (hex!("0122222222333333334444444455000003000000000000003010"), 0x0405a1), + (hex!("0122222222333333334444444455000003000000000000004080"), 0x040641), + (hex!("0122222222333333334444444455000003010000000000003020"), 0x0406e1), + (hex!("0122222222333333334444444455000003010000000000006340"), 0x040781), + (hex!("0122222222333333334444444455000003020000000000003030"), 0x040821), + (hex!("0122222222333333334444444455000003020000000000005b00"), 0x0408c1), + (hex!("0122222222333333334444444455000003020000000000007b20"), 0x040961), + (hex!("0122222222333333334444444455000003030000000000003040"), 0x040a01), + (hex!("01222222223333333344444444550000030300000000000056b0"), 0x040aa1), + (hex!("0122222222333333334444444455000003030000000000006280"), 0x040b41), + (hex!("0122222222333333334444444455000003030000000000007ad0"), 0x040be1), + (hex!("0122222222333333334444444455000003040000000000003050"), 0x040c81), + (hex!("0122222222333333334444444455000003040000000000005c50"), 0x040d21), + (hex!("0122222222333333334444444455000003050000000000003060"), 0x040dc1), + (hex!("01222222223333333344444444550000030500000000000072e0"), 0x040e61), + (hex!("0122222222333333334444444455000003060000000000003070"), 0x040f01), + (hex!("0122222222333333334444444455000003060000000000004360"), 0x040fa1), + (hex!("0122222222333333334444444455000003060000000000004380"), 0x041041), + (hex!("0122222222333333334444444455000003060000000000004820"), 0x0410e1), + (hex!("0122222222333333334444444455000003060000000000006d10"), 0x041181), + (hex!("0122222222333333334444444455000003070000000000003080"), 0x041221), + (hex!("0122222222333333334444444455000003070000000000004450"), 0x0412c1), + (hex!("0122222222333333334444444455000003080000000000003090"), 0x041361), + (hex!("0122222222333333334444444455000003080000000000005ad0"), 0x041401), + (hex!("01222222223333333344444444550000030900000000000030a0"), 0x0414a1), + (hex!("01222222223333333344444444550000030a00000000000030b0"), 0x041541), + (hex!("01222222223333333344444444550000030a0000000000007760"), 0x0415e1), + (hex!("01222222223333333344444444550000030b00000000000030c0"), 0x041681), + (hex!("01222222223333333344444444550000030b0000000000007a80"), 0x041721), + (hex!("01222222223333333344444444550000030c00000000000030d0"), 0x0417c1), + (hex!("01222222223333333344444444550000030d00000000000030e0"), 0x041861), + (hex!("01222222223333333344444444550000030d0000000000003eb0"), 0x041901), + (hex!("01222222223333333344444444550000030e00000000000030f0"), 0x0419a1), + (hex!("01222222223333333344444444550000030f0000000000003100"), 0x041a41), + (hex!("01222222223333333344444444550000030f0000000000004690"), 0x041ae1), + (hex!("01222222223333333344444444550000030f0000000000006900"), 0x041b81), + (hex!("0122222222333333334444444455000003100000000000003110"), 0x041c21), + (hex!("01222222223333333344444444550000031000000000000058a0"), 0x041cc1), + (hex!("0122222222333333334444444455000003110000000000003120"), 0x041d61), + (hex!("0122222222333333334444444455000003110000000000004200"), 0x041e01), + (hex!("0122222222333333334444444455000003120000000000003130"), 0x041ea1), + (hex!("0122222222333333334444444455000003130000000000003140"), 0x041f41), + (hex!("0122222222333333334444444455000003130000000000004d50"), 0x041fe1), + (hex!("0122222222333333334444444455000003130000000000005400"), 0x042081), + (hex!("0122222222333333334444444455000003130000000000005520"), 0x042121), + (hex!("0122222222333333334444444455000003140000000000003150"), 0x0421c1), + (hex!("0122222222333333334444444455000003140000000000006450"), 0x042261), + (hex!("0122222222333333334444444455000003150000000000003160"), 0x042301), + (hex!("01222222223333333344444444550000031500000000000062d0"), 0x0423a1), + (hex!("0122222222333333334444444455000003160000000000003170"), 0x042441), + (hex!("0122222222333333334444444455000003160000000000004c40"), 0x0424e1), + (hex!("0122222222333333334444444455000003160000000000007c80"), 0x042581), + (hex!("0122222222333333334444444455000003170000000000003180"), 0x042621), + (hex!("0122222222333333334444444455000003170000000000004400"), 0x0426c1), + (hex!("0122222222333333334444444455000003170000000000005090"), 0x042761), + (hex!("0122222222333333334444444455000003170000000000006cb0"), 0x042801), + (hex!("0122222222333333334444444455000003180000000000003190"), 0x0428a1), + (hex!("0122222222333333334444444455000003180000000000006560"), 0x042941), + (hex!("01222222223333333344444444550000031900000000000031a0"), 0x0429e1), + (hex!("01222222223333333344444444550000031900000000000052d0"), 0x042a81), + (hex!("01222222223333333344444444550000031900000000000057e0"), 0x042b21), + (hex!("01222222223333333344444444550000031a00000000000031b0"), 0x042bc1), + (hex!("01222222223333333344444444550000031a00000000000071e0"), 0x042c61), + (hex!("01222222223333333344444444550000031b00000000000031c0"), 0x042d01), + (hex!("01222222223333333344444444550000031c00000000000031d0"), 0x042da1), + (hex!("01222222223333333344444444550000031c0000000000004480"), 0x042e41), + (hex!("01222222223333333344444444550000031c0000000000005790"), 0x042ee1), + (hex!("01222222223333333344444444550000031c0000000000007be0"), 0x042f81), + (hex!("01222222223333333344444444550000031d00000000000031e0"), 0x043021), + (hex!("01222222223333333344444444550000031d0000000000005560"), 0x0430c1), + (hex!("01222222223333333344444444550000031e00000000000031f0"), 0x043161), + (hex!("01222222223333333344444444550000031f0000000000003200"), 0x043201), + (hex!("01222222223333333344444444550000031f0000000000004190"), 0x0432a1), + (hex!("0122222222333333334444444455000003200000000000003210"), 0x043341), + (hex!("0122222222333333334444444455000003210000000000003220"), 0x0433e1), + (hex!("0122222222333333334444444455000003220000000000003230"), 0x043481), + (hex!("0122222222333333334444444455000003230000000000003240"), 0x043521), + (hex!("01222222223333333344444444550000032300000000000069d0"), 0x0435c1), + (hex!("0122222222333333334444444455000003240000000000003250"), 0x043661), + (hex!("0122222222333333334444444455000003250000000000003260"), 0x043701), + (hex!("01222222223333333344444444550000032500000000000042b0"), 0x0437a1), + (hex!("01222222223333333344444444550000032500000000000064e0"), 0x043841), + (hex!("0122222222333333334444444455000003260000000000003270"), 0x0438e1), + (hex!("0122222222333333334444444455000003270000000000003280"), 0x043981), + (hex!("0122222222333333334444444455000003270000000000005b20"), 0x043a21), + (hex!("0122222222333333334444444455000003270000000000006330"), 0x043ac1), + (hex!("0122222222333333334444444455000003270000000000006810"), 0x043b61), + (hex!("0122222222333333334444444455000003280000000000003290"), 0x043c01), + (hex!("01222222223333333344444444550000032900000000000032a0"), 0x043ca1), + (hex!("01222222223333333344444444550000032900000000000056f0"), 0x043d41), + (hex!("0122222222333333334444444455000003290000000000005e20"), 0x043de1), + (hex!("0122222222333333334444444455000003290000000000005e70"), 0x043e81), + (hex!("01222222223333333344444444550000032a00000000000032b0"), 0x043f21), + (hex!("01222222223333333344444444550000032b00000000000032c0"), 0x043fc1), + (hex!("01222222223333333344444444550000032b0000000000005500"), 0x044061), + (hex!("01222222223333333344444444550000032b0000000000005a20"), 0x044101), + (hex!("01222222223333333344444444550000032c00000000000032d0"), 0x0441a1), + (hex!("01222222223333333344444444550000032c0000000000004060"), 0x044241), + (hex!("01222222223333333344444444550000032c0000000000004760"), 0x0442e1), + (hex!("01222222223333333344444444550000032d00000000000032e0"), 0x044381), + (hex!("01222222223333333344444444550000032d00000000000068a0"), 0x044421), + (hex!("01222222223333333344444444550000032e00000000000032f0"), 0x0444c1), + (hex!("01222222223333333344444444550000032f0000000000003300"), 0x044561), + (hex!("0122222222333333334444444455000003300000000000003310"), 0x044601), + (hex!("0122222222333333334444444455000003300000000000006e40"), 0x0446a1), + (hex!("0122222222333333334444444455000003310000000000003320"), 0x044741), + (hex!("0122222222333333334444444455000003310000000000004620"), 0x0447e1), + (hex!("0122222222333333334444444455000003320000000000003330"), 0x044881), + (hex!("0122222222333333334444444455000003330000000000003340"), 0x044921), + (hex!("0122222222333333334444444455000003330000000000004b80"), 0x0449c1), + (hex!("0122222222333333334444444455000003340000000000003350"), 0x044a61), + (hex!("0122222222333333334444444455000003350000000000003360"), 0x044b01), + (hex!("0122222222333333334444444455000003360000000000003370"), 0x044ba1), + (hex!("0122222222333333334444444455000003370000000000003380"), 0x044c41), + (hex!("0122222222333333334444444455000003380000000000003390"), 0x044ce1), + (hex!("01222222223333333344444444550000033900000000000033a0"), 0x044d81), + (hex!("0122222222333333334444444455000003390000000000006b90"), 0x044e21), + (hex!("01222222223333333344444444550000033a00000000000033b0"), 0x044ec1), + (hex!("01222222223333333344444444550000033a0000000000007420"), 0x044f61), + (hex!("01222222223333333344444444550000033b00000000000033c0"), 0x045001), + (hex!("01222222223333333344444444550000033b0000000000007620"), 0x0450a1), + (hex!("01222222223333333344444444550000033c00000000000033d0"), 0x045141), + (hex!("01222222223333333344444444550000033c0000000000006b30"), 0x0451e1), + (hex!("01222222223333333344444444550000033d00000000000033e0"), 0x045281), + (hex!("01222222223333333344444444550000033e00000000000033f0"), 0x045321), + (hex!("01222222223333333344444444550000033e00000000000048b0"), 0x0453c1), + (hex!("01222222223333333344444444550000033e0000000000004e70"), 0x045461), + (hex!("01222222223333333344444444550000033f0000000000003400"), 0x045501), + (hex!("01222222223333333344444444550000033f0000000000006380"), 0x0455a1), + (hex!("0122222222333333334444444455000003400000000000003410"), 0x045641), + (hex!("0122222222333333334444444455000003410000000000003420"), 0x0456e1), + (hex!("0122222222333333334444444455000003410000000000006090"), 0x045781), + (hex!("0122222222333333334444444455000003420000000000003430"), 0x045821), + (hex!("01222222223333333344444444550000034200000000000073d0"), 0x0458c1), + (hex!("0122222222333333334444444455000003430000000000003440"), 0x045961), + (hex!("0122222222333333334444444455000003430000000000006370"), 0x045a01), + (hex!("01222222223333333344444444550000034300000000000075c0"), 0x045aa1), + (hex!("0122222222333333334444444455000003440000000000003450"), 0x045b41), + (hex!("0122222222333333334444444455000003450000000000003460"), 0x045be1), + (hex!("0122222222333333334444444455000003460000000000003470"), 0x045c81), + (hex!("01222222223333333344444444550000034600000000000055f0"), 0x045d21), + (hex!("0122222222333333334444444455000003470000000000003480"), 0x045dc1), + (hex!("0122222222333333334444444455000003470000000000003fe0"), 0x045e61), + (hex!("0122222222333333334444444455000003480000000000003490"), 0x045f01), + (hex!("0122222222333333334444444455000003480000000000007990"), 0x045fa1), + (hex!("01222222223333333344444444550000034900000000000034a0"), 0x046041), + (hex!("0122222222333333334444444455000003490000000000004410"), 0x0460e1), + (hex!("01222222223333333344444444550000034a00000000000034b0"), 0x046181), + (hex!("01222222223333333344444444550000034a00000000000062a0"), 0x046221), + (hex!("01222222223333333344444444550000034a0000000000007260"), 0x0462c1), + (hex!("01222222223333333344444444550000034b00000000000034c0"), 0x046361), + (hex!("01222222223333333344444444550000034b0000000000005760"), 0x046401), + (hex!("01222222223333333344444444550000034b0000000000006200"), 0x0464a1), + (hex!("01222222223333333344444444550000034c00000000000034d0"), 0x046541), + (hex!("01222222223333333344444444550000034d00000000000034e0"), 0x0465e1), + (hex!("01222222223333333344444444550000034e00000000000034f0"), 0x046681), + (hex!("01222222223333333344444444550000034e0000000000007790"), 0x046721), + (hex!("01222222223333333344444444550000034f0000000000003500"), 0x0467c1), + (hex!("0122222222333333334444444455000003500000000000003510"), 0x046861), + (hex!("0122222222333333334444444455000003510000000000003520"), 0x046901), + (hex!("0122222222333333334444444455000003520000000000003530"), 0x0469a1), + (hex!("01222222223333333344444444550000035200000000000056a0"), 0x046a41), + (hex!("0122222222333333334444444455000003530000000000003540"), 0x046ae1), + (hex!("0122222222333333334444444455000003540000000000003550"), 0x046b81), + (hex!("01222222223333333344444444550000035400000000000047b0"), 0x046c21), + (hex!("0122222222333333334444444455000003550000000000003560"), 0x046cc1), + (hex!("0122222222333333334444444455000003550000000000004500"), 0x046d61), + (hex!("0122222222333333334444444455000003560000000000003570"), 0x046e01), + (hex!("0122222222333333334444444455000003560000000000004fc0"), 0x046ea1), + (hex!("0122222222333333334444444455000003560000000000007160"), 0x046f41), + (hex!("0122222222333333334444444455000003560000000000007400"), 0x046fe1), + (hex!("0122222222333333334444444455000003570000000000003580"), 0x047081), + (hex!("0122222222333333334444444455000003580000000000003590"), 0x047121), + (hex!("0122222222333333334444444455000003580000000000005a80"), 0x0471c1), + (hex!("01222222223333333344444444550000035900000000000035a0"), 0x047261), + (hex!("01222222223333333344444444550000035900000000000073b0"), 0x047301), + (hex!("01222222223333333344444444550000035a00000000000035b0"), 0x0473a1), + (hex!("01222222223333333344444444550000035a0000000000004c20"), 0x047441), + (hex!("01222222223333333344444444550000035b00000000000035c0"), 0x0474e1), + (hex!("01222222223333333344444444550000035b0000000000005120"), 0x047581), + (hex!("01222222223333333344444444550000035c00000000000035d0"), 0x047621), + (hex!("01222222223333333344444444550000035c0000000000004300"), 0x0476c1), + (hex!("01222222223333333344444444550000035c0000000000005a40"), 0x047761), + (hex!("01222222223333333344444444550000035c0000000000006620"), 0x047801), + (hex!("01222222223333333344444444550000035c0000000000006ed0"), 0x0478a1), + (hex!("01222222223333333344444444550000035d00000000000035e0"), 0x047941), + (hex!("01222222223333333344444444550000035d0000000000005df0"), 0x0479e1), + (hex!("01222222223333333344444444550000035e00000000000035f0"), 0x047a81), + (hex!("01222222223333333344444444550000035f0000000000003600"), 0x047b21), + (hex!("01222222223333333344444444550000035f00000000000058d0"), 0x047bc1), + (hex!("0122222222333333334444444455000003600000000000003610"), 0x047c61), + (hex!("0122222222333333334444444455000003600000000000007b90"), 0x047d01), + (hex!("0122222222333333334444444455000003610000000000003620"), 0x047da1), + (hex!("0122222222333333334444444455000003610000000000006ad0"), 0x047e41), + (hex!("0122222222333333334444444455000003620000000000003630"), 0x047ee1), + (hex!("01222222223333333344444444550000036200000000000063a0"), 0x047f81), + (hex!("0122222222333333334444444455000003630000000000003640"), 0x048021), + (hex!("0122222222333333334444444455000003630000000000007250"), 0x0480c1), + (hex!("0122222222333333334444444455000003640000000000003650"), 0x048161), + (hex!("0122222222333333334444444455000003640000000000005510"), 0x048201), + (hex!("0122222222333333334444444455000003640000000000007850"), 0x0482a1), + (hex!("0122222222333333334444444455000003650000000000003660"), 0x048341), + (hex!("0122222222333333334444444455000003660000000000003670"), 0x0483e1), + (hex!("0122222222333333334444444455000003660000000000004650"), 0x048481), + (hex!("01222222223333333344444444550000036600000000000050d0"), 0x048521), + (hex!("0122222222333333334444444455000003660000000000006eb0"), 0x0485c1), + (hex!("0122222222333333334444444455000003670000000000003680"), 0x048661), + (hex!("01222222223333333344444444550000036700000000000071f0"), 0x048701), + (hex!("0122222222333333334444444455000003680000000000003690"), 0x0487a1), + (hex!("01222222223333333344444444550000036900000000000036a0"), 0x048841), + (hex!("0122222222333333334444444455000003690000000000005c70"), 0x0488e1), + (hex!("01222222223333333344444444550000036a00000000000036b0"), 0x048981), + (hex!("01222222223333333344444444550000036a00000000000071b0"), 0x048a21), + (hex!("01222222223333333344444444550000036b00000000000036c0"), 0x048ac1), + (hex!("01222222223333333344444444550000036b0000000000004670"), 0x048b61), + (hex!("01222222223333333344444444550000036c00000000000036d0"), 0x048c01), + (hex!("01222222223333333344444444550000036c0000000000004750"), 0x048ca1), + (hex!("01222222223333333344444444550000036c0000000000006fa0"), 0x048d41), + (hex!("01222222223333333344444444550000036d00000000000036e0"), 0x048de1), + (hex!("01222222223333333344444444550000036d0000000000003f70"), 0x048e81), + (hex!("01222222223333333344444444550000036d0000000000004b90"), 0x048f21), + (hex!("01222222223333333344444444550000036d00000000000057a0"), 0x048fc1), + (hex!("01222222223333333344444444550000036e00000000000036f0"), 0x049061), + (hex!("01222222223333333344444444550000036e00000000000075d0"), 0x049101), + (hex!("01222222223333333344444444550000036f0000000000003700"), 0x0491a1), + (hex!("0122222222333333334444444455000003700000000000003710"), 0x049241), + (hex!("0122222222333333334444444455000003700000000000005aa0"), 0x0492e1), + (hex!("0122222222333333334444444455000003710000000000003720"), 0x049381), + (hex!("0122222222333333334444444455000003710000000000005130"), 0x049421), + (hex!("0122222222333333334444444455000003710000000000006fc0"), 0x0494c1), + (hex!("0122222222333333334444444455000003710000000000007b00"), 0x049561), + (hex!("0122222222333333334444444455000003720000000000003730"), 0x049601), + (hex!("01222222223333333344444444550000037200000000000054d0"), 0x0496a1), + (hex!("0122222222333333334444444455000003730000000000003740"), 0x049741), + (hex!("0122222222333333334444444455000003730000000000004220"), 0x0497e1), + (hex!("0122222222333333334444444455000003740000000000003750"), 0x049881), + (hex!("0122222222333333334444444455000003740000000000004720"), 0x049921), + (hex!("0122222222333333334444444455000003750000000000003760"), 0x0499c1), + (hex!("0122222222333333334444444455000003750000000000004110"), 0x049a61), + (hex!("0122222222333333334444444455000003760000000000003770"), 0x049b01), + (hex!("0122222222333333334444444455000003770000000000003780"), 0x049ba1), + (hex!("0122222222333333334444444455000003780000000000003790"), 0x049c41), + (hex!("0122222222333333334444444455000003780000000000004b40"), 0x049ce1), + (hex!("0122222222333333334444444455000003780000000000005660"), 0x049d81), + (hex!("0122222222333333334444444455000003780000000000005ea0"), 0x049e21), + (hex!("01222222223333333344444444550000037900000000000037a0"), 0x049ec1), + (hex!("01222222223333333344444444550000037a00000000000037b0"), 0x049f61), + (hex!("01222222223333333344444444550000037b00000000000037c0"), 0x04a001), + (hex!("01222222223333333344444444550000037c00000000000037d0"), 0x04a0a1), + (hex!("01222222223333333344444444550000037c0000000000004340"), 0x04a141), + (hex!("01222222223333333344444444550000037c0000000000005230"), 0x04a1e1), + (hex!("01222222223333333344444444550000037d00000000000037e0"), 0x04a281), + (hex!("01222222223333333344444444550000037d00000000000051e0"), 0x04a321), + (hex!("01222222223333333344444444550000037e00000000000037f0"), 0x04a3c1), + (hex!("01222222223333333344444444550000037e0000000000004090"), 0x04a461), + (hex!("01222222223333333344444444550000037e0000000000005c20"), 0x04a501), + (hex!("01222222223333333344444444550000037f0000000000003800"), 0x04a5a1), + (hex!("0122222222333333334444444455000003800000000000003810"), 0x04a641), + (hex!("0122222222333333334444444455000003800000000000007630"), 0x04a6e1), + (hex!("0122222222333333334444444455000003810000000000003820"), 0x04a781), + (hex!("0122222222333333334444444455000003820000000000003830"), 0x04a821), + (hex!("0122222222333333334444444455000003820000000000004170"), 0x04a8c1), + (hex!("0122222222333333334444444455000003830000000000003840"), 0x04a961), + (hex!("0122222222333333334444444455000003840000000000003850"), 0x04aa01), + (hex!("0122222222333333334444444455000003850000000000003860"), 0x04aaa1), + (hex!("0122222222333333334444444455000003850000000000004180"), 0x04ab41), + (hex!("0122222222333333334444444455000003850000000000005c90"), 0x04abe1), + (hex!("0122222222333333334444444455000003850000000000005da0"), 0x04ac81), + (hex!("0122222222333333334444444455000003850000000000006ff0"), 0x04ad21), + (hex!("0122222222333333334444444455000003860000000000003870"), 0x04adc1), + (hex!("01222222223333333344444444550000038600000000000065c0"), 0x04ae61), + (hex!("0122222222333333334444444455000003870000000000003880"), 0x04af01), + (hex!("0122222222333333334444444455000003870000000000007cc0"), 0x04afa1), + (hex!("0122222222333333334444444455000003880000000000003890"), 0x04b041), + (hex!("01222222223333333344444444550000038900000000000038a0"), 0x04b0e1), + (hex!("01222222223333333344444444550000038a00000000000038b0"), 0x04b181), + (hex!("01222222223333333344444444550000038a00000000000073e0"), 0x04b221), + (hex!("01222222223333333344444444550000038b00000000000038c0"), 0x04b2c1), + (hex!("01222222223333333344444444550000038c00000000000038d0"), 0x04b361), + (hex!("01222222223333333344444444550000038d00000000000038e0"), 0x04b401), + (hex!("01222222223333333344444444550000038d00000000000069f0"), 0x04b4a1), + (hex!("01222222223333333344444444550000038d0000000000007680"), 0x04b541), + (hex!("01222222223333333344444444550000038e00000000000038f0"), 0x04b5e1), + (hex!("01222222223333333344444444550000038f0000000000003900"), 0x04b681), + (hex!("01222222223333333344444444550000038f00000000000045b0"), 0x04b721), + (hex!("01222222223333333344444444550000038f0000000000007180"), 0x04b7c1), + (hex!("0122222222333333334444444455000003900000000000003910"), 0x04b861), + (hex!("0122222222333333334444444455000003910000000000003920"), 0x04b901), + (hex!("0122222222333333334444444455000003910000000000004a20"), 0x04b9a1), + (hex!("0122222222333333334444444455000003920000000000003930"), 0x04ba41), + (hex!("01222222223333333344444444550000039200000000000059b0"), 0x04bae1), + (hex!("0122222222333333334444444455000003930000000000003940"), 0x04bb81), + (hex!("0122222222333333334444444455000003930000000000006cc0"), 0x04bc21), + (hex!("0122222222333333334444444455000003940000000000003950"), 0x04bcc1), + (hex!("01222222223333333344444444550000039400000000000056c0"), 0x04bd61), + (hex!("0122222222333333334444444455000003950000000000003960"), 0x04be01), + (hex!("0122222222333333334444444455000003950000000000004cc0"), 0x04bea1), + (hex!("0122222222333333334444444455000003950000000000007720"), 0x04bf41), + (hex!("0122222222333333334444444455000003960000000000003970"), 0x04bfe1), + (hex!("0122222222333333334444444455000003960000000000004da0"), 0x04c081), + (hex!("0122222222333333334444444455000003960000000000004df0"), 0x04c121), + (hex!("0122222222333333334444444455000003960000000000004f30"), 0x04c1c1), + (hex!("01222222223333333344444444550000039600000000000050f0"), 0x04c261), + (hex!("0122222222333333334444444455000003960000000000007940"), 0x04c301), + (hex!("0122222222333333334444444455000003970000000000003980"), 0x04c3a1), + (hex!("0122222222333333334444444455000003970000000000005850"), 0x04c441), + (hex!("0122222222333333334444444455000003970000000000007bd0"), 0x04c4e1), + (hex!("0122222222333333334444444455000003980000000000003990"), 0x04c581), + (hex!("0122222222333333334444444455000003980000000000004c00"), 0x04c621), + (hex!("0122222222333333334444444455000003980000000000005580"), 0x04c6c1), + (hex!("01222222223333333344444444550000039900000000000039a0"), 0x04c761), + (hex!("0122222222333333334444444455000003990000000000005820"), 0x04c801), + (hex!("01222222223333333344444444550000039a00000000000039b0"), 0x04c8a1), + (hex!("01222222223333333344444444550000039b00000000000039c0"), 0x04c941), + (hex!("01222222223333333344444444550000039b0000000000004c10"), 0x04c9e1), + (hex!("01222222223333333344444444550000039b0000000000006460"), 0x04ca81), + (hex!("01222222223333333344444444550000039c00000000000039d0"), 0x04cb21), + (hex!("01222222223333333344444444550000039d00000000000039e0"), 0x04cbc1), + (hex!("01222222223333333344444444550000039d00000000000044c0"), 0x04cc61), + (hex!("01222222223333333344444444550000039d00000000000049e0"), 0x04cd01), + (hex!("01222222223333333344444444550000039e00000000000039f0"), 0x04cda1), + (hex!("01222222223333333344444444550000039f0000000000003a00"), 0x04ce41), + (hex!("0122222222333333334444444455000003a00000000000003a10"), 0x04cee1), + (hex!("0122222222333333334444444455000003a10000000000003a20"), 0x04cf81), + (hex!("0122222222333333334444444455000003a10000000000006a80"), 0x04d021), + (hex!("0122222222333333334444444455000003a20000000000003a30"), 0x04d0c1), + (hex!("0122222222333333334444444455000003a200000000000062b0"), 0x04d161), + (hex!("0122222222333333334444444455000003a30000000000003a40"), 0x04d201), + (hex!("0122222222333333334444444455000003a30000000000006ce0"), 0x04d2a1), + (hex!("0122222222333333334444444455000003a40000000000003a50"), 0x04d341), + (hex!("0122222222333333334444444455000003a50000000000003a60"), 0x04d3e1), + (hex!("0122222222333333334444444455000003a60000000000003a70"), 0x04d481), + (hex!("0122222222333333334444444455000003a60000000000007750"), 0x04d521), + (hex!("0122222222333333334444444455000003a70000000000003a80"), 0x04d5c1), + (hex!("0122222222333333334444444455000003a70000000000005b10"), 0x04d661), + (hex!("0122222222333333334444444455000003a80000000000003a90"), 0x04d701), + (hex!("0122222222333333334444444455000003a80000000000006c20"), 0x04d7a1), + (hex!("0122222222333333334444444455000003a90000000000003aa0"), 0x04d841), + (hex!("0122222222333333334444444455000003a90000000000005b70"), 0x04d8e1), + (hex!("0122222222333333334444444455000003a900000000000070e0"), 0x04d981), + (hex!("0122222222333333334444444455000003aa0000000000003ab0"), 0x04da21), + (hex!("0122222222333333334444444455000003aa00000000000049f0"), 0x04dac1), + (hex!("0122222222333333334444444455000003aa0000000000004d60"), 0x04db61), + (hex!("0122222222333333334444444455000003ab0000000000003ac0"), 0x04dc01), + (hex!("0122222222333333334444444455000003ac0000000000003ad0"), 0x04dca1), + (hex!("0122222222333333334444444455000003ac0000000000004580"), 0x04dd41), + (hex!("0122222222333333334444444455000003ad0000000000003ae0"), 0x04dde1), + (hex!("0122222222333333334444444455000003ae0000000000003af0"), 0x04de81), + (hex!("0122222222333333334444444455000003af0000000000003b00"), 0x04df21), + (hex!("0122222222333333334444444455000003b00000000000003b10"), 0x04dfc1), + (hex!("0122222222333333334444444455000003b10000000000003b20"), 0x04e061), + (hex!("0122222222333333334444444455000003b10000000000003fd0"), 0x04e101), + (hex!("0122222222333333334444444455000003b20000000000003b30"), 0x04e1a1), + (hex!("0122222222333333334444444455000003b30000000000003b40"), 0x04e241), + (hex!("0122222222333333334444444455000003b40000000000003b50"), 0x04e2e1), + (hex!("0122222222333333334444444455000003b40000000000007450"), 0x04e381), + (hex!("0122222222333333334444444455000003b50000000000003b60"), 0x04e421), + (hex!("0122222222333333334444444455000003b60000000000003b70"), 0x04e4c1), + (hex!("0122222222333333334444444455000003b70000000000003b80"), 0x04e561), + (hex!("0122222222333333334444444455000003b70000000000006d50"), 0x04e601), + (hex!("0122222222333333334444444455000003b80000000000003b90"), 0x04e6a1), + (hex!("0122222222333333334444444455000003b800000000000057c0"), 0x04e741), + (hex!("0122222222333333334444444455000003b800000000000078a0"), 0x04e7e1), + (hex!("0122222222333333334444444455000003b90000000000003ba0"), 0x04e881), + (hex!("0122222222333333334444444455000003b90000000000006750"), 0x04e921), + (hex!("0122222222333333334444444455000003ba0000000000003bb0"), 0x04e9c1), + (hex!("0122222222333333334444444455000003ba0000000000007a10"), 0x04ea61), + (hex!("0122222222333333334444444455000003ba0000000000007a20"), 0x04eb01), + (hex!("0122222222333333334444444455000003bb0000000000003bc0"), 0x04eba1), + (hex!("0122222222333333334444444455000003bb0000000000005bc0"), 0x04ec41), + (hex!("0122222222333333334444444455000003bc0000000000003bd0"), 0x04ece1), + (hex!("0122222222333333334444444455000003bc0000000000005e80"), 0x04ed81), + (hex!("0122222222333333334444444455000003bc0000000000007ab0"), 0x04ee21), + (hex!("0122222222333333334444444455000003bd0000000000003be0"), 0x04eec1), + (hex!("0122222222333333334444444455000003bd00000000000049b0"), 0x04ef61), + (hex!("0122222222333333334444444455000003be0000000000003bf0"), 0x04f001), + (hex!("0122222222333333334444444455000003be0000000000005780"), 0x04f0a1), + (hex!("0122222222333333334444444455000003be0000000000007930"), 0x04f141), + (hex!("0122222222333333334444444455000003bf0000000000003c00"), 0x04f1e1), + (hex!("0122222222333333334444444455000003bf0000000000005de0"), 0x04f281), + (hex!("0122222222333333334444444455000003bf00000000000060b0"), 0x04f321), + (hex!("0122222222333333334444444455000003bf00000000000060c0"), 0x04f3c1), + (hex!("0122222222333333334444444455000003bf0000000000006a50"), 0x04f461), + (hex!("0122222222333333334444444455000003c00000000000003c10"), 0x04f501), + (hex!("0122222222333333334444444455000003c00000000000004030"), 0x04f5a1), + (hex!("0122222222333333334444444455000003c10000000000003c20"), 0x04f641), + (hex!("0122222222333333334444444455000003c20000000000003c30"), 0x04f6e1), + (hex!("0122222222333333334444444455000003c200000000000040b0"), 0x04f781), + (hex!("0122222222333333334444444455000003c30000000000003c40"), 0x04f821), + (hex!("0122222222333333334444444455000003c40000000000003c50"), 0x04f8c1), + (hex!("0122222222333333334444444455000003c40000000000005ba0"), 0x04f961), + (hex!("0122222222333333334444444455000003c50000000000003c60"), 0x04fa01), + (hex!("0122222222333333334444444455000003c60000000000003c70"), 0x04faa1), + (hex!("0122222222333333334444444455000003c70000000000003c80"), 0x04fb41), + (hex!("0122222222333333334444444455000003c70000000000004270"), 0x04fbe1), + (hex!("0122222222333333334444444455000003c80000000000003c90"), 0x04fc81), + (hex!("0122222222333333334444444455000003c80000000000006e70"), 0x04fd21), + (hex!("0122222222333333334444444455000003c90000000000003ca0"), 0x04fdc1), + (hex!("0122222222333333334444444455000003ca0000000000003cb0"), 0x04fe61), + (hex!("0122222222333333334444444455000003ca0000000000006e20"), 0x04ff01), + (hex!("0122222222333333334444444455000003ca0000000000007c20"), 0x04ffa1), + (hex!("0122222222333333334444444455000003cb0000000000003cc0"), 0x050041), + (hex!("0122222222333333334444444455000003cc0000000000003cd0"), 0x0500e1), + (hex!("0122222222333333334444444455000003cc0000000000006120"), 0x050181), + (hex!("0122222222333333334444444455000003cc0000000000007950"), 0x050221), + (hex!("0122222222333333334444444455000003cd0000000000003ce0"), 0x0502c1), + (hex!("0122222222333333334444444455000003ce0000000000003cf0"), 0x050361), + (hex!("0122222222333333334444444455000003cf0000000000003d00"), 0x050401), + (hex!("0122222222333333334444444455000003d00000000000003d10"), 0x0504a1), + (hex!("0122222222333333334444444455000003d10000000000003d20"), 0x050541), + (hex!("0122222222333333334444444455000003d10000000000005e50"), 0x0505e1), + (hex!("0122222222333333334444444455000003d10000000000007880"), 0x050681), + (hex!("0122222222333333334444444455000003d20000000000003d30"), 0x050721), + (hex!("0122222222333333334444444455000003d20000000000005d00"), 0x0507c1), + (hex!("0122222222333333334444444455000003d30000000000003d40"), 0x050861), + (hex!("0122222222333333334444444455000003d30000000000005d40"), 0x050901), + (hex!("0122222222333333334444444455000003d300000000000063f0"), 0x0509a1), + (hex!("0122222222333333334444444455000003d40000000000003d50"), 0x050a41), + (hex!("0122222222333333334444444455000003d40000000000005700"), 0x050ae1), + (hex!("0122222222333333334444444455000003d400000000000078f0"), 0x050b81), + (hex!("0122222222333333334444444455000003d50000000000003d60"), 0x050c21), + (hex!("0122222222333333334444444455000003d60000000000003d70"), 0x050cc1), + (hex!("0122222222333333334444444455000003d70000000000003d80"), 0x050d61), + (hex!("0122222222333333334444444455000003d80000000000003d90"), 0x050e01), + (hex!("0122222222333333334444444455000003d80000000000006690"), 0x050ea1), + (hex!("0122222222333333334444444455000003d90000000000003da0"), 0x050f41), + (hex!("0122222222333333334444444455000003d900000000000076d0"), 0x050fe1), + (hex!("0122222222333333334444444455000003da0000000000003db0"), 0x051081), + (hex!("0122222222333333334444444455000003db0000000000003dc0"), 0x051121), + (hex!("0122222222333333334444444455000003db0000000000004a30"), 0x0511c1), + (hex!("0122222222333333334444444455000003db0000000000005390"), 0x051261), + (hex!("0122222222333333334444444455000003dc0000000000003dd0"), 0x051301), + (hex!("0122222222333333334444444455000003dc0000000000006d60"), 0x0513a1), + (hex!("0122222222333333334444444455000003dd0000000000003de0"), 0x051441), + (hex!("0122222222333333334444444455000003de0000000000003df0"), 0x0514e1), + (hex!("0122222222333333334444444455000003df0000000000003e00"), 0x051581), + (hex!("0122222222333333334444444455000003df0000000000005240"), 0x051621), + (hex!("0122222222333333334444444455000003df0000000000005610"), 0x0516c1), + (hex!("0122222222333333334444444455000003e00000000000003e10"), 0x051761), + (hex!("0122222222333333334444444455000003e00000000000006500"), 0x051801), + (hex!("0122222222333333334444444455000003e10000000000003e20"), 0x0518a1), + (hex!("0122222222333333334444444455000003e10000000000006a10"), 0x051941), + (hex!("0122222222333333334444444455000003e10000000000007c10"), 0x0519e1), + (hex!("0122222222333333334444444455000003e20000000000003e30"), 0x051a81), + (hex!("0122222222333333334444444455000003e20000000000006310"), 0x051b21), + (hex!("0122222222333333334444444455000003e30000000000003e40"), 0x051bc1), + (hex!("0122222222333333334444444455000003e40000000000003e50"), 0x051c61), + (hex!("0122222222333333334444444455000003e40000000000006780"), 0x051d01), + (hex!("0122222222333333334444444455000003e40000000000007ce0"), 0x051da1), + (hex!("0122222222333333334444444455000003e50000000000003e60"), 0x051e41), + (hex!("0122222222333333334444444455000003e60000000000003e70"), 0x051ee1), + (hex!("0122222222333333334444444455000003e60000000000005040"), 0x051f81), + (hex!("0122222222333333334444444455000003e60000000000005bf0"), 0x052021), + (hex!("0122222222333333334444444455000003e70000000000003e80"), 0x0520c1), + (hex!("0122222222333333334444444455000003e70000000000003f50"), 0x052161), +]; diff --git a/pageserver/src/layered_repository/ephemeral_file.rs b/pageserver/src/layered_repository/ephemeral_file.rs index 79a72f4563..d509186e6f 100644 --- a/pageserver/src/layered_repository/ephemeral_file.rs +++ b/pageserver/src/layered_repository/ephemeral_file.rs @@ -2,6 +2,8 @@ //! used to keep in-memory layers spilled on disk. use crate::config::PageServerConf; +use crate::layered_repository::blob_io::BlobWriter; +use crate::layered_repository::block_io::BlockReader; use crate::page_cache; use crate::page_cache::PAGE_SZ; use crate::page_cache::{ReadBufResult, WriteBufResult}; @@ -10,7 +12,7 @@ use lazy_static::lazy_static; use std::cmp::min; use std::collections::HashMap; use std::fs::OpenOptions; -use std::io::{Error, ErrorKind, Seek, SeekFrom, Write}; +use std::io::{Error, ErrorKind}; use std::ops::DerefMut; use std::path::PathBuf; use std::sync::{Arc, RwLock}; @@ -41,7 +43,7 @@ pub struct EphemeralFile { _timelineid: ZTimelineId, file: Arc, - pos: u64, + size: u64, } impl EphemeralFile { @@ -70,11 +72,11 @@ impl EphemeralFile { _tenantid: tenantid, _timelineid: timelineid, file: file_rc, - pos: 0, + size: 0, }) } - pub fn fill_buffer(&self, buf: &mut [u8], blkno: u32) -> Result<(), Error> { + fn fill_buffer(&self, buf: &mut [u8], blkno: u32) -> Result<(), Error> { let mut off = 0; while off < PAGE_SZ { let n = self @@ -93,6 +95,26 @@ impl EphemeralFile { } Ok(()) } + + fn get_buf_for_write(&self, blkno: u32) -> Result { + // Look up the right page + let cache = page_cache::get(); + let mut write_guard = match cache.write_ephemeral_buf(self.file_id, blkno) { + WriteBufResult::Found(guard) => guard, + WriteBufResult::NotFound(mut guard) => { + // Read the page from disk into the buffer + // TODO: if we're overwriting the whole page, no need to read it in first + self.fill_buffer(guard.deref_mut(), blkno)?; + guard.mark_valid(); + + // And then fall through to modify it. + guard + } + }; + write_guard.mark_dirty(); + + Ok(write_guard) + } } /// Does the given filename look like an ephemeral file? @@ -167,48 +189,49 @@ impl FileExt for EphemeralFile { } } -impl Write for EphemeralFile { - fn write(&mut self, buf: &[u8]) -> Result { - let n = self.write_at(buf, self.pos)?; - self.pos += n as u64; - Ok(n) - } +impl BlobWriter for EphemeralFile { + fn write_blob(&mut self, srcbuf: &[u8]) -> Result { + let pos = self.size; - fn flush(&mut self) -> Result<(), std::io::Error> { - // we don't need to flush data: - // * we either write input bytes or not, not keeping any intermediate data buffered - // * rust unix file `flush` impl does not flush things either, returning `Ok(())` - Ok(()) - } -} + let mut blknum = (self.size / PAGE_SZ as u64) as u32; + let mut off = (pos % PAGE_SZ as u64) as usize; -impl Seek for EphemeralFile { - fn seek(&mut self, pos: SeekFrom) -> Result { - match pos { - SeekFrom::Start(offset) => { - self.pos = offset; - } - SeekFrom::End(_offset) => { - return Err(Error::new( - ErrorKind::Other, - "SeekFrom::End not supported by EphemeralFile", - )); - } - SeekFrom::Current(offset) => { - let pos = self.pos as i128 + offset as i128; - if pos < 0 { - return Err(Error::new( - ErrorKind::InvalidInput, - "offset would be negative", - )); - } - if pos > u64::MAX as i128 { - return Err(Error::new(ErrorKind::InvalidInput, "offset overflow")); - } - self.pos = pos as u64; - } + let mut buf = self.get_buf_for_write(blknum)?; + + // Write the length field + let len_buf = u32::to_ne_bytes(srcbuf.len() as u32); + let thislen = PAGE_SZ - off; + if thislen < 4 { + // it needs to be split across pages + buf[off..(off + thislen)].copy_from_slice(&len_buf[..thislen]); + blknum += 1; + buf = self.get_buf_for_write(blknum)?; + buf[0..4 - thislen].copy_from_slice(&len_buf[thislen..]); + off = 4 - thislen; + } else { + buf[off..off + 4].copy_from_slice(&len_buf); + off += 4; } - Ok(self.pos) + + // Write the payload + let mut buf_remain = srcbuf; + while !buf_remain.is_empty() { + let mut page_remain = PAGE_SZ - off; + if page_remain == 0 { + blknum += 1; + buf = self.get_buf_for_write(blknum)?; + off = 0; + page_remain = PAGE_SZ; + } + let this_blk_len = min(page_remain, buf_remain.len()); + buf[off..(off + this_blk_len)].copy_from_slice(&buf_remain[..this_blk_len]); + off += this_blk_len; + buf_remain = &buf_remain[this_blk_len..]; + } + drop(buf); + self.size += 4 + srcbuf.len() as u64; + + Ok(pos) } } @@ -239,11 +262,34 @@ pub fn writeback(file_id: u64, blkno: u32, buf: &[u8]) -> Result<(), std::io::Er } } +impl BlockReader for EphemeralFile { + type BlockLease = page_cache::PageReadGuard<'static>; + + fn read_blk(&self, blknum: u32) -> Result { + // Look up the right page + let cache = page_cache::get(); + loop { + match cache.read_ephemeral_buf(self.file_id, blknum) { + ReadBufResult::Found(guard) => return Ok(guard), + ReadBufResult::NotFound(mut write_guard) => { + // Read the page from disk into the buffer + self.fill_buffer(write_guard.deref_mut(), blknum)?; + write_guard.mark_valid(); + + // Swap for read lock + continue; + } + }; + } + } +} + #[cfg(test)] mod tests { use super::*; - use rand::seq::SliceRandom; - use rand::thread_rng; + use crate::layered_repository::blob_io::{BlobCursor, BlobWriter}; + use crate::layered_repository::block_io::BlockCursor; + use rand::{seq::SliceRandom, thread_rng, RngCore}; use std::fs; use std::str::FromStr; @@ -281,19 +327,19 @@ mod tests { fn test_ephemeral_files() -> Result<(), Error> { let (conf, tenantid, timelineid) = repo_harness("ephemeral_files")?; - let mut file_a = EphemeralFile::create(conf, tenantid, timelineid)?; + let file_a = EphemeralFile::create(conf, tenantid, timelineid)?; - file_a.write_all(b"foo")?; + file_a.write_all_at(b"foo", 0)?; assert_eq!("foo", read_string(&file_a, 0, 20)?); - file_a.write_all(b"bar")?; + file_a.write_all_at(b"bar", 3)?; assert_eq!("foobar", read_string(&file_a, 0, 20)?); // Open a lot of files, enough to cause some page evictions. let mut efiles = Vec::new(); for fileno in 0..100 { - let mut efile = EphemeralFile::create(conf, tenantid, timelineid)?; - efile.write_all(format!("file {}", fileno).as_bytes())?; + let efile = EphemeralFile::create(conf, tenantid, timelineid)?; + efile.write_all_at(format!("file {}", fileno).as_bytes(), 0)?; assert_eq!(format!("file {}", fileno), read_string(&efile, 0, 10)?); efiles.push((fileno, efile)); } @@ -307,4 +353,41 @@ mod tests { Ok(()) } + + #[test] + fn test_ephemeral_blobs() -> Result<(), Error> { + let (conf, tenantid, timelineid) = repo_harness("ephemeral_blobs")?; + + let mut file = EphemeralFile::create(conf, tenantid, timelineid)?; + + let pos_foo = file.write_blob(b"foo")?; + assert_eq!(b"foo", file.block_cursor().read_blob(pos_foo)?.as_slice()); + let pos_bar = file.write_blob(b"bar")?; + assert_eq!(b"foo", file.block_cursor().read_blob(pos_foo)?.as_slice()); + assert_eq!(b"bar", file.block_cursor().read_blob(pos_bar)?.as_slice()); + + let mut blobs = Vec::new(); + for i in 0..10000 { + let data = Vec::from(format!("blob{}", i).as_bytes()); + let pos = file.write_blob(&data)?; + blobs.push((pos, data)); + } + + let mut cursor = BlockCursor::new(&file); + for (pos, expected) in blobs { + let actual = cursor.read_blob(pos)?; + assert_eq!(actual, expected); + } + drop(cursor); + + // Test a large blob that spans multiple pages + let mut large_data = Vec::new(); + large_data.resize(20000, 0); + thread_rng().fill_bytes(&mut large_data); + let pos_large = file.write_blob(&large_data)?; + let result = file.block_cursor().read_blob(pos_large)?; + assert_eq!(result, large_data); + + Ok(()) + } } diff --git a/pageserver/src/layered_repository/filename.rs b/pageserver/src/layered_repository/filename.rs index cd63f014c4..497912b408 100644 --- a/pageserver/src/layered_repository/filename.rs +++ b/pageserver/src/layered_repository/filename.rs @@ -25,9 +25,7 @@ impl PartialOrd for DeltaFileName { impl Ord for DeltaFileName { fn cmp(&self, other: &Self) -> Ordering { - let mut cmp; - - cmp = self.key_range.start.cmp(&other.key_range.start); + let mut cmp = self.key_range.start.cmp(&other.key_range.start); if cmp != Ordering::Equal { return cmp; } @@ -117,9 +115,7 @@ impl PartialOrd for ImageFileName { impl Ord for ImageFileName { fn cmp(&self, other: &Self) -> Ordering { - let mut cmp; - - cmp = self.key_range.start.cmp(&other.key_range.start); + let mut cmp = self.key_range.start.cmp(&other.key_range.start); if cmp != Ordering::Equal { return cmp; } diff --git a/pageserver/src/layered_repository/image_layer.rs b/pageserver/src/layered_repository/image_layer.rs index 948e5b1433..08e635f073 100644 --- a/pageserver/src/layered_repository/image_layer.rs +++ b/pageserver/src/layered_repository/image_layer.rs @@ -13,65 +13,76 @@ //! //! 000000067F000032BE0000400000000070B6-000000067F000032BE0000400000000080B6__00000000346BC568 //! -//! An image file is constructed using the 'bookfile' crate. -//! -//! Only metadata is loaded into memory by the load function. -//! When images are needed, they are read directly from disk. -//! +//! Every image layer file consists of three parts: "summary", +//! "index", and "values". The summary is a fixed size header at the +//! beginning of the file, and it contains basic information about the +//! layer, and offsets to the other parts. The "index" is a B-tree, +//! mapping from Key to an offset in the "values" part. The +//! actual page images are stored in the "values" part. use crate::config::PageServerConf; +use crate::layered_repository::blob_io::{BlobCursor, BlobWriter, WriteBlobWriter}; +use crate::layered_repository::block_io::{BlockBuf, BlockReader, FileBlockReader}; +use crate::layered_repository::disk_btree::{DiskBtreeBuilder, DiskBtreeReader, VisitDirection}; use crate::layered_repository::filename::{ImageFileName, PathOrConf}; use crate::layered_repository::storage_layer::{ - BlobRef, Layer, ValueReconstructResult, ValueReconstructState, + Layer, ValueReconstructResult, ValueReconstructState, }; -use crate::repository::{Key, Value}; +use crate::page_cache::PAGE_SZ; +use crate::repository::{Key, Value, KEY_SIZE}; use crate::virtual_file::VirtualFile; use crate::{ZTenantId, ZTimelineId}; -use anyhow::{bail, Context, Result}; +use crate::{IMAGE_FILE_MAGIC, STORAGE_FORMAT_VERSION}; +use anyhow::{bail, ensure, Context, Result}; use bytes::Bytes; +use hex; use log::*; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use std::fs; -use std::io::{BufWriter, Write}; +use std::io::Write; +use std::io::{Seek, SeekFrom}; use std::ops::Range; use std::path::{Path, PathBuf}; -use std::sync::{Mutex, MutexGuard}; - -use bookfile::{Book, BookWriter, ChapterWriter}; +use std::sync::{RwLock, RwLockReadGuard}; use zenith_utils::bin_ser::BeSer; use zenith_utils::lsn::Lsn; -// Magic constant to identify a Zenith image layer file -pub const IMAGE_FILE_MAGIC: u32 = 0x5A616E11 + 1; - -/// Mapping from (key, lsn) -> page/WAL record -/// byte ranges in VALUES_CHAPTER -static INDEX_CHAPTER: u64 = 1; - -/// Contains each block in block # order -const VALUES_CHAPTER: u64 = 2; - -/// Contains the [`Summary`] struct -const SUMMARY_CHAPTER: u64 = 3; - +/// +/// Header stored in the beginning of the file +/// +/// After this comes the 'values' part, starting on block 1. After that, +/// the 'index' starts at the block indicated by 'index_start_blk' +/// #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] struct Summary { + /// Magic value to identify this as a zenith image file. Always IMAGE_FILE_MAGIC. + magic: u16, + format_version: u16, + tenantid: ZTenantId, timelineid: ZTimelineId, key_range: Range, - lsn: Lsn, + + /// Block number where the 'index' part of the file begins. + index_start_blk: u32, + /// Block within the 'index', where the B-tree root page is stored + index_root_blk: u32, + // the 'values' part starts after the summary header, on block 1. } impl From<&ImageLayer> for Summary { fn from(layer: &ImageLayer) -> Self { Self { + magic: IMAGE_FILE_MAGIC, + format_version: STORAGE_FORMAT_VERSION, tenantid: layer.tenantid, timelineid: layer.timelineid, key_range: layer.key_range.clone(), - lsn: layer.lsn, + + index_start_blk: 0, + index_root_blk: 0, } } } @@ -92,19 +103,19 @@ pub struct ImageLayer { // This entry contains an image of all pages as of this LSN pub lsn: Lsn, - inner: Mutex, + inner: RwLock, } pub struct ImageLayerInner { /// If false, the 'index' has not been loaded into memory yet. loaded: bool, - /// The underlying (virtual) file handle. None if the layer hasn't been loaded - /// yet. - book: Option>, + // values copied from summary + index_start_blk: u32, + index_root_blk: u32, - /// offset of each value - index: HashMap, + /// Reader object for reading blocks from the file. (None if not loaded yet) + file: Option>, } impl Layer for ImageLayer { @@ -135,30 +146,25 @@ impl Layer for ImageLayer { key: Key, lsn_range: Range, reconstruct_state: &mut ValueReconstructState, - ) -> Result { + ) -> anyhow::Result { assert!(self.key_range.contains(&key)); assert!(lsn_range.end >= self.lsn); let inner = self.load()?; - if let Some(blob_ref) = inner.index.get(&key) { - let chapter = inner - .book - .as_ref() - .unwrap() - .chapter_reader(VALUES_CHAPTER)?; + let file = inner.file.as_ref().unwrap(); + let tree_reader = DiskBtreeReader::new(inner.index_start_blk, inner.index_root_blk, file); - let mut blob = vec![0; blob_ref.size()]; - chapter - .read_exact_at(&mut blob, blob_ref.pos()) - .with_context(|| { - format!( - "failed to read {} bytes from data file {} at offset {}", - blob_ref.size(), - self.filename().display(), - blob_ref.pos() - ) - })?; + let mut keybuf: [u8; KEY_SIZE] = [0u8; KEY_SIZE]; + key.write_to_byte_slice(&mut keybuf); + if let Some(offset) = tree_reader.get(&keybuf)? { + let blob = file.block_cursor().read_blob(offset).with_context(|| { + format!( + "failed to read value from data file {} at offset {}", + self.filename().display(), + offset + ) + })?; let value = Bytes::from(blob); reconstruct_state.img = Some((self.lsn, value)); @@ -172,29 +178,6 @@ impl Layer for ImageLayer { todo!(); } - fn unload(&self) -> Result<()> { - // Unload the index. - // - // TODO: we should access the index directly from pages on the disk, - // using the buffer cache. This load/unload mechanism is really ad hoc. - - // FIXME: In debug mode, loading and unloading the index slows - // things down so much that you get timeout errors. At least - // with the test_parallel_copy test. So as an even more ad hoc - // stopgap fix for that, only unload every on average 10 - // checkpoint cycles. - use rand::RngCore; - if rand::thread_rng().next_u32() > (u32::MAX / 10) { - return Ok(()); - } - - let mut inner = self.inner.lock().unwrap(); - inner.index = HashMap::default(); - inner.loaded = false; - - Ok(()) - } - fn delete(&self) -> Result<()> { // delete underlying file fs::remove_file(self.path())?; @@ -210,26 +193,28 @@ impl Layer for ImageLayer { } /// debugging function to print out the contents of the layer - fn dump(&self) -> Result<()> { + fn dump(&self, verbose: bool) -> Result<()> { println!( "----- image layer for ten {} tli {} key {}-{} at {} ----", self.tenantid, self.timelineid, self.key_range.start, self.key_range.end, self.lsn ); - let inner = self.load()?; - - let mut index_vec: Vec<(&Key, &BlobRef)> = inner.index.iter().collect(); - index_vec.sort_by_key(|x| x.1.pos()); - - for (key, blob_ref) in index_vec { - println!( - "key: {} size {} offset {}", - key, - blob_ref.size(), - blob_ref.pos() - ); + if !verbose { + return Ok(()); } + let inner = self.load()?; + let file = inner.file.as_ref().unwrap(); + let tree_reader = + DiskBtreeReader::<_, KEY_SIZE>::new(inner.index_start_blk, inner.index_root_blk, file); + + tree_reader.dump()?; + + tree_reader.visit(&[0u8; KEY_SIZE], VisitDirection::Forwards, |key, value| { + println!("key: {} offset {}", hex::encode(key), value); + true + })?; + Ok(()) } } @@ -250,34 +235,55 @@ impl ImageLayer { } /// - /// Load the contents of the file into memory + /// Open the underlying file and read the metadata into memory, if it's + /// not loaded already. /// - fn load(&self) -> Result> { - // quick exit if already loaded - let mut inner = self.inner.lock().unwrap(); + fn load(&self) -> Result> { + loop { + // Quick exit if already loaded + let inner = self.inner.read().unwrap(); + if inner.loaded { + return Ok(inner); + } - if inner.loaded { - return Ok(inner); + // Need to open the file and load the metadata. Upgrade our lock to + // a write lock. (Or rather, release and re-lock in write mode.) + drop(inner); + let mut inner = self.inner.write().unwrap(); + if !inner.loaded { + self.load_inner(&mut inner)?; + } else { + // Another thread loaded it while we were not holding the lock. + } + + // We now have the file open and loaded. There's no function to do + // that in the std library RwLock, so we have to release and re-lock + // in read mode. (To be precise, the lock guard was moved in the + // above call to `load_inner`, so it's already been released). And + // while we do that, another thread could unload again, so we have + // to re-check and retry if that happens. + drop(inner); } + } + fn load_inner(&self, inner: &mut ImageLayerInner) -> Result<()> { let path = self.path(); // Open the file if it's not open already. - if inner.book.is_none() { + if inner.file.is_none() { let file = VirtualFile::open(&path) .with_context(|| format!("Failed to open file '{}'", path.display()))?; - inner.book = Some(Book::new(file).with_context(|| { - format!("Failed to open file '{}' as a bookfile", path.display()) - })?); + inner.file = Some(FileBlockReader::new(file)); } - let book = inner.book.as_ref().unwrap(); + let file = inner.file.as_mut().unwrap(); + let summary_blk = file.read_blk(0)?; + let actual_summary = Summary::des_prefix(summary_blk.as_ref())?; match &self.path_or_conf { PathOrConf::Conf(_) => { - let chapter = book.read_chapter(SUMMARY_CHAPTER)?; - let actual_summary = Summary::des(&chapter)?; - - let expected_summary = Summary::from(self); + let mut expected_summary = Summary::from(self); + expected_summary.index_start_blk = actual_summary.index_start_blk; + expected_summary.index_root_blk = actual_summary.index_root_blk; if actual_summary != expected_summary { bail!("in-file summary does not match expected summary. actual = {:?} expected = {:?}", actual_summary, expected_summary); @@ -297,15 +303,10 @@ impl ImageLayer { } } - let chapter = book.read_chapter(INDEX_CHAPTER)?; - let index = HashMap::des(&chapter)?; - - info!("loaded from {}", &path.display()); - - inner.index = index; + inner.index_start_blk = actual_summary.index_start_blk; + inner.index_root_blk = actual_summary.index_root_blk; inner.loaded = true; - - Ok(inner) + Ok(()) } /// Create an ImageLayer struct representing an existing file on disk @@ -321,10 +322,11 @@ impl ImageLayer { tenantid, key_range: filename.key_range.clone(), lsn: filename.lsn, - inner: Mutex::new(ImageLayerInner { - book: None, - index: HashMap::new(), + inner: RwLock::new(ImageLayerInner { loaded: false, + file: None, + index_start_blk: 0, + index_root_blk: 0, }), } } @@ -332,12 +334,14 @@ impl ImageLayer { /// Create an ImageLayer struct representing an existing file on disk. /// /// This variant is only used for debugging purposes, by the 'dump_layerfile' binary. - pub fn new_for_path(path: &Path, book: &Book) -> Result + pub fn new_for_path(path: &Path, file: F) -> Result where F: std::os::unix::prelude::FileExt, { - let chapter = book.read_chapter(SUMMARY_CHAPTER)?; - let summary = Summary::des(&chapter)?; + let mut summary_buf = Vec::new(); + summary_buf.resize(PAGE_SZ, 0); + file.read_exact_at(&mut summary_buf, 0)?; + let summary = Summary::des_prefix(&summary_buf)?; Ok(ImageLayer { path_or_conf: PathOrConf::Path(path.to_path_buf()), @@ -345,10 +349,11 @@ impl ImageLayer { tenantid: summary.tenantid, key_range: summary.key_range, lsn: summary.lsn, - inner: Mutex::new(ImageLayerInner { - book: None, - index: HashMap::new(), + inner: RwLock::new(ImageLayerInner { + file: None, loaded: false, + index_start_blk: 0, + index_root_blk: 0, }), }) } @@ -377,25 +382,21 @@ impl ImageLayer { /// /// 1. Create the ImageLayerWriter by calling ImageLayerWriter::new(...) /// -/// 2. Write the contents by calling `put_page_image` for every page -/// in the segment. +/// 2. Write the contents by calling `put_page_image` for every key-value +/// pair in the key range. /// /// 3. Call `finish`. /// pub struct ImageLayerWriter { conf: &'static PageServerConf, - path: PathBuf, + _path: PathBuf, timelineid: ZTimelineId, tenantid: ZTenantId, key_range: Range, lsn: Lsn, - values_writer: Option>>, - end_offset: u64, - - index: HashMap, - - finished: bool, + blob_writer: WriteBlobWriter, + tree: DiskBtreeBuilder, } impl ImageLayerWriter { @@ -405,7 +406,7 @@ impl ImageLayerWriter { tenantid: ZTenantId, key_range: &Range, lsn: Lsn, - ) -> Result { + ) -> anyhow::Result { // Create the file // // Note: This overwrites any existing file. There shouldn't be any. @@ -420,25 +421,24 @@ impl ImageLayerWriter { }, ); info!("new image layer {}", path.display()); - let file = VirtualFile::create(&path)?; - let buf_writer = BufWriter::new(file); - let book = BookWriter::new(buf_writer, IMAGE_FILE_MAGIC)?; + let mut file = VirtualFile::create(&path)?; + // make room for the header block + file.seek(SeekFrom::Start(PAGE_SZ as u64))?; + let blob_writer = WriteBlobWriter::new(file, PAGE_SZ as u64); - // Open the page-images chapter for writing. The calls to - // `put_image` will use this to write the contents. - let chapter = book.new_chapter(VALUES_CHAPTER); + // Initialize the b-tree index builder + let block_buf = BlockBuf::new(); + let tree_builder = DiskBtreeBuilder::new(block_buf); let writer = ImageLayerWriter { conf, - path, + _path: path, timelineid, tenantid, key_range: key_range.clone(), lsn, - values_writer: Some(chapter), - index: HashMap::new(), - end_offset: 0, - finished: false, + tree: tree_builder, + blob_writer, }; Ok(writer) @@ -450,79 +450,61 @@ impl ImageLayerWriter { /// The page versions must be appended in blknum order. /// pub fn put_image(&mut self, key: Key, img: &[u8]) -> Result<()> { - assert!(self.key_range.contains(&key)); - let off = self.end_offset; + ensure!(self.key_range.contains(&key)); + let off = self.blob_writer.write_blob(img)?; - if let Some(writer) = &mut self.values_writer { - let len = img.len(); - writer.write_all(img)?; - self.end_offset += len as u64; - - let old = self.index.insert(key, BlobRef::new(off, len, true)); - assert!(old.is_none()); - } else { - panic!() - } + let mut keybuf: [u8; KEY_SIZE] = [0u8; KEY_SIZE]; + key.write_to_byte_slice(&mut keybuf); + self.tree.append(&keybuf, off)?; Ok(()) } - pub fn finish(&mut self) -> Result { - // Close the values chapter - let book = self.values_writer.take().unwrap().close()?; + pub fn finish(self) -> anyhow::Result { + let index_start_blk = + ((self.blob_writer.size() + PAGE_SZ as u64 - 1) / PAGE_SZ as u64) as u32; + + let mut file = self.blob_writer.into_inner(); // Write out the index - let mut chapter = book.new_chapter(INDEX_CHAPTER); - let buf = HashMap::ser(&self.index)?; - chapter.write_all(&buf)?; - let book = chapter.close()?; + file.seek(SeekFrom::Start(index_start_blk as u64 * PAGE_SZ as u64))?; + let (index_root_blk, block_buf) = self.tree.finish()?; + for buf in block_buf.blocks { + file.write_all(buf.as_ref())?; + } - // Write out the summary chapter - let mut chapter = book.new_chapter(SUMMARY_CHAPTER); + // Fill in the summary on blk 0 let summary = Summary { + magic: IMAGE_FILE_MAGIC, + format_version: STORAGE_FORMAT_VERSION, tenantid: self.tenantid, timelineid: self.timelineid, key_range: self.key_range.clone(), lsn: self.lsn, + index_start_blk, + index_root_blk, }; - Summary::ser_into(&summary, &mut chapter)?; - let book = chapter.close()?; - - // This flushes the underlying 'buf_writer'. - book.close()?; + file.seek(SeekFrom::Start(0))?; + Summary::ser_into(&summary, &mut file)?; // Note: Because we open the file in write-only mode, we cannot // reuse the same VirtualFile for reading later. That's why we don't - // set inner.book here. The first read will have to re-open it. + // set inner.file here. The first read will have to re-open it. let layer = ImageLayer { path_or_conf: PathOrConf::Conf(self.conf), timelineid: self.timelineid, tenantid: self.tenantid, key_range: self.key_range.clone(), lsn: self.lsn, - inner: Mutex::new(ImageLayerInner { - book: None, + inner: RwLock::new(ImageLayerInner { loaded: false, - index: HashMap::new(), + file: None, + index_start_blk, + index_root_blk, }), }; trace!("created image layer {}", layer.path().display()); - self.finished = true; - Ok(layer) } } - -impl Drop for ImageLayerWriter { - fn drop(&mut self) { - if let Some(page_image_writer) = self.values_writer.take() { - if let Ok(book) = page_image_writer.close() { - let _ = book.close(); - } - } - if !self.finished { - let _ = fs::remove_file(&self.path); - } - } -} diff --git a/pageserver/src/layered_repository/inmemory_layer.rs b/pageserver/src/layered_repository/inmemory_layer.rs index 1e2f4f52df..a45af51487 100644 --- a/pageserver/src/layered_repository/inmemory_layer.rs +++ b/pageserver/src/layered_repository/inmemory_layer.rs @@ -5,23 +5,23 @@ //! its position in the file, is kept in memory, though. //! use crate::config::PageServerConf; +use crate::layered_repository::blob_io::{BlobCursor, BlobWriter}; +use crate::layered_repository::block_io::BlockReader; use crate::layered_repository::delta_layer::{DeltaLayer, DeltaLayerWriter}; use crate::layered_repository::ephemeral_file::EphemeralFile; use crate::layered_repository::storage_layer::{ - BlobRef, Layer, ValueReconstructResult, ValueReconstructState, + Layer, ValueReconstructResult, ValueReconstructState, }; use crate::repository::{Key, Value}; use crate::walrecord; use crate::{ZTenantId, ZTimelineId}; -use anyhow::Result; +use anyhow::{bail, ensure, Result}; use log::*; use std::collections::HashMap; // avoid binding to Write (conflicts with std::io::Write) // while being able to use std::fmt::Write's methods use std::fmt::Write as _; -use std::io::Write; use std::ops::Range; -use std::os::unix::fs::FileExt; use std::path::PathBuf; use std::sync::RwLock; use zenith_utils::bin_ser::BeSer; @@ -54,14 +54,12 @@ pub struct InMemoryLayerInner { /// by block number and LSN. The value is an offset into the /// ephemeral file where the page version is stored. /// - index: HashMap>, + index: HashMap>, /// The values are stored in a serialized format in this file. /// Each serialized Value is preceded by a 'u32' length field. /// PerSeg::page_versions map stores offsets into this file. file: EphemeralFile, - - end_offset: u64, } impl InMemoryLayerInner { @@ -114,16 +112,18 @@ impl Layer for InMemoryLayer { key: Key, lsn_range: Range, reconstruct_state: &mut ValueReconstructState, - ) -> Result { - assert!(lsn_range.start <= self.start_lsn); + ) -> anyhow::Result { + ensure!(lsn_range.start <= self.start_lsn); let mut need_image = true; let inner = self.inner.read().unwrap(); + let mut reader = inner.file.block_cursor(); + // Scan the page versions backwards, starting from `lsn`. if let Some(vec_map) = inner.index.get(&key) { let slice = vec_map.slice_range(lsn_range); - for (entry_lsn, blob_ref) in slice.iter().rev() { + for (entry_lsn, pos) in slice.iter().rev() { match &reconstruct_state.img { Some((cached_lsn, _)) if entry_lsn <= cached_lsn => { return Ok(ValueReconstructResult::Complete) @@ -131,8 +131,7 @@ impl Layer for InMemoryLayer { _ => {} } - let mut buf = vec![0u8; blob_ref.size()]; - inner.file.read_exact_at(&mut buf, blob_ref.pos())?; + let buf = reader.read_blob(*pos)?; let value = Value::des(&buf)?; match value { Value::Image(img) => { @@ -167,17 +166,10 @@ impl Layer for InMemoryLayer { todo!(); } - /// Cannot unload anything in an in-memory layer, since there's no backing - /// store. To release memory used by an in-memory layer, use 'freeze' to turn - /// it into an on-disk layer. - fn unload(&self) -> Result<()> { - Ok(()) - } - /// Nothing to do here. When you drop the last reference to the layer, it will /// be deallocated. fn delete(&self) -> Result<()> { - panic!("can't delete an InMemoryLayer") + bail!("can't delete an InMemoryLayer") } fn is_incremental(&self) -> bool { @@ -190,7 +182,7 @@ impl Layer for InMemoryLayer { } /// debugging function to print out the contents of the layer - fn dump(&self) -> Result<()> { + fn dump(&self, verbose: bool) -> Result<()> { let inner = self.inner.read().unwrap(); let end_str = inner @@ -204,12 +196,16 @@ impl Layer for InMemoryLayer { self.timelineid, self.start_lsn, end_str, ); + if !verbose { + return Ok(()); + } + + let mut cursor = inner.file.block_cursor(); let mut buf = Vec::new(); for (key, vec_map) in inner.index.iter() { - for (lsn, blob_ref) in vec_map.as_slice() { + for (lsn, pos) in vec_map.as_slice() { let mut desc = String::new(); - buf.resize(blob_ref.size(), 0); - inner.file.read_exact_at(&mut buf, blob_ref.pos())?; + cursor.read_blob_into_buf(*pos, &mut buf)?; let val = Value::des(&buf); match val { Ok(Value::Image(img)) => { @@ -264,7 +260,6 @@ impl InMemoryLayer { end_lsn: None, index: HashMap::new(), file, - end_offset: 0, }), }) } @@ -279,15 +274,10 @@ impl InMemoryLayer { inner.assert_writeable(); - let off = inner.end_offset; - let buf = Value::ser(&val)?; - let len = buf.len(); - inner.file.write_all(&buf)?; - inner.end_offset += len as u64; + let off = inner.file.write_blob(&Value::ser(&val)?)?; let vec_map = inner.index.entry(key).or_default(); - let blob_ref = BlobRef::new(off, len, val.will_init()); - let old = vec_map.append_or_update_last(lsn, blob_ref).unwrap().0; + let old = vec_map.append_or_update_last(lsn, off).unwrap().0; if old.is_some() { // We already had an entry for this LSN. That's odd.. warn!("Key {} at {} already exists", key, lsn); @@ -311,30 +301,16 @@ impl InMemoryLayer { assert!(self.start_lsn < end_lsn); inner.end_lsn = Some(end_lsn); - // FIXME - /* - for perseg in inner.segs.values() { - if let Some((lsn, _)) = perseg.seg_sizes.as_slice().last() { - assert!(lsn < &end_lsn, "{:?} {:?}", lsn, end_lsn); - } - - for (_blk, vec_map) in perseg.page_versions.iter() { - for (lsn, _pos) in vec_map.as_slice() { - assert!(*lsn < end_lsn); - } - } - } - */ + for vec_map in inner.index.values() { + for (lsn, _pos) in vec_map.as_slice() { + assert!(*lsn < end_lsn); + } + } } /// Write this frozen in-memory layer to disk. /// - /// Returns new layers that replace this one. - /// If not dropped and reconstruct_pages is true, returns a new image layer containing the page versions - /// at the `end_lsn`. Can also return a DeltaLayer that includes all the - /// WAL records between start and end LSN. (The delta layer is not needed - /// when a new relish is created with a single LSN, so that the start and - /// end LSN are the same.) + /// Returns a new delta layer with all the same data as this in-memory layer pub fn write_to_disk(&self) -> Result { // Grab the lock in read-mode. We hold it over the I/O, but because this // layer is not writeable anymore, no one should be trying to acquire the @@ -355,21 +331,21 @@ impl InMemoryLayer { self.start_lsn..inner.end_lsn.unwrap(), )?; - let mut do_steps = || -> Result<()> { - for (key, vec_map) in inner.index.iter() { - // Write all page versions - for (lsn, blob_ref) in vec_map.as_slice() { - let mut buf = vec![0u8; blob_ref.size()]; - inner.file.read_exact_at(&mut buf, blob_ref.pos())?; - let val = Value::des(&buf)?; - delta_layer_writer.put_value(*key, *lsn, val)?; - } + let mut buf = Vec::new(); + + let mut cursor = inner.file.block_cursor(); + + let mut keys: Vec<(&Key, &VecMap)> = inner.index.iter().collect(); + keys.sort_by_key(|k| k.0); + + for (key, vec_map) in keys.iter() { + let key = **key; + // Write all page versions + for (lsn, pos) in vec_map.as_slice() { + cursor.read_blob_into_buf(*pos, &mut buf)?; + let val = Value::des(&buf)?; + delta_layer_writer.put_value(key, *lsn, val)?; } - Ok(()) - }; - if let Err(err) = do_steps() { - delta_layer_writer.abort(); - return Err(err); } let delta_layer = delta_layer_writer.finish(Key::MAX)?; diff --git a/pageserver/src/layered_repository/layer_map.rs b/pageserver/src/layered_repository/layer_map.rs index 4b0d950414..3984ee550f 100644 --- a/pageserver/src/layered_repository/layer_map.rs +++ b/pageserver/src/layered_repository/layer_map.rs @@ -1,5 +1,5 @@ //! -//! The layer map tracks what layers exist for all the relishes in a timeline. +//! The layer map tracks what layers exist in a timeline. //! //! When the timeline is first accessed, the server lists of all layer files //! in the timelines/ directory, and populates this map with @@ -61,12 +61,24 @@ pub struct LayerMap { historic_layers: Vec>, } +/// Return value of LayerMap::search pub struct SearchResult { pub layer: Arc, pub lsn_floor: Lsn, } impl LayerMap { + /// + /// Find the latest layer that covers the given 'key', with lsn < + /// 'end_lsn'. + /// + /// Returns the layer, if any, and an 'lsn_floor' value that + /// indicates which portion of the layer the caller should + /// check. 'lsn_floor' is normally the start-LSN of the layer, but + /// can be greater if there is an overlapping layer that might + /// contain the version, even if it's missing from the returned + /// layer. + /// pub fn search(&self, key: Key, end_lsn: Lsn) -> Result> { // linear search // Find the latest image layer that covers the given key @@ -195,11 +207,11 @@ impl LayerMap { NUM_ONDISK_LAYERS.dec(); } - /// Is there a newer image layer for given segment? + /// Is there a newer image layer for given key-range? /// /// This is used for garbage collection, to determine if an old layer can /// be deleted. - /// We ignore segments newer than disk_consistent_lsn because they will be removed at restart + /// We ignore layers newer than disk_consistent_lsn because they will be removed at restart /// We also only look at historic layers //#[allow(dead_code)] pub fn newer_image_layer_exists( @@ -238,28 +250,13 @@ impl LayerMap { } } - /// Is there any layer for given segment that is alive at the lsn? - /// - /// This is a public wrapper for SegEntry fucntion, - /// used for garbage collection, to determine if some alive layer - /// exists at the lsn. If so, we shouldn't delete a newer dropped layer - /// to avoid incorrectly making it visible. - /* - pub fn layer_exists_at_lsn(&self, seg: SegmentTag, lsn: Lsn) -> Result { - Ok(if let Some(segentry) = self.historic_layers.get(&seg) { - segentry.exists_at_lsn(seg, lsn)?.unwrap_or(false) - } else { - false - }) - } - */ - pub fn iter_historic_layers(&self) -> std::slice::Iter> { self.historic_layers.iter() } + /// Find the last image layer that covers 'key', ignoring any image layers + /// newer than 'lsn'. fn find_latest_image(&self, key: Key, lsn: Lsn) -> Option> { - // Find the last image layer that covers the key let mut candidate_lsn = Lsn(0); let mut candidate = None; for l in self.historic_layers.iter() { @@ -299,9 +296,7 @@ impl LayerMap { key_range: &Range, lsn: Lsn, ) -> Result, Option>)>> { - let mut points: Vec; - - points = vec![key_range.start]; + let mut points = vec![key_range.start]; for l in self.historic_layers.iter() { if l.get_lsn_range().start > lsn { continue; @@ -334,6 +329,8 @@ impl LayerMap { Ok(ranges) } + /// Count how many L1 delta layers there are that overlap with the + /// given key and LSN range. pub fn count_deltas(&self, key_range: &Range, lsn_range: &Range) -> Result { let mut result = 0; for l in self.historic_layers.iter() { @@ -360,6 +357,7 @@ impl LayerMap { Ok(result) } + /// Return all L0 delta layers pub fn get_level0_deltas(&self) -> Result>> { let mut deltas = Vec::new(); for l in self.historic_layers.iter() { @@ -376,10 +374,22 @@ impl LayerMap { /// debugging function to print out the contents of the layer map #[allow(unused)] - pub fn dump(&self) -> Result<()> { + pub fn dump(&self, verbose: bool) -> Result<()> { println!("Begin dump LayerMap"); + + println!("open_layer:"); + if let Some(open_layer) = &self.open_layer { + open_layer.dump(verbose)?; + } + + println!("frozen_layers:"); + for frozen_layer in self.frozen_layers.iter() { + frozen_layer.dump(verbose)?; + } + + println!("historic_layers:"); for layer in self.historic_layers.iter() { - layer.dump()?; + layer.dump(verbose)?; } println!("End dump LayerMap"); Ok(()) diff --git a/pageserver/src/layered_repository/metadata.rs b/pageserver/src/layered_repository/metadata.rs index 960a1b7fe3..7daf899ba2 100644 --- a/pageserver/src/layered_repository/metadata.rs +++ b/pageserver/src/layered_repository/metadata.rs @@ -6,9 +6,10 @@ //! //! The module contains all structs and related helper methods related to timeline metadata. -use std::{convert::TryInto, path::PathBuf}; +use std::path::PathBuf; use anyhow::ensure; +use serde::{Deserialize, Serialize}; use zenith_utils::{ bin_ser::BeSer, lsn::Lsn, @@ -16,11 +17,13 @@ use zenith_utils::{ }; use crate::config::PageServerConf; +use crate::STORAGE_FORMAT_VERSION; -// Taken from PG_CONTROL_MAX_SAFE_SIZE -const METADATA_MAX_SAFE_SIZE: usize = 512; -const METADATA_CHECKSUM_SIZE: usize = std::mem::size_of::(); -const METADATA_MAX_DATA_SIZE: usize = METADATA_MAX_SAFE_SIZE - METADATA_CHECKSUM_SIZE; +/// We assume that a write of up to METADATA_MAX_SIZE bytes is atomic. +/// +/// This is the same assumption that PostgreSQL makes with the control file, +/// see PG_CONTROL_MAX_SAFE_SIZE +const METADATA_MAX_SIZE: usize = 512; /// The name of the metadata file pageserver creates per timeline. pub const METADATA_FILE_NAME: &str = "metadata"; @@ -28,8 +31,22 @@ pub const METADATA_FILE_NAME: &str = "metadata"; /// Metadata stored on disk for each timeline /// /// The fields correspond to the values we hold in memory, in LayeredTimeline. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct TimelineMetadata { + hdr: TimelineMetadataHeader, + body: TimelineMetadataBody, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +struct TimelineMetadataHeader { + checksum: u32, // CRC of serialized metadata body + size: u16, // size of serialized metadata + format_version: u16, // storage format version (used for compatibility checks) +} +const METADATA_HDR_SIZE: usize = std::mem::size_of::(); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +struct TimelineMetadataBody { disk_consistent_lsn: Lsn, // This is only set if we know it. We track it in memory when the page // server is running, but we only track the value corresponding to @@ -69,130 +86,90 @@ impl TimelineMetadata { initdb_lsn: Lsn, ) -> Self { Self { - disk_consistent_lsn, - prev_record_lsn, - ancestor_timeline, - ancestor_lsn, - latest_gc_cutoff_lsn, - initdb_lsn, + hdr: TimelineMetadataHeader { + checksum: 0, + size: 0, + format_version: STORAGE_FORMAT_VERSION, + }, + body: TimelineMetadataBody { + disk_consistent_lsn, + prev_record_lsn, + ancestor_timeline, + ancestor_lsn, + latest_gc_cutoff_lsn, + initdb_lsn, + }, } } pub fn from_bytes(metadata_bytes: &[u8]) -> anyhow::Result { ensure!( - metadata_bytes.len() == METADATA_MAX_SAFE_SIZE, + metadata_bytes.len() == METADATA_MAX_SIZE, "metadata bytes size is wrong" ); - - let data = &metadata_bytes[..METADATA_MAX_DATA_SIZE]; - let calculated_checksum = crc32c::crc32c(data); - - let checksum_bytes: &[u8; METADATA_CHECKSUM_SIZE] = - metadata_bytes[METADATA_MAX_DATA_SIZE..].try_into()?; - let expected_checksum = u32::from_le_bytes(*checksum_bytes); + let hdr = TimelineMetadataHeader::des(&metadata_bytes[0..METADATA_HDR_SIZE])?; ensure!( - calculated_checksum == expected_checksum, + hdr.format_version == STORAGE_FORMAT_VERSION, + "format version mismatch" + ); + let metadata_size = hdr.size as usize; + ensure!( + metadata_size <= METADATA_MAX_SIZE, + "corrupted metadata file" + ); + let calculated_checksum = crc32c::crc32c(&metadata_bytes[METADATA_HDR_SIZE..metadata_size]); + ensure!( + hdr.checksum == calculated_checksum, "metadata checksum mismatch" ); + let body = TimelineMetadataBody::des(&metadata_bytes[METADATA_HDR_SIZE..metadata_size])?; + ensure!( + body.disk_consistent_lsn.is_aligned(), + "disk_consistent_lsn is not aligned" + ); - let data = TimelineMetadata::from(serialize::DeTimelineMetadata::des_prefix(data)?); - assert!(data.disk_consistent_lsn.is_aligned()); - - Ok(data) + Ok(TimelineMetadata { hdr, body }) } pub fn to_bytes(&self) -> anyhow::Result> { - let serializeable_metadata = serialize::SeTimelineMetadata::from(self); - let mut metadata_bytes = serialize::SeTimelineMetadata::ser(&serializeable_metadata)?; - assert!(metadata_bytes.len() <= METADATA_MAX_DATA_SIZE); - metadata_bytes.resize(METADATA_MAX_SAFE_SIZE, 0u8); - - let checksum = crc32c::crc32c(&metadata_bytes[..METADATA_MAX_DATA_SIZE]); - metadata_bytes[METADATA_MAX_DATA_SIZE..].copy_from_slice(&u32::to_le_bytes(checksum)); + let body_bytes = self.body.ser()?; + let metadata_size = METADATA_HDR_SIZE + body_bytes.len(); + let hdr = TimelineMetadataHeader { + size: metadata_size as u16, + format_version: STORAGE_FORMAT_VERSION, + checksum: crc32c::crc32c(&body_bytes), + }; + let hdr_bytes = hdr.ser()?; + let mut metadata_bytes = vec![0u8; METADATA_MAX_SIZE]; + metadata_bytes[0..METADATA_HDR_SIZE].copy_from_slice(&hdr_bytes); + metadata_bytes[METADATA_HDR_SIZE..metadata_size].copy_from_slice(&body_bytes); Ok(metadata_bytes) } /// [`Lsn`] that corresponds to the corresponding timeline directory /// contents, stored locally in the pageserver workdir. pub fn disk_consistent_lsn(&self) -> Lsn { - self.disk_consistent_lsn + self.body.disk_consistent_lsn } pub fn prev_record_lsn(&self) -> Option { - self.prev_record_lsn + self.body.prev_record_lsn } pub fn ancestor_timeline(&self) -> Option { - self.ancestor_timeline + self.body.ancestor_timeline } pub fn ancestor_lsn(&self) -> Lsn { - self.ancestor_lsn + self.body.ancestor_lsn } pub fn latest_gc_cutoff_lsn(&self) -> Lsn { - self.latest_gc_cutoff_lsn + self.body.latest_gc_cutoff_lsn } pub fn initdb_lsn(&self) -> Lsn { - self.initdb_lsn - } -} - -/// This module is for direct conversion of metadata to bytes and back. -/// For a certain metadata, besides the conversion a few verification steps has to -/// be done, so all serde derives are hidden from the user, to avoid accidental -/// verification-less metadata creation. -mod serialize { - use serde::{Deserialize, Serialize}; - use zenith_utils::{lsn::Lsn, zid::ZTimelineId}; - - use super::TimelineMetadata; - - #[derive(Serialize)] - pub(super) struct SeTimelineMetadata<'a> { - disk_consistent_lsn: &'a Lsn, - prev_record_lsn: &'a Option, - ancestor_timeline: &'a Option, - ancestor_lsn: &'a Lsn, - latest_gc_cutoff_lsn: &'a Lsn, - initdb_lsn: &'a Lsn, - } - - impl<'a> From<&'a TimelineMetadata> for SeTimelineMetadata<'a> { - fn from(other: &'a TimelineMetadata) -> Self { - Self { - disk_consistent_lsn: &other.disk_consistent_lsn, - prev_record_lsn: &other.prev_record_lsn, - ancestor_timeline: &other.ancestor_timeline, - ancestor_lsn: &other.ancestor_lsn, - latest_gc_cutoff_lsn: &other.latest_gc_cutoff_lsn, - initdb_lsn: &other.initdb_lsn, - } - } - } - - #[derive(Deserialize)] - pub(super) struct DeTimelineMetadata { - disk_consistent_lsn: Lsn, - prev_record_lsn: Option, - ancestor_timeline: Option, - ancestor_lsn: Lsn, - latest_gc_cutoff_lsn: Lsn, - initdb_lsn: Lsn, - } - - impl From for TimelineMetadata { - fn from(other: DeTimelineMetadata) -> Self { - Self { - disk_consistent_lsn: other.disk_consistent_lsn, - prev_record_lsn: other.prev_record_lsn, - ancestor_timeline: other.ancestor_timeline, - ancestor_lsn: other.ancestor_lsn, - latest_gc_cutoff_lsn: other.latest_gc_cutoff_lsn, - initdb_lsn: other.initdb_lsn, - } - } + self.body.initdb_lsn } } @@ -204,14 +181,14 @@ mod tests { #[test] fn metadata_serializes_correctly() { - let original_metadata = TimelineMetadata { - disk_consistent_lsn: Lsn(0x200), - prev_record_lsn: Some(Lsn(0x100)), - ancestor_timeline: Some(TIMELINE_ID), - ancestor_lsn: Lsn(0), - latest_gc_cutoff_lsn: Lsn(0), - initdb_lsn: Lsn(0), - }; + let original_metadata = TimelineMetadata::new( + Lsn(0x200), + Some(Lsn(0x100)), + Some(TIMELINE_ID), + Lsn(0), + Lsn(0), + Lsn(0), + ); let metadata_bytes = original_metadata .to_bytes() @@ -221,7 +198,7 @@ mod tests { .expect("Should deserialize its own bytes"); assert_eq!( - deserialized_metadata, original_metadata, + deserialized_metadata.body, original_metadata.body, "Metadata that was serialized to bytes and deserialized back should not change" ); } diff --git a/pageserver/src/layered_repository/storage_layer.rs b/pageserver/src/layered_repository/storage_layer.rs index 5847f9cb75..e413f311c3 100644 --- a/pageserver/src/layered_repository/storage_layer.rs +++ b/pageserver/src/layered_repository/storage_layer.rs @@ -7,7 +7,6 @@ use crate::walrecord::ZenithWalRecord; use crate::{ZTenantId, ZTimelineId}; use anyhow::Result; use bytes::Bytes; -use serde::{Deserialize, Serialize}; use std::ops::Range; use std::path::PathBuf; @@ -85,10 +84,10 @@ pub enum ValueReconstructResult { pub trait Layer: Send + Sync { fn get_tenant_id(&self) -> ZTenantId; - /// Identify the timeline this relish belongs to + /// Identify the timeline this layer belongs to fn get_timeline_id(&self) -> ZTimelineId; - /// Range of segments that this layer covers + /// Range of keys that this layer covers fn get_key_range(&self) -> Range; /// Inclusive start bound of the LSN range that this layer holds @@ -123,7 +122,7 @@ pub trait Layer: Send + Sync { reconstruct_data: &mut ValueReconstructState, ) -> Result; - /// Does this layer only contain some data for the segment (incremental), + /// Does this layer only contain some data for the key-range (incremental), /// or does it contain a version of every page? This is important to know /// for garbage collecting old layers: an incremental layer depends on /// the previous non-incremental layer. @@ -135,46 +134,9 @@ pub trait Layer: Send + Sync { /// Iterate through all keys and values stored in the layer fn iter(&self) -> Box> + '_>; - /// Release memory used by this layer. There is no corresponding 'load' - /// function, that's done implicitly when you call one of the get-functions. - fn unload(&self) -> Result<()>; - /// Permanently remove this layer from disk. fn delete(&self) -> Result<()>; /// Dump summary of the contents of the layer to stdout - fn dump(&self) -> Result<()>; -} - -// Flag indicating that this version initialize the page -const WILL_INIT: u64 = 1; - -/// -/// Struct representing reference to BLOB in layers. Reference contains BLOB offset and size. -/// For WAL records (delta layer) it also contains `will_init` flag which helps to determine range of records -/// which needs to be applied without reading/deserializing records themselves. -/// -#[derive(Debug, Serialize, Deserialize, Copy, Clone)] -pub struct BlobRef(u64); - -impl BlobRef { - pub fn will_init(&self) -> bool { - (self.0 & WILL_INIT) != 0 - } - - pub fn pos(&self) -> u64 { - self.0 >> 32 - } - - pub fn size(&self) -> usize { - ((self.0 & 0xFFFFFFFF) >> 1) as usize - } - - pub fn new(pos: u64, size: usize, will_init: bool) -> BlobRef { - let mut blob_ref = (pos << 32) | ((size as u64) << 1); - if will_init { - blob_ref |= WILL_INIT; - } - BlobRef(blob_ref) - } + fn dump(&self, verbose: bool) -> Result<()>; } diff --git a/pageserver/src/lib.rs b/pageserver/src/lib.rs index be8cc9122b..65bd2b55c3 100644 --- a/pageserver/src/lib.rs +++ b/pageserver/src/lib.rs @@ -7,7 +7,7 @@ pub mod layered_repository; pub mod page_cache; pub mod page_service; pub mod pgdatadir_mapping; -pub mod relish; +pub mod reltag; pub mod remote_storage; pub mod repository; pub mod tenant_mgr; @@ -22,12 +22,29 @@ pub mod walredo; pub mod wal_metadata; use lazy_static::lazy_static; +use tracing::info; use zenith_metrics::{register_int_gauge_vec, IntGaugeVec}; -use zenith_utils::zid::{ZTenantId, ZTimelineId}; +use zenith_utils::{ + postgres_backend, + zid::{ZTenantId, ZTimelineId}, +}; + +use crate::thread_mgr::ThreadKind; use layered_repository::LayeredRepository; use pgdatadir_mapping::DatadirTimeline; +/// Current storage format version +/// +/// This is embedded in the metadata file, and also in the header of all the +/// layer files. If you make any backwards-incompatible changes to the storage +/// format, bump this! +pub const STORAGE_FORMAT_VERSION: u16 = 3; + +// Magic constants used to identify different kinds of files +pub const IMAGE_FILE_MAGIC: u16 = 0x5A60; +pub const DELTA_FILE_MAGIC: u16 = 0x5A61; + lazy_static! { static ref LIVE_CONNECTIONS_COUNT: IntGaugeVec = register_int_gauge_vec!( "pageserver_live_connections_count", @@ -51,3 +68,33 @@ pub enum CheckpointConfig { pub type RepositoryImpl = LayeredRepository; pub type DatadirTimelineImpl = DatadirTimeline; + +pub fn shutdown_pageserver() { + // Shut down the libpq endpoint thread. This prevents new connections from + // being accepted. + thread_mgr::shutdown_threads(Some(ThreadKind::LibpqEndpointListener), None, None); + + // Shut down any page service threads. + postgres_backend::set_pgbackend_shutdown_requested(); + thread_mgr::shutdown_threads(Some(ThreadKind::PageRequestHandler), None, None); + + // Shut down all the tenants. This flushes everything to disk and kills + // the checkpoint and GC threads. + tenant_mgr::shutdown_all_tenants(); + + // Stop syncing with remote storage. + // + // FIXME: Does this wait for the sync thread to finish syncing what's queued up? + // Should it? + thread_mgr::shutdown_threads(Some(ThreadKind::StorageSync), None, None); + + // Shut down the HTTP endpoint last, so that you can still check the server's + // status while it's shutting down. + thread_mgr::shutdown_threads(Some(ThreadKind::HttpEndpointListener), None, None); + + // There should be nothing left, but let's be sure + thread_mgr::shutdown_threads(None, None, None); + + info!("Shut down successfully completed"); + std::process::exit(0); +} diff --git a/pageserver/src/page_cache.rs b/pageserver/src/page_cache.rs index 90ab5622bd..bc6eb0e88b 100644 --- a/pageserver/src/page_cache.rs +++ b/pageserver/src/page_cache.rs @@ -41,7 +41,7 @@ use std::{ convert::TryInto, sync::{ atomic::{AtomicU8, AtomicUsize, Ordering}, - RwLock, RwLockReadGuard, RwLockWriteGuard, + RwLock, RwLockReadGuard, RwLockWriteGuard, TryLockError, }, }; @@ -57,16 +57,13 @@ use crate::layered_repository::writeback_ephemeral_file; use crate::repository::Key; static PAGE_CACHE: OnceCell = OnceCell::new(); -const TEST_PAGE_CACHE_SIZE: usize = 10; +const TEST_PAGE_CACHE_SIZE: usize = 50; /// /// Initialize the page cache. This must be called once at page server startup. /// -pub fn init(conf: &'static PageServerConf) { - if PAGE_CACHE - .set(PageCache::new(conf.page_cache_size)) - .is_err() - { +pub fn init(size: usize) { + if PAGE_CACHE.set(PageCache::new(size)).is_err() { panic!("page cache already initialized"); } } @@ -94,6 +91,7 @@ const MAX_USAGE_COUNT: u8 = 5; /// CacheKey uniquely identifies a "thing" to cache in the page cache. /// #[derive(Debug, PartialEq, Eq, Clone)] +#[allow(clippy::enum_variant_names)] enum CacheKey { MaterializedPage { hash_key: MaterializedPageHashKey, @@ -103,6 +101,10 @@ enum CacheKey { file_id: u64, blkno: u32, }, + ImmutableFilePage { + file_id: u64, + blkno: u32, + }, } #[derive(Debug, PartialEq, Eq, Hash, Clone)] @@ -177,6 +179,8 @@ pub struct PageCache { ephemeral_page_map: RwLock>, + immutable_page_map: RwLock>, + /// The actual buffers with their metadata. slots: Box<[Slot]>, @@ -199,6 +203,12 @@ impl std::ops::Deref for PageReadGuard<'_> { } } +impl AsRef<[u8; PAGE_SZ]> for PageReadGuard<'_> { + fn as_ref(&self) -> &[u8; PAGE_SZ] { + self.0.buf + } +} + /// /// PageWriteGuard is a lease on a buffer for modifying it. The page is kept locked /// until the guard is dropped. @@ -230,6 +240,12 @@ impl std::ops::Deref for PageWriteGuard<'_> { } } +impl AsMut<[u8; PAGE_SZ]> for PageWriteGuard<'_> { + fn as_mut(&mut self) -> &mut [u8; PAGE_SZ] { + self.inner.buf + } +} + impl PageWriteGuard<'_> { /// Mark that the buffer contents are now valid. pub fn mark_valid(&mut self) { @@ -385,6 +401,36 @@ impl PageCache { } } + // Section 1.3: Public interface functions for working with immutable file pages. + + pub fn read_immutable_buf(&self, file_id: u64, blkno: u32) -> ReadBufResult { + let mut cache_key = CacheKey::ImmutableFilePage { file_id, blkno }; + + self.lock_for_read(&mut cache_key) + } + + /// Immediately drop all buffers belonging to given file, without writeback + pub fn drop_buffers_for_immutable(&self, drop_file_id: u64) { + for slot_idx in 0..self.slots.len() { + let slot = &self.slots[slot_idx]; + + let mut inner = slot.inner.write().unwrap(); + if let Some(key) = &inner.key { + match key { + CacheKey::ImmutableFilePage { file_id, blkno: _ } + if *file_id == drop_file_id => + { + // remove mapping for old buffer + self.remove_mapping(key); + inner.key = None; + inner.dirty = false; + } + _ => {} + } + } + } + } + // // Section 2: Internal interface functions for lookup/update. // @@ -582,6 +628,10 @@ impl PageCache { let map = self.ephemeral_page_map.read().unwrap(); Some(*map.get(&(*file_id, *blkno))?) } + CacheKey::ImmutableFilePage { file_id, blkno } => { + let map = self.immutable_page_map.read().unwrap(); + Some(*map.get(&(*file_id, *blkno))?) + } } } @@ -605,6 +655,10 @@ impl PageCache { let map = self.ephemeral_page_map.read().unwrap(); Some(*map.get(&(*file_id, *blkno))?) } + CacheKey::ImmutableFilePage { file_id, blkno } => { + let map = self.immutable_page_map.read().unwrap(); + Some(*map.get(&(*file_id, *blkno))?) + } } } @@ -636,6 +690,11 @@ impl PageCache { map.remove(&(*file_id, *blkno)) .expect("could not find old key in mapping"); } + CacheKey::ImmutableFilePage { file_id, blkno } => { + let mut map = self.immutable_page_map.write().unwrap(); + map.remove(&(*file_id, *blkno)) + .expect("could not find old key in mapping"); + } } } @@ -676,6 +735,16 @@ impl PageCache { } } } + CacheKey::ImmutableFilePage { file_id, blkno } => { + let mut map = self.immutable_page_map.write().unwrap(); + match map.entry((*file_id, *blkno)) { + Entry::Occupied(entry) => Some(*entry.get()), + Entry::Vacant(entry) => { + entry.insert(slot_idx); + None + } + } + } } } @@ -687,16 +756,33 @@ impl PageCache { /// /// On return, the slot is empty and write-locked. fn find_victim(&self) -> (usize, RwLockWriteGuard) { - let iter_limit = self.slots.len() * 2; + let iter_limit = self.slots.len() * 10; let mut iters = 0; loop { + iters += 1; let slot_idx = self.next_evict_slot.fetch_add(1, Ordering::Relaxed) % self.slots.len(); let slot = &self.slots[slot_idx]; - if slot.dec_usage_count() == 0 || iters >= iter_limit { - let mut inner = slot.inner.write().unwrap(); - + if slot.dec_usage_count() == 0 { + let mut inner = match slot.inner.try_write() { + Ok(inner) => inner, + Err(TryLockError::Poisoned(err)) => { + panic!("buffer lock was poisoned: {:?}", err) + } + Err(TryLockError::WouldBlock) => { + // If we have looped through the whole buffer pool 10 times + // and still haven't found a victim buffer, something's wrong. + // Maybe all the buffers were in locked. That could happen in + // theory, if you have more threads holding buffers locked than + // there are buffers in the pool. In practice, with a reasonably + // large buffer pool it really shouldn't happen. + if iters > iter_limit { + panic!("could not find a victim buffer to evict"); + } + continue; + } + }; if let Some(old_key) = &inner.key { if inner.dirty { if let Err(err) = Self::writeback(old_key, inner.buf) { @@ -721,8 +807,6 @@ impl PageCache { } return (slot_idx, inner); } - - iters += 1; } } @@ -731,12 +815,20 @@ impl PageCache { CacheKey::MaterializedPage { hash_key: _, lsn: _, - } => { - panic!("unexpected dirty materialized page"); - } + } => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "unexpected dirty materialized page", + )), CacheKey::EphemeralPage { file_id, blkno } => { writeback_ephemeral_file(*file_id, *blkno, buf) } + CacheKey::ImmutableFilePage { + file_id: _, + blkno: _, + } => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "unexpected dirty immutable page", + )), } } @@ -767,6 +859,7 @@ impl PageCache { Self { materialized_page_map: Default::default(), ephemeral_page_map: Default::default(), + immutable_page_map: Default::default(), slots, next_evict_slot: AtomicUsize::new(0), } diff --git a/pageserver/src/page_service.rs b/pageserver/src/page_service.rs index 947a2d72ab..e7a4117b3e 100644 --- a/pageserver/src/page_service.rs +++ b/pageserver/src/page_service.rs @@ -33,7 +33,7 @@ use zenith_utils::zid::{ZTenantId, ZTimelineId}; use crate::basebackup; use crate::config::PageServerConf; use crate::pgdatadir_mapping::DatadirTimeline; -use crate::relish::*; +use crate::reltag::RelTag; use crate::repository::Repository; use crate::repository::Timeline; use crate::tenant_mgr; @@ -230,6 +230,7 @@ pub fn thread_main( None, None, "serving Page Service thread", + false, move || page_service_conn_main(conf, local_auth, socket, auth_type), ) { // Thread creation failed. Log the error and continue. @@ -324,8 +325,8 @@ impl PageServerHandler { let _enter = info_span!("pagestream", timeline = %timelineid, tenant = %tenantid).entered(); // Check that the timeline exists - let timeline = tenant_mgr::get_timeline_for_tenant(tenantid, timelineid) - .context("Cannot handle pagerequests for a remote timeline")?; + let timeline = tenant_mgr::get_timeline_for_tenant_load(tenantid, timelineid) + .context("Cannot load local timeline")?; /* switch client to COPYBOTH */ pgb.write_message(&BeMessage::CopyBothResponse)?; @@ -513,10 +514,11 @@ impl PageServerHandler { ) -> anyhow::Result<()> { let span = info_span!("basebackup", timeline = %timelineid, tenant = %tenantid, lsn = field::Empty); let _enter = span.enter(); + info!("starting"); // check that the timeline exists - let timeline = tenant_mgr::get_timeline_for_tenant(tenantid, timelineid) - .context("Cannot handle basebackup request for a remote timeline")?; + let timeline = tenant_mgr::get_timeline_for_tenant_load(tenantid, timelineid) + .context("Cannot load local timeline")?; let latest_gc_cutoff_lsn = timeline.tline.get_latest_gc_cutoff_lsn(); if let Some(lsn) = lsn { timeline @@ -535,7 +537,7 @@ impl PageServerHandler { basebackup.send_tarball()?; } pgb.write_message(&BeMessage::CopyDone)?; - debug!("CopyDone sent!"); + info!("done"); Ok(()) } @@ -569,7 +571,6 @@ impl postgres_backend::Handler for PageServerHandler { let data = self .auth .as_ref() - .as_ref() .unwrap() .decode(str::from_utf8(jwt_response)?)?; @@ -650,8 +651,8 @@ impl postgres_backend::Handler for PageServerHandler { info_span!("callmemaybe", timeline = %timelineid, tenant = %tenantid).entered(); // Check that the timeline exists - tenant_mgr::get_timeline_for_tenant(tenantid, timelineid) - .context("Failed to fetch local timeline for callmemaybe requests")?; + tenant_mgr::get_timeline_for_tenant_load(tenantid, timelineid) + .context("Cannot load local timeline")?; walreceiver::launch_wal_receiver(self.conf, tenantid, timelineid, &connstr)?; @@ -725,8 +726,8 @@ impl postgres_backend::Handler for PageServerHandler { let tenantid = ZTenantId::from_str(caps.get(1).unwrap().as_str())?; let timelineid = ZTimelineId::from_str(caps.get(2).unwrap().as_str())?; - let timeline = tenant_mgr::get_timeline_for_tenant(tenantid, timelineid) - .context("Failed to fetch local timeline for checkpoint request")?; + let timeline = tenant_mgr::get_timeline_for_tenant_load(tenantid, timelineid) + .context("Cannot load local timeline")?; timeline.tline.checkpoint(CheckpointConfig::Forced)?; diff --git a/pageserver/src/pgdatadir_mapping.rs b/pageserver/src/pgdatadir_mapping.rs index e4c8e8884c..0b9ea7c7a7 100644 --- a/pageserver/src/pgdatadir_mapping.rs +++ b/pageserver/src/pgdatadir_mapping.rs @@ -6,22 +6,21 @@ //! walingest.rs handles a few things like implicit relation creation and extension. //! Clarify that) //! -use crate::keyspace::{KeySpace, KeySpaceAccum, TARGET_FILE_SIZE_BYTES}; -use crate::relish::*; +use crate::keyspace::{KeyPartitioning, KeySpace, KeySpaceAccum}; +use crate::reltag::{RelTag, SlruKind}; use crate::repository::*; use crate::repository::{Repository, Timeline}; use crate::walrecord::ZenithWalRecord; -use anyhow::{bail, Result}; +use anyhow::{bail, ensure, Result}; use bytes::{Buf, Bytes}; use postgres_ffi::{pg_constants, Oid, TransactionId}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::ops::Range; use std::sync::atomic::{AtomicIsize, Ordering}; -use std::sync::{Arc, RwLockReadGuard}; +use std::sync::{Arc, Mutex, RwLockReadGuard}; use tracing::{debug, error, trace, warn}; use zenith_utils::bin_ser::BeSer; -use zenith_utils::lsn::AtomicLsn; use zenith_utils::lsn::Lsn; /// Block number within a relation or SLRU. This matches PostgreSQL's BlockNumber type. @@ -38,7 +37,7 @@ where pub tline: Arc, /// When did we last calculate the partitioning? - last_partitioning: AtomicLsn, + partitioning: Mutex<(KeyPartitioning, Lsn)>, /// Configuration: how often should the partitioning be recalculated. repartition_threshold: u64, @@ -51,7 +50,7 @@ impl DatadirTimeline { pub fn new(tline: Arc, repartition_threshold: u64) -> Self { DatadirTimeline { tline, - last_partitioning: AtomicLsn::new(0), + partitioning: Mutex::new((KeyPartitioning::new(), Lsn(0))), current_logical_size: AtomicIsize::new(0), repartition_threshold, } @@ -69,23 +68,25 @@ impl DatadirTimeline { Ok(()) } - /// Start updating a WAL record + /// Start ingesting a WAL record, or other atomic modification of + /// the timeline. /// /// This provides a transaction-like interface to perform a bunch - /// of modifications atomically, with one LSN. + /// of modifications atomically, all stamped with one LSN. /// - /// To ingest a WAL record, call begin_record(lsn) to get a writer - /// object. Use the functions in the writer-object to modify the - /// repository state, updating all the pages and metadata that the - /// WAL record affects. When you're done, call writer.finish() to + /// To ingest a WAL record, call begin_modification(lsn) to get a + /// DatadirModification object. Use the functions in the object to + /// modify the repository state, updating all the pages and metadata + /// that the WAL record affects. When you're done, call commit() to /// commit the changes. /// - /// Note that any pending modifications you make through the writer - /// won't be visible to calls to the get functions until you finish! - /// If you update the same page twice, the last update wins. + /// Note that any pending modifications you make through the + /// modification object won't be visible to calls to the 'get' and list + /// functions of the timeline until you finish! And if you update the + /// same page twice, the last update wins. /// - pub fn begin_record(&self, lsn: Lsn) -> DatadirTimelineWriter { - DatadirTimelineWriter { + pub fn begin_modification(&self, lsn: Lsn) -> DatadirModification { + DatadirModification { tline: self, lsn, pending_updates: HashMap::new(), @@ -100,6 +101,8 @@ impl DatadirTimeline { /// Look up given page version. pub fn get_rel_page_at_lsn(&self, tag: RelTag, blknum: BlockNumber, lsn: Lsn) -> Result { + ensure!(tag.relnode != 0, "invalid relnode"); + let nblocks = self.get_rel_size(tag, lsn)?; if blknum >= nblocks { debug!( @@ -115,14 +118,16 @@ impl DatadirTimeline { /// Get size of a relation file pub fn get_rel_size(&self, tag: RelTag, lsn: Lsn) -> Result { + ensure!(tag.relnode != 0, "invalid relnode"); + if (tag.forknum == pg_constants::FSM_FORKNUM || tag.forknum == pg_constants::VISIBILITYMAP_FORKNUM) && !self.get_rel_exists(tag, lsn)? { - // FIXME: Postgres sometimes calls calls smgrcreate() to - // create FSM, and smgrnblocks() on it immediately - // afterwards, without extending it. Tolerate that by - // claiming that any non-existent FSM fork has size 0. + // FIXME: Postgres sometimes calls smgrcreate() to create + // FSM, and smgrnblocks() on it immediately afterwards, + // without extending it. Tolerate that by claiming that + // any non-existent FSM fork has size 0. return Ok(0); } @@ -133,6 +138,8 @@ impl DatadirTimeline { /// Does relation exist? pub fn get_rel_exists(&self, tag: RelTag, lsn: Lsn) -> Result { + ensure!(tag.relnode != 0, "invalid relnode"); + // fetch directory listing let key = rel_dir_to_key(tag.spcnode, tag.dbnode); let buf = self.tline.get(key, lsn)?; @@ -380,14 +387,30 @@ impl DatadirTimeline { Ok(result.to_keyspace()) } + + pub fn repartition(&self, lsn: Lsn, partition_size: u64) -> Result<(KeyPartitioning, Lsn)> { + let mut partitioning_guard = self.partitioning.lock().unwrap(); + if partitioning_guard.1 == Lsn(0) + || lsn.0 - partitioning_guard.1 .0 > self.repartition_threshold + { + let keyspace = self.collect_keyspace(lsn)?; + let partitioning = keyspace.partition(partition_size); + *partitioning_guard = (partitioning, lsn); + return Ok((partitioning_guard.0.clone(), lsn)); + } + Ok((partitioning_guard.0.clone(), partitioning_guard.1)) + } } -/// DatadirTimelineWriter represents an operation to ingest an atomic set of +/// DatadirModification represents an operation to ingest an atomic set of /// updates to the repository. It is created by the 'begin_record' /// function. It is called for each WAL record, so that all the modifications -/// by a one WAL record appear atomic -pub struct DatadirTimelineWriter<'a, R: Repository> { - tline: &'a DatadirTimeline, +/// by a one WAL record appear atomic. +pub struct DatadirModification<'a, R: Repository> { + /// The timeline this modification applies to. You can access this to + /// read the state, but note that any pending updates are *not* reflected + /// in the state in 'tline' yet. + pub tline: &'a DatadirTimeline, lsn: Lsn, @@ -399,19 +422,7 @@ pub struct DatadirTimelineWriter<'a, R: Repository> { pending_nblocks: isize, } -// TODO Currently, Deref is used to allow easy access to read methods from this trait. -// This is probably considered a bad practice in Rust and should be fixed eventually, -// but will cause large code changes. -impl<'a, R: Repository> std::ops::Deref for DatadirTimelineWriter<'a, R> { - type Target = DatadirTimeline; - - fn deref(&self) -> &Self::Target { - self.tline - } -} - -/// Various functions to mutate the repository state. -impl<'a, R: Repository> DatadirTimelineWriter<'a, R> { +impl<'a, R: Repository> DatadirModification<'a, R> { /// Initialize a completely new repository. /// /// This inserts the directory metadata entries that are assumed to @@ -450,6 +461,7 @@ impl<'a, R: Repository> DatadirTimelineWriter<'a, R> { blknum: BlockNumber, rec: ZenithWalRecord, ) -> Result<()> { + ensure!(rel.relnode != 0, "invalid relnode"); self.put(rel_block_to_key(rel, blknum), Value::WalRecord(rec)); Ok(()) } @@ -476,6 +488,7 @@ impl<'a, R: Repository> DatadirTimelineWriter<'a, R> { blknum: BlockNumber, img: Bytes, ) -> Result<()> { + ensure!(rel.relnode != 0, "invalid relnode"); self.put(rel_block_to_key(rel, blknum), Value::Image(img)); Ok(()) } @@ -491,6 +504,7 @@ impl<'a, R: Repository> DatadirTimelineWriter<'a, R> { Ok(()) } + /// Store a relmapper file (pg_filenode.map) in the repository pub fn put_relmap_file(&mut self, spcnode: Oid, dbnode: Oid, img: Bytes) -> Result<()> { // Add it to the directory (if it doesn't exist already) let buf = self.get(DBDIR_KEY)?; @@ -566,22 +580,11 @@ impl<'a, R: Repository> DatadirTimelineWriter<'a, R> { Ok(()) } - // When a new relation is created: - // - create/update the directory entry to remember that it exists - // - create relish header to indicate the size (0) - - // When a relation is extended: - // - update relish header with new size - // - insert the block - - // when a relation is truncated: - // - delete truncated blocks - // - update relish header with size - /// Create a relation fork. /// /// 'nblocks' is the initial size. pub fn put_rel_creation(&mut self, rel: RelTag, nblocks: BlockNumber) -> Result<()> { + ensure!(rel.relnode != 0, "invalid relnode"); // It's possible that this is the first rel for this db in this // tablespace. Create the reldir entry for it if so. let mut dbdir = DbDirectory::des(&self.get(DBDIR_KEY)?)?; @@ -623,6 +626,7 @@ impl<'a, R: Repository> DatadirTimelineWriter<'a, R> { /// Truncate relation pub fn put_rel_truncation(&mut self, rel: RelTag, nblocks: BlockNumber) -> Result<()> { + ensure!(rel.relnode != 0, "invalid relnode"); let size_key = rel_size_to_key(rel); // Fetch the old size first @@ -639,6 +643,8 @@ impl<'a, R: Repository> DatadirTimelineWriter<'a, R> { /// Extend relation pub fn put_rel_extend(&mut self, rel: RelTag, nblocks: BlockNumber) -> Result<()> { + ensure!(rel.relnode != 0, "invalid relnode"); + // Put size let size_key = rel_size_to_key(rel); let old_size = self.get(size_key)?.get_u32_le(); @@ -652,6 +658,8 @@ impl<'a, R: Repository> DatadirTimelineWriter<'a, R> { /// Drop a relation. pub fn put_rel_drop(&mut self, rel: RelTag) -> Result<()> { + ensure!(rel.relnode != 0, "invalid relnode"); + // Remove it from the directory entry let dir_key = rel_dir_to_key(rel.spcnode, rel.dbnode); let buf = self.get(dir_key)?; @@ -738,7 +746,7 @@ impl<'a, R: Repository> DatadirTimelineWriter<'a, R> { Ok(()) } - /// This method is used for marking dropped relations and truncated SLRU files and aborted two phase records + /// Drop a relmapper file (pg_filenode.map) pub fn drop_relmap_file(&mut self, _spcnode: Oid, _dbnode: Oid) -> Result<()> { // TODO Ok(()) @@ -764,10 +772,13 @@ impl<'a, R: Repository> DatadirTimelineWriter<'a, R> { Ok(()) } - pub fn finish(self) -> Result<()> { + /// + /// Finish this atomic update, writing all the updated keys to the + /// underlying timeline. + /// + pub fn commit(self) -> Result<()> { let writer = self.tline.tline.writer(); - let last_partitioning = self.last_partitioning.load(); let pending_nblocks = self.pending_nblocks; for (key, value) in self.pending_updates { @@ -779,15 +790,6 @@ impl<'a, R: Repository> DatadirTimelineWriter<'a, R> { writer.finish_write(self.lsn); - if last_partitioning == Lsn(0) - || self.lsn.0 - last_partitioning.0 > self.tline.repartition_threshold - { - let keyspace = self.tline.collect_keyspace(self.lsn)?; - let partitioning = keyspace.partition(TARGET_FILE_SIZE_BYTES); - self.tline.tline.hint_partitioning(partitioning, self.lsn)?; - self.tline.last_partitioning.store(self.lsn); - } - if pending_nblocks != 0 { self.tline.current_logical_size.fetch_add( pending_nblocks * pg_constants::BLCKSZ as isize, @@ -915,7 +917,7 @@ static ZERO_PAGE: Bytes = Bytes::from_static(&[0u8; pg_constants::BLCKSZ as usiz // 00 SPCNODE DBNODE 00000000 00 00000000 // // RelDir: -// 00 SPCNODE DBNODE 00000000 00 00000001 +// 00 SPCNODE DBNODE 00000000 00 00000001 (Postgres never uses relfilenode 0) // // RelBlock: // 00 SPCNODE DBNODE RELNODE FORK BLKNUM @@ -1213,11 +1215,10 @@ pub fn create_test_timeline( timeline_id: zenith_utils::zid::ZTimelineId, ) -> Result>> { let tline = repo.create_empty_timeline(timeline_id, Lsn(8))?; - let tline = DatadirTimeline::new(tline, crate::layered_repository::tests::TEST_FILE_SIZE / 10); - let mut writer = tline.begin_record(Lsn(8)); - writer.init_empty()?; - - writer.finish()?; + let tline = DatadirTimeline::new(tline, 256 * 1024); + let mut m = tline.begin_modification(Lsn(8)); + m.init_empty()?; + m.commit()?; Ok(Arc::new(tline)) } diff --git a/pageserver/src/relish.rs b/pageserver/src/reltag.rs similarity index 97% rename from pageserver/src/relish.rs rename to pageserver/src/reltag.rs index 46ff468f2f..18e26cc37a 100644 --- a/pageserver/src/relish.rs +++ b/pageserver/src/reltag.rs @@ -39,9 +39,7 @@ impl PartialOrd for RelTag { impl Ord for RelTag { fn cmp(&self, other: &Self) -> Ordering { - let mut cmp; - - cmp = self.spcnode.cmp(&other.spcnode); + let mut cmp = self.spcnode.cmp(&other.spcnode); if cmp != Ordering::Equal { return cmp; } diff --git a/pageserver/src/remote_storage.rs b/pageserver/src/remote_storage.rs index 4af1f8ed56..aebd74af5a 100644 --- a/pageserver/src/remote_storage.rs +++ b/pageserver/src/remote_storage.rs @@ -5,7 +5,7 @@ //! There are a few components the storage machinery consists of: //! * [`RemoteStorage`] trait a CRUD-like generic abstraction to use for adapting external storages with a few implementations: //! * [`local_fs`] allows to use local file system as an external storage -//! * [`rust_s3`] uses AWS S3 bucket as an external storage +//! * [`s3_bucket`] uses AWS S3 bucket as an external storage //! //! * synchronization logic at [`storage_sync`] module that keeps pageserver state (both runtime one and the workdir files) and storage state in sync. //! Synchronization internals are split into submodules @@ -82,7 +82,7 @@ //! The sync queue processing also happens in batches, so the sync tasks can wait in the queue for some time. mod local_fs; -mod rust_s3; +mod s3_bucket; mod storage_sync; use std::{ @@ -93,28 +93,34 @@ use std::{ use anyhow::{bail, Context}; use tokio::io; -use tracing::{error, info}; +use tracing::{debug, error, info}; use zenith_utils::zid::{ZTenantId, ZTenantTimelineId, ZTimelineId}; +pub use self::storage_sync::index::{RemoteIndex, TimelineIndexEntry}; pub use self::storage_sync::{schedule_timeline_checkpoint_upload, schedule_timeline_download}; -use self::{local_fs::LocalFs, rust_s3::S3}; +use self::{local_fs::LocalFs, s3_bucket::S3Bucket}; +use crate::layered_repository::ephemeral_file::is_ephemeral_file; use crate::{ config::{PageServerConf, RemoteStorageKind}, layered_repository::metadata::{TimelineMetadata, METADATA_FILE_NAME}, - repository::TimelineSyncState, }; pub use storage_sync::compression; +#[derive(Clone, Copy, Debug)] +pub enum LocalTimelineInitStatus { + LocallyComplete, + NeedsSync, +} + +type LocalTimelineInitStatuses = HashMap>; + /// A structure to combine all synchronization data to share with pageserver after a successful sync loop initialization. /// Successful initialization includes a case when sync loop is not started, in which case the startup data is returned still, /// to simplify the received code. pub struct SyncStartupData { - /// A sync state, derived from initial comparison of local timeline files and the remote archives, - /// before any sync tasks are executed. - /// To reuse the local file scan logic, the timeline states are returned even if no sync loop get started during init: - /// in this case, no remote files exist and all local timelines with correct metadata files are considered ready. - pub initial_timeline_states: HashMap>, + pub remote_index: RemoteIndex, + pub local_timeline_init_statuses: LocalTimelineInitStatuses, } /// Based on the config, initiates the remote storage connection and starts a separate thread @@ -145,7 +151,7 @@ pub fn start_local_timeline_sync( storage_sync::spawn_storage_sync_thread( config, local_timeline_files, - S3::new(s3_config, &config.workdir)?, + S3Bucket::new(s3_config, &config.workdir)?, storage_config.max_concurrent_sync, storage_config.max_sync_errors, ) @@ -154,23 +160,18 @@ pub fn start_local_timeline_sync( .context("Failed to spawn the storage sync thread"), None => { info!("No remote storage configured, skipping storage sync, considering all local timelines with correct metadata files enabled"); - let mut initial_timeline_states: HashMap< - ZTenantId, - HashMap, - > = HashMap::new(); - for (ZTenantTimelineId{tenant_id, timeline_id}, (timeline_metadata, _)) in + let mut local_timeline_init_statuses = LocalTimelineInitStatuses::new(); + for (ZTenantTimelineId { tenant_id, timeline_id }, _) in local_timeline_files { - initial_timeline_states + local_timeline_init_statuses .entry(tenant_id) .or_default() - .insert( - timeline_id, - TimelineSyncState::Ready(timeline_metadata.disk_consistent_lsn()), - ); + .insert(timeline_id, LocalTimelineInitStatus::LocallyComplete); } Ok(SyncStartupData { - initial_timeline_states, + local_timeline_init_statuses, + remote_index: RemoteIndex::empty(), }) } } @@ -260,6 +261,8 @@ fn collect_timelines_for_tenant( Ok(timelines) } +// discover timeline files and extract timeline metadata +// NOTE: ephemeral files are excluded from the list fn collect_timeline_files( timeline_dir: &Path, ) -> anyhow::Result<(ZTimelineId, TimelineMetadata, Vec)> { @@ -279,6 +282,9 @@ fn collect_timeline_files( if entry_path.is_file() { if entry_path.file_name().and_then(ffi::OsStr::to_str) == Some(METADATA_FILE_NAME) { timeline_metadata_path = Some(entry_path); + } else if is_ephemeral_file(&entry_path.file_name().unwrap().to_string_lossy()) { + debug!("skipping ephemeral file {}", entry_path.display()); + continue; } else { timeline_files.push(entry_path); } @@ -319,27 +325,35 @@ trait RemoteStorage: Send + Sync { &self, from: impl io::AsyncRead + Unpin + Send + Sync + 'static, to: &Self::StoragePath, + metadata: Option, ) -> anyhow::Result<()>; /// Streams the remote storage entry contents into the buffered writer given, returns the filled writer. + /// Returns the metadata, if any was stored with the file previously. async fn download( &self, from: &Self::StoragePath, to: &mut (impl io::AsyncWrite + Unpin + Send + Sync), - ) -> anyhow::Result<()>; + ) -> anyhow::Result>; /// Streams a given byte range of the remote storage entry contents into the buffered writer given, returns the filled writer. + /// Returns the metadata, if any was stored with the file previously. async fn download_range( &self, from: &Self::StoragePath, start_inclusive: u64, end_exclusive: Option, to: &mut (impl io::AsyncWrite + Unpin + Send + Sync), - ) -> anyhow::Result<()>; + ) -> anyhow::Result>; async fn delete(&self, path: &Self::StoragePath) -> anyhow::Result<()>; } +/// Extra set of key-value pairs that contain arbitrary metadata about the storage entry. +/// Immutable, cannot be changed once the file is created. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StorageMetadata(HashMap); + fn strip_path_prefix<'a>(prefix: &'a Path, path: &'a Path) -> anyhow::Result<&'a Path> { if prefix == path { anyhow::bail!( diff --git a/pageserver/src/remote_storage/README.md b/pageserver/src/remote_storage/README.md index 3c77275da8..43a47e09d8 100644 --- a/pageserver/src/remote_storage/README.md +++ b/pageserver/src/remote_storage/README.md @@ -17,7 +17,7 @@ This way, the backups are managed in background, not affecting directly other pa Current implementation * provides remote storage wrappers for AWS S3 and local FS * synchronizes the differences with local timelines and remote states as fast as possible -* uploads new relishes, frozen by pageserver checkpoint thread +* uploads new layer files * downloads and registers timelines, found on the remote storage, but missing locally, if those are requested somehow via pageserver (e.g. http api, gc) * uses compression when deals with files, for better S3 usage * maintains an index of what's stored remotely @@ -46,18 +46,6 @@ This could be avoided by a background thread/future storing the serialized index No file checksum assertion is done currently, but should be (AWS S3 returns file checksums during the `list` operation) -* sad rust-s3 api - -rust-s3 is not very pleasant to use: -1. it returns `anyhow::Result` and it's hard to distinguish "missing file" cases from "no connection" one, for instance -2. at least one function it its API that we need (`get_object_stream`) has `async` keyword and blocks (!), see details [here](https://github.com/zenithdb/zenith/pull/752#discussion_r728373091) -3. it's a prerelease library with unclear maintenance status -4. noisy on debug level - -But it's already used in the project, so for now it's reused to avoid bloating the dependency tree. -Based on previous evaluation, even `rusoto-s3` could be a better choice over this library, but needs further benchmarking. - - * gc is ignored So far, we don't adjust the remote storage based on GC thread loop results, only checkpointer loop affects the remote storage. diff --git a/pageserver/src/remote_storage/local_fs.rs b/pageserver/src/remote_storage/local_fs.rs index b6ebd004b5..b40089d53c 100644 --- a/pageserver/src/remote_storage/local_fs.rs +++ b/pageserver/src/remote_storage/local_fs.rs @@ -17,7 +17,7 @@ use tokio::{ }; use tracing::*; -use super::{strip_path_prefix, RemoteStorage}; +use super::{strip_path_prefix, RemoteStorage, StorageMetadata}; pub struct LocalFs { pageserver_workdir: &'static Path, @@ -53,6 +53,32 @@ impl LocalFs { ) } } + + async fn read_storage_metadata( + &self, + file_path: &Path, + ) -> anyhow::Result> { + let metadata_path = storage_metadata_path(file_path); + if metadata_path.exists() && metadata_path.is_file() { + let metadata_string = fs::read_to_string(&metadata_path).await.with_context(|| { + format!( + "Failed to read metadata from the local storage at '{}'", + metadata_path.display() + ) + })?; + + serde_json::from_str(&metadata_string) + .with_context(|| { + format!( + "Failed to deserialize metadata from the local storage at '{}'", + metadata_path.display() + ) + }) + .map(|metadata| Some(StorageMetadata(metadata))) + } else { + Ok(None) + } + } } #[async_trait::async_trait] @@ -80,14 +106,19 @@ impl RemoteStorage for LocalFs { &self, mut from: impl io::AsyncRead + Unpin + Send + Sync + 'static, to: &Self::StoragePath, + metadata: Option, ) -> anyhow::Result<()> { let target_file_path = self.resolve_in_storage(to)?; create_target_directory(&target_file_path).await?; + // We need this dance with sort of durable rename (without fsyncs) + // to prevent partial uploads. This was really hit when pageserver shutdown + // cancelled the upload and partial file was left on the fs + let temp_file_path = path_with_suffix_extension(&target_file_path, ".temp"); let mut destination = io::BufWriter::new( fs::OpenOptions::new() .write(true) .create(true) - .open(&target_file_path) + .open(&temp_file_path) .await .with_context(|| { format!( @@ -101,16 +132,43 @@ impl RemoteStorage for LocalFs { .await .with_context(|| { format!( - "Failed to upload file to the local storage at '{}'", + "Failed to upload file (write temp) to the local storage at '{}'", + temp_file_path.display() + ) + })?; + + destination.flush().await.with_context(|| { + format!( + "Failed to upload (flush temp) file to the local storage at '{}'", + temp_file_path.display() + ) + })?; + + fs::rename(temp_file_path, &target_file_path) + .await + .with_context(|| { + format!( + "Failed to upload (rename) file to the local storage at '{}'", target_file_path.display() ) })?; - destination.flush().await.with_context(|| { - format!( - "Failed to upload file to the local storage at '{}'", - target_file_path.display() + + if let Some(storage_metadata) = metadata { + let storage_metadata_path = storage_metadata_path(&target_file_path); + fs::write( + &storage_metadata_path, + serde_json::to_string(&storage_metadata.0) + .context("Failed to serialize storage metadata as json")?, ) - })?; + .await + .with_context(|| { + format!( + "Failed to write metadata to the local storage at '{}'", + storage_metadata_path.display() + ) + })?; + } + Ok(()) } @@ -118,7 +176,7 @@ impl RemoteStorage for LocalFs { &self, from: &Self::StoragePath, to: &mut (impl io::AsyncWrite + Unpin + Send + Sync), - ) -> anyhow::Result<()> { + ) -> anyhow::Result> { let file_path = self.resolve_in_storage(from)?; if file_path.exists() && file_path.is_file() { @@ -141,7 +199,8 @@ impl RemoteStorage for LocalFs { ) })?; source.flush().await?; - Ok(()) + + self.read_storage_metadata(&file_path).await } else { bail!( "File '{}' either does not exist or is not a file", @@ -156,7 +215,7 @@ impl RemoteStorage for LocalFs { start_inclusive: u64, end_exclusive: Option, to: &mut (impl io::AsyncWrite + Unpin + Send + Sync), - ) -> anyhow::Result<()> { + ) -> anyhow::Result> { if let Some(end_exclusive) = end_exclusive { ensure!( end_exclusive > start_inclusive, @@ -165,7 +224,7 @@ impl RemoteStorage for LocalFs { end_exclusive ); if start_inclusive == end_exclusive.saturating_sub(1) { - return Ok(()); + return Ok(None); } } let file_path = self.resolve_in_storage(from)?; @@ -199,7 +258,8 @@ impl RemoteStorage for LocalFs { file_path.display() ) })?; - Ok(()) + + self.read_storage_metadata(&file_path).await } else { bail!( "File '{}' either does not exist or is not a file", @@ -221,6 +281,17 @@ impl RemoteStorage for LocalFs { } } +fn path_with_suffix_extension(original_path: &Path, suffix: &str) -> PathBuf { + let mut extension_with_suffix = original_path.extension().unwrap_or_default().to_os_string(); + extension_with_suffix.push(suffix); + + original_path.with_extension(extension_with_suffix) +} + +fn storage_metadata_path(original_path: &Path) -> PathBuf { + path_with_suffix_extension(original_path, ".metadata") +} + fn get_all_files<'a, P>( directory_path: P, ) -> Pin>> + Send + Sync + 'a>> @@ -430,7 +501,7 @@ mod fs_tests { use super::*; use crate::repository::repo_harness::{RepoHarness, TIMELINE_ID}; - use std::io::Write; + use std::{collections::HashMap, io::Write}; use tempfile::tempdir; #[tokio::test] @@ -444,7 +515,7 @@ mod fs_tests { ) .await?; let target_path = PathBuf::from("/").join("somewhere").join("else"); - match storage.upload(source, &target_path).await { + match storage.upload(source, &target_path, None).await { Ok(()) => panic!("Should not allow storing files with wrong target path"), Err(e) => { let message = format!("{:?}", e); @@ -454,14 +525,14 @@ mod fs_tests { } assert!(storage.list().await?.is_empty()); - let target_path_1 = upload_dummy_file(&repo_harness, &storage, "upload_1").await?; + let target_path_1 = upload_dummy_file(&repo_harness, &storage, "upload_1", None).await?; assert_eq!( storage.list().await?, vec![target_path_1.clone()], "Should list a single file after first upload" ); - let target_path_2 = upload_dummy_file(&repo_harness, &storage, "upload_2").await?; + let target_path_2 = upload_dummy_file(&repo_harness, &storage, "upload_2", None).await?; assert_eq!( list_files_sorted(&storage).await?, vec![target_path_1.clone(), target_path_2.clone()], @@ -482,12 +553,16 @@ mod fs_tests { let repo_harness = RepoHarness::create("download_file")?; let storage = create_storage()?; let upload_name = "upload_1"; - let upload_target = upload_dummy_file(&repo_harness, &storage, upload_name).await?; + let upload_target = upload_dummy_file(&repo_harness, &storage, upload_name, None).await?; let mut content_bytes = io::BufWriter::new(std::io::Cursor::new(Vec::new())); - storage.download(&upload_target, &mut content_bytes).await?; - content_bytes.flush().await?; + let metadata = storage.download(&upload_target, &mut content_bytes).await?; + assert!( + metadata.is_none(), + "No metadata should be returned for no metadata upload" + ); + content_bytes.flush().await?; let contents = String::from_utf8(content_bytes.into_inner().into_inner())?; assert_eq!( dummy_contents(upload_name), @@ -512,12 +587,16 @@ mod fs_tests { let repo_harness = RepoHarness::create("download_file_range_positive")?; let storage = create_storage()?; let upload_name = "upload_1"; - let upload_target = upload_dummy_file(&repo_harness, &storage, upload_name).await?; + let upload_target = upload_dummy_file(&repo_harness, &storage, upload_name, None).await?; let mut full_range_bytes = io::BufWriter::new(std::io::Cursor::new(Vec::new())); - storage + let metadata = storage .download_range(&upload_target, 0, None, &mut full_range_bytes) .await?; + assert!( + metadata.is_none(), + "No metadata should be returned for no metadata upload" + ); full_range_bytes.flush().await?; assert_eq!( dummy_contents(upload_name), @@ -527,7 +606,7 @@ mod fs_tests { let mut zero_range_bytes = io::BufWriter::new(std::io::Cursor::new(Vec::new())); let same_byte = 1_000_000_000; - storage + let metadata = storage .download_range( &upload_target, same_byte, @@ -535,6 +614,10 @@ mod fs_tests { &mut zero_range_bytes, ) .await?; + assert!( + metadata.is_none(), + "No metadata should be returned for no metadata upload" + ); zero_range_bytes.flush().await?; assert!( zero_range_bytes.into_inner().into_inner().is_empty(), @@ -545,7 +628,7 @@ mod fs_tests { let (first_part_local, second_part_local) = uploaded_bytes.split_at(3); let mut first_part_remote = io::BufWriter::new(std::io::Cursor::new(Vec::new())); - storage + let metadata = storage .download_range( &upload_target, 0, @@ -553,6 +636,11 @@ mod fs_tests { &mut first_part_remote, ) .await?; + assert!( + metadata.is_none(), + "No metadata should be returned for no metadata upload" + ); + first_part_remote.flush().await?; let first_part_remote = first_part_remote.into_inner().into_inner(); assert_eq!( @@ -562,7 +650,7 @@ mod fs_tests { ); let mut second_part_remote = io::BufWriter::new(std::io::Cursor::new(Vec::new())); - storage + let metadata = storage .download_range( &upload_target, first_part_local.len() as u64, @@ -570,6 +658,11 @@ mod fs_tests { &mut second_part_remote, ) .await?; + assert!( + metadata.is_none(), + "No metadata should be returned for no metadata upload" + ); + second_part_remote.flush().await?; let second_part_remote = second_part_remote.into_inner().into_inner(); assert_eq!( @@ -586,7 +679,7 @@ mod fs_tests { let repo_harness = RepoHarness::create("download_file_range_negative")?; let storage = create_storage()?; let upload_name = "upload_1"; - let upload_target = upload_dummy_file(&repo_harness, &storage, upload_name).await?; + let upload_target = upload_dummy_file(&repo_harness, &storage, upload_name, None).await?; let start = 10000; let end = 234; @@ -624,7 +717,7 @@ mod fs_tests { let repo_harness = RepoHarness::create("delete_file")?; let storage = create_storage()?; let upload_name = "upload_1"; - let upload_target = upload_dummy_file(&repo_harness, &storage, upload_name).await?; + let upload_target = upload_dummy_file(&repo_harness, &storage, upload_name, None).await?; storage.delete(&upload_target).await?; assert!(storage.list().await?.is_empty()); @@ -640,10 +733,69 @@ mod fs_tests { Ok(()) } + #[tokio::test] + async fn file_with_metadata() -> anyhow::Result<()> { + let repo_harness = RepoHarness::create("download_file")?; + let storage = create_storage()?; + let upload_name = "upload_1"; + let metadata = StorageMetadata(HashMap::from([ + ("one".to_string(), "1".to_string()), + ("two".to_string(), "2".to_string()), + ])); + let upload_target = + upload_dummy_file(&repo_harness, &storage, upload_name, Some(metadata.clone())).await?; + + let mut content_bytes = io::BufWriter::new(std::io::Cursor::new(Vec::new())); + let full_download_metadata = storage.download(&upload_target, &mut content_bytes).await?; + + content_bytes.flush().await?; + let contents = String::from_utf8(content_bytes.into_inner().into_inner())?; + assert_eq!( + dummy_contents(upload_name), + contents, + "We should upload and download the same contents" + ); + + assert_eq!( + full_download_metadata.as_ref(), + Some(&metadata), + "We should get the same metadata back for full download" + ); + + let uploaded_bytes = dummy_contents(upload_name).into_bytes(); + let (first_part_local, _) = uploaded_bytes.split_at(3); + + let mut first_part_remote = io::BufWriter::new(std::io::Cursor::new(Vec::new())); + let partial_download_metadata = storage + .download_range( + &upload_target, + 0, + Some(first_part_local.len() as u64), + &mut first_part_remote, + ) + .await?; + first_part_remote.flush().await?; + let first_part_remote = first_part_remote.into_inner().into_inner(); + assert_eq!( + first_part_local, + first_part_remote.as_slice(), + "First part bytes should be returned when requested" + ); + + assert_eq!( + partial_download_metadata.as_ref(), + Some(&metadata), + "We should get the same metadata back for partial download" + ); + + Ok(()) + } + async fn upload_dummy_file( harness: &RepoHarness<'_>, storage: &LocalFs, name: &str, + metadata: Option, ) -> anyhow::Result { let timeline_path = harness.timeline_path(&TIMELINE_ID); let relative_timeline_path = timeline_path.strip_prefix(&harness.conf.workdir)?; @@ -656,6 +808,7 @@ mod fs_tests { ) .await?, &storage_path, + metadata, ) .await?; Ok(storage_path) diff --git a/pageserver/src/remote_storage/rust_s3.rs b/pageserver/src/remote_storage/s3_bucket.rs similarity index 66% rename from pageserver/src/remote_storage/rust_s3.rs rename to pageserver/src/remote_storage/s3_bucket.rs index 527bdf48ff..bfd28168f4 100644 --- a/pageserver/src/remote_storage/rust_s3.rs +++ b/pageserver/src/remote_storage/s3_bucket.rs @@ -1,4 +1,4 @@ -//! AWS S3 storage wrapper around `rust_s3` library. +//! AWS S3 storage wrapper around `rusoto` library. //! //! Respects `prefix_in_bucket` property from [`S3Config`], //! allowing multiple pageservers to independently work with the same S3 bucket, if @@ -7,15 +7,25 @@ use std::path::{Path, PathBuf}; use anyhow::Context; -use s3::{bucket::Bucket, creds::Credentials, region::Region}; -use tokio::io::{self, AsyncWriteExt}; -use tracing::debug; +use rusoto_core::{ + credential::{InstanceMetadataProvider, StaticProvider}, + HttpClient, Region, +}; +use rusoto_s3::{ + DeleteObjectRequest, GetObjectRequest, ListObjectsV2Request, PutObjectRequest, S3Client, + StreamingBody, S3, +}; +use tokio::io; +use tokio_util::io::ReaderStream; +use tracing::{debug, trace}; use crate::{ config::S3Config, remote_storage::{strip_path_prefix, RemoteStorage}, }; +use super::StorageMetadata; + const S3_FILE_SEPARATOR: char = '/'; #[derive(Debug, Eq, PartialEq)] @@ -50,38 +60,50 @@ impl S3ObjectKey { } /// AWS S3 storage. -pub struct S3 { +pub struct S3Bucket { pageserver_workdir: &'static Path, - bucket: Bucket, + client: S3Client, + bucket_name: String, prefix_in_bucket: Option, } -impl S3 { - /// Creates the storage, errors if incorrect AWS S3 configuration provided. +impl S3Bucket { + /// Creates the S3 storage, errors if incorrect AWS S3 configuration provided. pub fn new(aws_config: &S3Config, pageserver_workdir: &'static Path) -> anyhow::Result { + // TODO kb check this + // Keeping a single client may cause issues due to timeouts. + // https://github.com/rusoto/rusoto/issues/1686 + debug!( - "Creating s3 remote storage around bucket {}", + "Creating s3 remote storage for S3 bucket {}", aws_config.bucket_name ); let region = match aws_config.endpoint.clone() { - Some(endpoint) => Region::Custom { - endpoint, - region: aws_config.bucket_region.clone(), + Some(custom_endpoint) => Region::Custom { + name: aws_config.bucket_region.clone(), + endpoint: custom_endpoint, }, None => aws_config .bucket_region .parse::() .context("Failed to parse the s3 region from config")?, }; - - let credentials = Credentials::new( - aws_config.access_key_id.as_deref(), - aws_config.secret_access_key.as_deref(), - None, - None, - None, - ) - .context("Failed to create the s3 credentials")?; + let request_dispatcher = HttpClient::new().context("Failed to create S3 http client")?; + let client = if aws_config.access_key_id.is_none() && aws_config.secret_access_key.is_none() + { + trace!("Using IAM-based AWS access"); + S3Client::new_with(request_dispatcher, InstanceMetadataProvider::new(), region) + } else { + trace!("Using credentials-based AWS access"); + S3Client::new_with( + request_dispatcher, + StaticProvider::new_minimal( + aws_config.access_key_id.clone().unwrap_or_default(), + aws_config.secret_access_key.clone().unwrap_or_default(), + ), + region, + ) + }; let prefix_in_bucket = aws_config.prefix_in_bucket.as_deref().map(|prefix| { let mut prefix = prefix; @@ -97,20 +119,16 @@ impl S3 { }); Ok(Self { - bucket: Bucket::new_with_path_style( - aws_config.bucket_name.as_str(), - region, - credentials, - ) - .context("Failed to create the s3 bucket")?, + client, pageserver_workdir, + bucket_name: aws_config.bucket_name.clone(), prefix_in_bucket, }) } } #[async_trait::async_trait] -impl RemoteStorage for S3 { +impl RemoteStorage for S3Bucket { type StoragePath = S3ObjectKey; fn storage_path(&self, local_path: &Path) -> anyhow::Result { @@ -129,74 +147,74 @@ impl RemoteStorage for S3 { } async fn list(&self) -> anyhow::Result> { - let list_response = self - .bucket - .list(self.prefix_in_bucket.clone().unwrap_or_default(), None) - .await - .context("Failed to list s3 objects")?; + let mut document_keys = Vec::new(); - Ok(list_response - .into_iter() - .flat_map(|response| response.contents) - .map(|s3_object| S3ObjectKey(s3_object.key)) - .collect()) + let mut continuation_token = None; + loop { + let fetch_response = self + .client + .list_objects_v2(ListObjectsV2Request { + bucket: self.bucket_name.clone(), + prefix: self.prefix_in_bucket.clone(), + continuation_token, + ..ListObjectsV2Request::default() + }) + .await?; + document_keys.extend( + fetch_response + .contents + .unwrap_or_default() + .into_iter() + .filter_map(|o| Some(S3ObjectKey(o.key?))), + ); + + match fetch_response.continuation_token { + Some(new_token) => continuation_token = Some(new_token), + None => break, + } + } + + Ok(document_keys) } async fn upload( &self, - mut from: impl io::AsyncRead + Unpin + Send + Sync + 'static, + from: impl io::AsyncRead + Unpin + Send + Sync + 'static, to: &Self::StoragePath, + metadata: Option, ) -> anyhow::Result<()> { - let mut upload_contents = io::BufWriter::new(std::io::Cursor::new(Vec::new())); - io::copy(&mut from, &mut upload_contents) - .await - .context("Failed to read the upload contents")?; - upload_contents - .flush() - .await - .context("Failed to read the upload contents")?; - let upload_contents = upload_contents.into_inner().into_inner(); - - let (_, code) = self - .bucket - .put_object(to.key(), &upload_contents) - .await - .with_context(|| format!("Failed to create s3 object with key {}", to.key()))?; - if code != 200 { - Err(anyhow::format_err!( - "Received non-200 exit code during creating object with key '{}', code: {}", - to.key(), - code - )) - } else { - Ok(()) - } + self.client + .put_object(PutObjectRequest { + body: Some(StreamingBody::new(ReaderStream::new(from))), + bucket: self.bucket_name.clone(), + key: to.key().to_owned(), + metadata: metadata.map(|m| m.0), + ..PutObjectRequest::default() + }) + .await?; + Ok(()) } async fn download( &self, from: &Self::StoragePath, to: &mut (impl io::AsyncWrite + Unpin + Send + Sync), - ) -> anyhow::Result<()> { - let (data, code) = self - .bucket - .get_object(from.key()) - .await - .with_context(|| format!("Failed to download s3 object with key {}", from.key()))?; - if code != 200 { - Err(anyhow::format_err!( - "Received non-200 exit code during downloading object, code: {}", - code - )) - } else { - // we don't have to write vector into the destination this way, `to_write_all` would be enough. - // but we want to prepare for migration on `rusoto`, that has a streaming HTTP body instead here, with - // which it makes more sense to use `io::copy`. - io::copy(&mut data.as_slice(), to) - .await - .context("Failed to write downloaded data into the destination buffer")?; - Ok(()) + ) -> anyhow::Result> { + let object_output = self + .client + .get_object(GetObjectRequest { + bucket: self.bucket_name.clone(), + key: from.key().to_owned(), + ..GetObjectRequest::default() + }) + .await?; + + if let Some(body) = object_output.body { + let mut from = io::BufReader::new(body.into_async_read()); + io::copy(&mut from, to).await?; } + + Ok(object_output.metadata.map(StorageMetadata)) } async fn download_range( @@ -205,44 +223,41 @@ impl RemoteStorage for S3 { start_inclusive: u64, end_exclusive: Option, to: &mut (impl io::AsyncWrite + Unpin + Send + Sync), - ) -> anyhow::Result<()> { + ) -> anyhow::Result> { // S3 accepts ranges as https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35 // and needs both ends to be exclusive let end_inclusive = end_exclusive.map(|end| end.saturating_sub(1)); - let (data, code) = self - .bucket - .get_object_range(from.key(), start_inclusive, end_inclusive) - .await - .with_context(|| format!("Failed to download s3 object with key {}", from.key()))?; - if code != 206 { - Err(anyhow::format_err!( - "Received non-206 exit code during downloading object range, code: {}", - code - )) - } else { - // see `download` function above for the comment on why `Vec` buffer is copied this way - io::copy(&mut data.as_slice(), to) - .await - .context("Failed to write downloaded range into the destination buffer")?; - Ok(()) + let range = Some(match end_inclusive { + Some(end_inclusive) => format!("bytes={}-{}", start_inclusive, end_inclusive), + None => format!("bytes={}-", start_inclusive), + }); + let object_output = self + .client + .get_object(GetObjectRequest { + bucket: self.bucket_name.clone(), + key: from.key().to_owned(), + range, + ..GetObjectRequest::default() + }) + .await?; + + if let Some(body) = object_output.body { + let mut from = io::BufReader::new(body.into_async_read()); + io::copy(&mut from, to).await?; } + + Ok(object_output.metadata.map(StorageMetadata)) } async fn delete(&self, path: &Self::StoragePath) -> anyhow::Result<()> { - let (_, code) = self - .bucket - .delete_object(path.key()) - .await - .with_context(|| format!("Failed to delete s3 object with key {}", path.key()))?; - if code != 204 { - Err(anyhow::format_err!( - "Received non-204 exit code during deleting object with key '{}', code: {}", - path.key(), - code - )) - } else { - Ok(()) - } + self.client + .delete_object(DeleteObjectRequest { + bucket: self.bucket_name.clone(), + key: path.key().to_owned(), + ..DeleteObjectRequest::default() + }) + .await?; + Ok(()) } } @@ -314,7 +329,7 @@ mod tests { #[test] fn storage_path_negatives() -> anyhow::Result<()> { #[track_caller] - fn storage_path_error(storage: &S3, mismatching_path: &Path) -> String { + fn storage_path_error(storage: &S3Bucket, mismatching_path: &Path) -> String { match storage.storage_path(mismatching_path) { Ok(wrong_key) => panic!( "Expected path '{}' to error, but got S3 key: {:?}", @@ -412,15 +427,11 @@ mod tests { Ok(()) } - fn dummy_storage(pageserver_workdir: &'static Path) -> S3 { - S3 { + fn dummy_storage(pageserver_workdir: &'static Path) -> S3Bucket { + S3Bucket { pageserver_workdir, - bucket: Bucket::new( - "dummy-bucket", - "us-east-1".parse().unwrap(), - Credentials::anonymous().unwrap(), - ) - .unwrap(), + client: S3Client::new("us-east-1".parse().unwrap()), + bucket_name: "dummy-bucket".to_string(), prefix_in_bucket: Some("dummy_prefix/".to_string()), } } diff --git a/pageserver/src/remote_storage/storage_sync.rs b/pageserver/src/remote_storage/storage_sync.rs index 30db5a4d2f..50a260491b 100644 --- a/pageserver/src/remote_storage/storage_sync.rs +++ b/pageserver/src/remote_storage/storage_sync.rs @@ -25,8 +25,9 @@ //! * all never local state gets scheduled for upload, such timelines are "local" and fully operational //! * the rest of the remote timelines are reported to pageserver, but not downloaded before they are actually accessed in pageserver, //! it may schedule the download on such occasions. +//! Then, the index is shared across pageserver under [`RemoteIndex`] guard to ensure proper synchronization. //! -//! The synchronization unit is an archive: a set of timeline files (or relishes) and a special metadata file, all compressed into a blob. +//! The synchronization unit is an archive: a set of layer files and a special metadata file, all compressed into a blob. //! Currently, there's no way to process an archive partially, if the archive processing fails, it has to be started from zero next time again. //! An archive contains set of files of a certain timeline, added during checkpoint(s) and the timeline metadata at that moment. //! The archive contains that metadata's `disk_consistent_lsn` in its name, to be able to restore partial index information from just a remote storage file list. @@ -58,7 +59,7 @@ //! Synchronization never removes any local from pageserver workdir or remote files from the remote storage, yet there could be overwrites of the same files (metadata file updates; future checksum mismatch fixes). //! NOTE: No real contents or checksum check happens right now and is a subject to improve later. //! -//! After the whole timeline is downloaded, [`crate::tenant_mgr::set_timeline_states`] function is used to update pageserver memory stage for the timeline processed. +//! After the whole timeline is downloaded, [`crate::tenant_mgr::apply_timeline_sync_status_updates`] function is used to update pageserver memory stage for the timeline processed. //! //! When pageserver signals shutdown, current sync task gets finished and the loop exists. @@ -80,10 +81,7 @@ use futures::stream::{FuturesUnordered, StreamExt}; use lazy_static::lazy_static; use tokio::{ runtime::Runtime, - sync::{ - mpsc::{self, UnboundedReceiver}, - RwLock, - }, + sync::mpsc::{self, UnboundedReceiver}, time::{Duration, Instant}, }; use tracing::*; @@ -92,18 +90,26 @@ use self::{ compression::ArchiveHeader, download::{download_timeline, DownloadedTimeline}, index::{ - ArchiveDescription, ArchiveId, RemoteTimeline, RemoteTimelineIndex, TimelineIndexEntry, + ArchiveDescription, ArchiveId, RemoteIndex, RemoteTimeline, RemoteTimelineIndex, + TimelineIndexEntry, TimelineIndexEntryInner, }, upload::upload_timeline_checkpoint, }; -use super::{RemoteStorage, SyncStartupData, ZTenantTimelineId}; +use super::{ + LocalTimelineInitStatus, LocalTimelineInitStatuses, RemoteStorage, SyncStartupData, + ZTenantTimelineId, +}; use crate::{ config::PageServerConf, layered_repository::metadata::TimelineMetadata, - remote_storage::storage_sync::compression::read_archive_header, repository::TimelineSyncState, - tenant_mgr::set_timeline_states, thread_mgr, thread_mgr::ThreadKind, + remote_storage::storage_sync::compression::read_archive_header, + repository::TimelineSyncStatusUpdate, tenant_mgr::apply_timeline_sync_status_updates, + thread_mgr, thread_mgr::ThreadKind, }; -use zenith_metrics::{register_histogram_vec, register_int_gauge, HistogramVec, IntGauge}; +use zenith_metrics::{ + register_histogram_vec, register_int_counter, register_int_gauge, HistogramVec, IntCounter, + IntGauge, +}; use zenith_utils::zid::{ZTenantId, ZTimelineId}; lazy_static! { @@ -112,6 +118,11 @@ lazy_static! { "Number of storage sync items left in the queue" ) .expect("failed to register pageserver remote storage remaining sync items int gauge"); + static ref FATAL_TASK_FAILURES: IntCounter = register_int_counter!( + "pageserver_remote_storage_fatal_task_failures", + "Number of critically failed tasks" + ) + .expect("failed to register pageserver remote storage remaining sync items int gauge"); static ref IMAGE_SYNC_TIME: HistogramVec = register_histogram_vec!( "pageserver_remote_storage_image_sync_time", "Time took to synchronize (download or upload) a whole pageserver image. \ @@ -129,7 +140,7 @@ lazy_static! { /// mpsc approach was picked to allow blocking the sync loop if no tasks are present, to avoid meaningless spinning. mod sync_queue { use std::{ - collections::{BTreeSet, HashMap}, + collections::HashMap, sync::atomic::{AtomicUsize, Ordering}, }; @@ -192,9 +203,9 @@ mod sync_queue { pub async fn next_task_batch( receiver: &mut UnboundedReceiver, mut max_batch_size: usize, - ) -> BTreeSet { + ) -> Vec { if max_batch_size == 0 { - return BTreeSet::new(); + return Vec::new(); } let mut tasks = HashMap::with_capacity(max_batch_size); @@ -231,7 +242,7 @@ mod sync_queue { /// A task to run in the async download/upload loop. /// Limited by the number of retries, after certain threshold the failing task gets evicted and the timeline disabled. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +#[derive(Debug, Clone)] pub struct SyncTask { sync_id: ZTenantTimelineId, retries: u32, @@ -248,7 +259,7 @@ impl SyncTask { } } -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +#[derive(Debug, Clone)] enum SyncKind { /// A certain amount of images (archive files) to download. Download(TimelineDownload), @@ -268,15 +279,15 @@ impl SyncKind { /// Local timeline files for upload, appeared after the new checkpoint. /// Current checkpoint design assumes new files are added only, no deletions or amendment happens. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +#[derive(Debug, Clone)] pub struct NewCheckpoint { - /// Relish file paths in the pageserver workdir, that were added for the corresponding checkpoint. + /// layer file paths in the pageserver workdir, that were added for the corresponding checkpoint. layers: Vec, metadata: TimelineMetadata, } /// Info about the remote image files. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +#[derive(Debug, Clone)] struct TimelineDownload { files_to_skip: Arc>, archives_to_skip: BTreeSet, @@ -310,8 +321,8 @@ pub fn schedule_timeline_checkpoint_upload( tenant_id, timeline_id ) } else { - warn!( - "Could not send an upload task for tenant {}, timeline {}: the sync queue is not initialized", + debug!( + "Upload task for tenant {}, timeline {} sent", tenant_id, timeline_id ) } @@ -379,35 +390,42 @@ pub(super) fn spawn_storage_sync_thread< None } }); - let remote_index = RemoteTimelineIndex::try_parse_descriptions_from_paths(conf, download_paths); + let remote_index = RemoteIndex::try_parse_descriptions_from_paths(conf, download_paths); - let initial_timeline_states = schedule_first_sync_tasks(&remote_index, local_timeline_files); + let local_timeline_init_statuses = schedule_first_sync_tasks( + &mut runtime.block_on(remote_index.write()), + local_timeline_files, + ); + let loop_index = remote_index.clone(); thread_mgr::spawn( ThreadKind::StorageSync, None, None, "Remote storage sync thread", + false, move || { storage_sync_loop( runtime, conf, receiver, - remote_index, + loop_index, storage, max_concurrent_sync, max_sync_errors, - ) + ); + Ok(()) }, ) .context("Failed to spawn remote storage sync thread")?; Ok(SyncStartupData { - initial_timeline_states, + remote_index, + local_timeline_init_statuses, }) } enum LoopStep { - NewStates(HashMap>), + SyncStatusUpdates(HashMap>), Shutdown, } @@ -419,41 +437,48 @@ fn storage_sync_loop< runtime: Runtime, conf: &'static PageServerConf, mut receiver: UnboundedReceiver, - index: RemoteTimelineIndex, + index: RemoteIndex, storage: S, max_concurrent_sync: NonZeroUsize, max_sync_errors: NonZeroU32, -) -> anyhow::Result<()> { - let remote_assets = Arc::new((storage, RwLock::new(index))); +) { + let remote_assets = Arc::new((storage, index.clone())); + info!("Starting remote storage sync loop"); loop { + let index = index.clone(); let loop_step = runtime.block_on(async { tokio::select! { - new_timeline_states = loop_step( + step = loop_step( conf, &mut receiver, Arc::clone(&remote_assets), max_concurrent_sync, max_sync_errors, ) - .instrument(debug_span!("storage_sync_loop_step")) => LoopStep::NewStates(new_timeline_states), + .instrument(info_span!("storage_sync_loop_step")) => step, _ = thread_mgr::shutdown_watcher() => LoopStep::Shutdown, } }); match loop_step { - LoopStep::NewStates(new_timeline_states) => { - // Batch timeline download registration to ensure that the external registration code won't block any running tasks before. - set_timeline_states(conf, new_timeline_states); - debug!("Sync loop step completed"); + LoopStep::SyncStatusUpdates(new_timeline_states) => { + if new_timeline_states.is_empty() { + debug!("Sync loop step completed, no new timeline states"); + } else { + info!( + "Sync loop step completed, {} new timeline state update(s)", + new_timeline_states.len() + ); + // Batch timeline download registration to ensure that the external registration code won't block any running tasks before. + apply_timeline_sync_status_updates(conf, index, new_timeline_states); + } } LoopStep::Shutdown => { - debug!("Shutdown requested, stopping"); + info!("Shutdown requested, stopping"); break; } } } - - Ok(()) } async fn loop_step< @@ -462,19 +487,18 @@ async fn loop_step< >( conf: &'static PageServerConf, receiver: &mut UnboundedReceiver, - remote_assets: Arc<(S, RwLock)>, + remote_assets: Arc<(S, RemoteIndex)>, max_concurrent_sync: NonZeroUsize, max_sync_errors: NonZeroU32, -) -> HashMap> { +) -> LoopStep { let max_concurrent_sync = max_concurrent_sync.get(); - let mut next_tasks = BTreeSet::new(); + let mut next_tasks = Vec::new(); // request the first task in blocking fashion to do less meaningless work if let Some(first_task) = sync_queue::next_task(receiver).await { - next_tasks.insert(first_task); + next_tasks.push(first_task); } else { - debug!("Shutdown requested, stopping"); - return HashMap::new(); + return LoopStep::Shutdown; }; next_tasks.extend( sync_queue::next_task_batch(receiver, max_concurrent_sync - 1) @@ -483,12 +507,17 @@ async fn loop_step< ); let remaining_queue_length = sync_queue::len(); - debug!( - "Processing {} tasks in batch, more tasks left to process: {}", - next_tasks.len(), - remaining_queue_length - ); REMAINING_SYNC_ITEMS.set(remaining_queue_length as i64); + if remaining_queue_length > 0 || !next_tasks.is_empty() { + info!( + "Processing {} tasks in batch, more tasks left to process: {}", + next_tasks.len(), + remaining_queue_length + ); + } else { + debug!("No tasks to process"); + return LoopStep::SyncStatusUpdates(HashMap::new()); + } let mut task_batch = next_tasks .into_iter() @@ -498,8 +527,9 @@ async fn loop_step< let sync_name = task.kind.sync_name(); let extra_step = match tokio::spawn( - process_task(conf, Arc::clone(&remote_assets), task, max_sync_errors) - .instrument(debug_span!("", sync_id = %sync_id, attempt, sync_name)), + process_task(conf, Arc::clone(&remote_assets), task, max_sync_errors).instrument( + info_span!("process_sync_task", sync_id = %sync_id, attempt, sync_name), + ), ) .await { @@ -516,8 +546,10 @@ async fn loop_step< }) .collect::>(); - let mut new_timeline_states: HashMap> = - HashMap::with_capacity(max_concurrent_sync); + let mut new_timeline_states: HashMap< + ZTenantId, + HashMap, + > = HashMap::with_capacity(max_concurrent_sync); while let Some((sync_id, state_update)) = task_batch.next().await { debug!("Finished storage sync task for sync id {}", sync_id); if let Some(state_update) = state_update { @@ -532,7 +564,7 @@ async fn loop_step< } } - new_timeline_states + LoopStep::SyncStatusUpdates(new_timeline_states) } async fn process_task< @@ -540,24 +572,19 @@ async fn process_task< S: RemoteStorage + Send + Sync + 'static, >( conf: &'static PageServerConf, - remote_assets: Arc<(S, RwLock)>, + remote_assets: Arc<(S, RemoteIndex)>, task: SyncTask, max_sync_errors: NonZeroU32, -) -> Option { +) -> Option { if task.retries > max_sync_errors.get() { error!( "Evicting task {:?} that failed {} times, exceeding the error threshold", task.kind, task.retries ); - return Some(TimelineSyncState::Evicted( - remote_assets - .as_ref() - .1 - .read() - .await - .timeline_entry(&task.sync_id) - .and_then(TimelineIndexEntry::disk_consistent_lsn), - )); + FATAL_TASK_FAILURES.inc(); + // FIXME (rodionov) this can potentially leave holes in timeline uploads + // planneed to be fixed as part of https://github.com/zenithdb/zenith/issues/977 + return None; } if task.retries > 0 { @@ -569,13 +596,15 @@ async fn process_task< tokio::time::sleep(Duration::from_secs_f64(seconds_to_wait)).await; } + let remote_index = &remote_assets.1; + let sync_start = Instant::now(); let sync_name = task.kind.sync_name(); match task.kind { SyncKind::Download(download_data) => { let download_result = download_timeline( conf, - remote_assets, + remote_assets.clone(), task.sync_id, download_data, task.retries + 1, @@ -585,19 +614,25 @@ async fn process_task< match download_result { DownloadedTimeline::Abort => { register_sync_status(sync_start, sync_name, None); + remote_index + .write() + .await + .set_awaits_download(&task.sync_id, false) + .expect("timeline should be present in remote index"); None } - DownloadedTimeline::FailedAndRescheduled { - disk_consistent_lsn, - } => { + DownloadedTimeline::FailedAndRescheduled => { register_sync_status(sync_start, sync_name, Some(false)); - Some(TimelineSyncState::AwaitsDownload(disk_consistent_lsn)) + None } - DownloadedTimeline::Successful { - disk_consistent_lsn, - } => { + DownloadedTimeline::Successful => { register_sync_status(sync_start, sync_name, Some(true)); - Some(TimelineSyncState::Ready(disk_consistent_lsn)) + remote_index + .write() + .await + .set_awaits_download(&task.sync_id, false) + .expect("timeline should be present in remote index"); + Some(TimelineSyncStatusUpdate::Downloaded) } } } @@ -617,45 +652,45 @@ async fn process_task< } fn schedule_first_sync_tasks( - index: &RemoteTimelineIndex, + index: &mut RemoteTimelineIndex, local_timeline_files: HashMap)>, -) -> HashMap> { - let mut initial_timeline_statuses: HashMap> = - HashMap::new(); +) -> LocalTimelineInitStatuses { + let mut local_timeline_init_statuses = LocalTimelineInitStatuses::new(); let mut new_sync_tasks = VecDeque::with_capacity(local_timeline_files.len().max(local_timeline_files.len())); for (sync_id, (local_metadata, local_files)) in local_timeline_files { - let local_disk_consistent_lsn = local_metadata.disk_consistent_lsn(); - let ZTenantTimelineId { tenant_id, timeline_id, } = sync_id; - match index.timeline_entry(&sync_id) { + match index.timeline_entry_mut(&sync_id) { Some(index_entry) => { - let timeline_status = compare_local_and_remote_timeline( + let (timeline_status, awaits_download) = compare_local_and_remote_timeline( &mut new_sync_tasks, sync_id, local_metadata, local_files, index_entry, ); - match timeline_status { - Some(timeline_status) => { - initial_timeline_statuses - .entry(tenant_id) - .or_default() - .insert(timeline_id, timeline_status); - } - None => error!( - "Failed to compare local and remote timeline for task {}", - sync_id - ), + let was_there = local_timeline_init_statuses + .entry(tenant_id) + .or_default() + .insert(timeline_id, timeline_status); + + if was_there.is_some() { + // defensive check + warn!( + "Overwriting timeline init sync status. Status {:?} Timeline {}", + timeline_status, timeline_id + ); } + index_entry.set_awaits_download(awaits_download); } None => { + // TODO (rodionov) does this mean that we've crashed during tenant creation? + // is it safe to upload this checkpoint? could it be half broken? new_sync_tasks.push_back(SyncTask::new( sync_id, 0, @@ -664,56 +699,18 @@ fn schedule_first_sync_tasks( metadata: local_metadata, }), )); - initial_timeline_statuses + local_timeline_init_statuses .entry(tenant_id) .or_default() - .insert( - timeline_id, - TimelineSyncState::Ready(local_disk_consistent_lsn), - ); + .insert(timeline_id, LocalTimelineInitStatus::LocallyComplete); } } } - let unprocessed_remote_ids = |remote_id: &ZTenantTimelineId| { - initial_timeline_statuses - .get(&remote_id.tenant_id) - .and_then(|timelines| timelines.get(&remote_id.timeline_id)) - .is_none() - }; - for unprocessed_remote_id in index - .all_sync_ids() - .filter(unprocessed_remote_ids) - .collect::>() - { - let ZTenantTimelineId { - tenant_id: cloud_only_tenant_id, - timeline_id: cloud_only_timeline_id, - } = unprocessed_remote_id; - match index - .timeline_entry(&unprocessed_remote_id) - .and_then(TimelineIndexEntry::disk_consistent_lsn) - { - Some(remote_disk_consistent_lsn) => { - initial_timeline_statuses - .entry(cloud_only_tenant_id) - .or_default() - .insert( - cloud_only_timeline_id, - TimelineSyncState::CloudOnly(remote_disk_consistent_lsn), - ); - } - None => error!( - "Failed to find disk consistent LSN for remote timeline {}", - unprocessed_remote_id - ), - } - } - new_sync_tasks.into_iter().for_each(|task| { sync_queue::push(task); }); - initial_timeline_statuses + local_timeline_init_statuses } fn compare_local_and_remote_timeline( @@ -722,10 +719,21 @@ fn compare_local_and_remote_timeline( local_metadata: TimelineMetadata, local_files: Vec, remote_entry: &TimelineIndexEntry, -) -> Option { +) -> (LocalTimelineInitStatus, bool) { let local_lsn = local_metadata.disk_consistent_lsn(); let uploads = remote_entry.uploaded_checkpoints(); + let mut initial_timeline_status = LocalTimelineInitStatus::LocallyComplete; + + let mut awaits_download = false; + // TODO probably here we need more sophisticated logic, + // if more data is available remotely can we just download whats there? + // without trying to upload something. It may be tricky, needs further investigation. + // For now looks strange that we can request upload + // and dowload for the same timeline simultaneously. + // (upload needs to be only for previously unsynced files, not whole timeline dir). + // If one of the tasks fails they will be reordered in the queue which can lead + // to timeline being stuck in evicted state if !uploads.contains(&local_lsn) { new_sync_tasks.push_back(SyncTask::new( sync_id, @@ -735,6 +743,7 @@ fn compare_local_and_remote_timeline( metadata: local_metadata, }), )); + // Note that status here doesnt change. } let uploads_count = uploads.len(); @@ -743,7 +752,7 @@ fn compare_local_and_remote_timeline( .filter(|upload_lsn| upload_lsn <= &local_lsn) .map(ArchiveId) .collect(); - Some(if archives_to_skip.len() != uploads_count { + if archives_to_skip.len() != uploads_count { new_sync_tasks.push_back(SyncTask::new( sync_id, 0, @@ -752,10 +761,12 @@ fn compare_local_and_remote_timeline( archives_to_skip, }), )); - TimelineSyncState::AwaitsDownload(remote_entry.disk_consistent_lsn()?) - } else { - TimelineSyncState::Ready(remote_entry.disk_consistent_lsn().unwrap_or(local_lsn)) - }) + initial_timeline_status = LocalTimelineInitStatus::NeedsSync; + awaits_download = true; + // we do not need to manupulate with remote consistent lsn here + // because it will be updated when sync will be completed + } + (initial_timeline_status, awaits_download) } fn register_sync_status(sync_start: Instant, sync_name: &str, sync_status: Option) { @@ -769,21 +780,23 @@ fn register_sync_status(sync_start: Instant, sync_name: &str, sync_status: Optio .observe(secs_elapsed) } -async fn update_index_description< +async fn fetch_full_index< P: Send + Sync + 'static, S: RemoteStorage + Send + Sync + 'static, >( - (storage, index): &(S, RwLock), + (storage, index): &(S, RemoteIndex), timeline_dir: &Path, id: ZTenantTimelineId, ) -> anyhow::Result { - let mut index_write = index.write().await; - let full_index = match index_write.timeline_entry(&id) { + let index_read = index.read().await; + let full_index = match index_read.timeline_entry(&id).map(|e| e.inner()) { None => bail!("Timeline not found for sync id {}", id), - Some(TimelineIndexEntry::Full(_)) => bail!("Index is already populated for sync id {}", id), - Some(TimelineIndexEntry::Description(description)) => { + Some(TimelineIndexEntryInner::Full(_)) => { + bail!("Index is already populated for sync id {}", id) + } + Some(TimelineIndexEntryInner::Description(description)) => { let mut archive_header_downloads = FuturesUnordered::new(); - for (&archive_id, description) in description { + for (archive_id, description) in description { archive_header_downloads.push(async move { let header = download_archive_header(storage, timeline_dir, description) .await @@ -795,18 +808,23 @@ async fn update_index_description< let mut full_index = RemoteTimeline::empty(); while let Some(header_data) = archive_header_downloads.next().await { match header_data { - Ok((archive_id, header_size, header)) => full_index.update_archive_contents(archive_id.0, header, header_size), - Err((e, archive_id)) => bail!( - "Failed to download archive header for tenant {}, timeline {}, archive for Lsn {}: {}", - id.tenant_id, id.timeline_id, archive_id.0, - e - ), - } + Ok((archive_id, header_size, header)) => full_index.update_archive_contents(archive_id.0, header, header_size), + Err((e, archive_id)) => bail!( + "Failed to download archive header for tenant {}, timeline {}, archive for Lsn {}: {}", + id.tenant_id, id.timeline_id, archive_id.0, + e + ), + } } full_index } }; - index_write.add_timeline_entry(id, TimelineIndexEntry::Full(full_index.clone())); + drop(index_read); // tokio rw lock is not upgradeable + index + .write() + .await + .upgrade_timeline_entry(&id, full_index.clone()) + .context("cannot upgrade timeline entry in remote index")?; Ok(full_index) } @@ -850,7 +868,7 @@ mod test_utils { #[track_caller] pub async fn ensure_correct_timeline_upload( harness: &RepoHarness<'_>, - remote_assets: Arc<(LocalFs, RwLock)>, + remote_assets: Arc<(LocalFs, RemoteIndex)>, timeline_id: ZTimelineId, new_upload: NewCheckpoint, ) { @@ -867,7 +885,7 @@ mod test_utils { let (storage, index) = remote_assets.as_ref(); assert_index_descriptions( index, - RemoteTimelineIndex::try_parse_descriptions_from_paths( + &RemoteIndex::try_parse_descriptions_from_paths( harness.conf, remote_assets .0 @@ -909,11 +927,14 @@ mod test_utils { } pub async fn expect_timeline( - index: &RwLock, + index: &RemoteIndex, sync_id: ZTenantTimelineId, ) -> RemoteTimeline { - if let Some(TimelineIndexEntry::Full(remote_timeline)) = - index.read().await.timeline_entry(&sync_id) + if let Some(TimelineIndexEntryInner::Full(remote_timeline)) = index + .read() + .await + .timeline_entry(&sync_id) + .map(|e| e.inner()) { remote_timeline.clone() } else { @@ -926,9 +947,11 @@ mod test_utils { #[track_caller] pub async fn assert_index_descriptions( - index: &RwLock, - expected_index_with_descriptions: RemoteTimelineIndex, + index: &RemoteIndex, + expected_index_with_descriptions: &RemoteIndex, ) { + let expected_index_with_descriptions = expected_index_with_descriptions.read().await; + let index_read = index.read().await; let actual_sync_ids = index_read.all_sync_ids().collect::>(); let expected_sync_ids = expected_index_with_descriptions @@ -965,26 +988,26 @@ mod test_utils { sync_id ) }); - let expected_timeline_description = match expected_timeline_description { - TimelineIndexEntry::Description(description) => description, - TimelineIndexEntry::Full(_) => panic!("Expected index entry for sync id {} is a full entry, while a description was expected", sync_id), + let expected_timeline_description = match expected_timeline_description.inner() { + TimelineIndexEntryInner::Description(description) => description, + TimelineIndexEntryInner::Full(_) => panic!("Expected index entry for sync id {} is a full entry, while a description was expected", sync_id), }; - match actual_timeline_entry { - TimelineIndexEntry::Description(actual_descriptions) => { + match actual_timeline_entry.inner() { + TimelineIndexEntryInner::Description(description) => { assert_eq!( - actual_descriptions, expected_timeline_description, + description, expected_timeline_description, "Index contains unexpected descriptions entry for sync id {}", sync_id ) } - TimelineIndexEntry::Full(actual_full_entry) => { + TimelineIndexEntryInner::Full(remote_timeline) => { let expected_lsns = expected_timeline_description .values() .map(|description| description.disk_consistent_lsn) .collect::>(); assert_eq!( - actual_full_entry.checkpoints().collect::>(), + remote_timeline.checkpoints().collect::>(), expected_lsns, "Timeline {} should have the same checkpoints uploaded", sync_id, diff --git a/pageserver/src/remote_storage/storage_sync/compression.rs b/pageserver/src/remote_storage/storage_sync/compression.rs index ca245359bf..511f79e0cf 100644 --- a/pageserver/src/remote_storage/storage_sync/compression.rs +++ b/pageserver/src/remote_storage/storage_sync/compression.rs @@ -10,7 +10,7 @@ //! Archiving is almost agnostic to timeline file types, with an exception of the metadata file, that's currently distinguished in the [un]compression code. //! The metadata file is treated separately when [de]compression is involved, to reduce the risk of corrupting the metadata file. //! When compressed, the metadata file is always required and stored as the last file in the archive stream. -//! When uncompressed, the metadata file gets naturally uncompressed last, to ensure that all other relishes are decompressed successfully first. +//! When uncompressed, the metadata file gets naturally uncompressed last, to ensure that all other layer files are decompressed successfully first. //! //! Archive structure: //! +----------------------------------------+ @@ -201,8 +201,7 @@ pub async fn read_archive_header( .await .context("Failed to decompress a header from the archive")?; - Ok(ArchiveHeader::des(&header_bytes) - .context("Failed to deserialize a header from the archive")?) + ArchiveHeader::des(&header_bytes).context("Failed to deserialize a header from the archive") } /// Reads the archive metadata out of the archive name: diff --git a/pageserver/src/remote_storage/storage_sync/download.rs b/pageserver/src/remote_storage/storage_sync/download.rs index 00115ba8d5..773b4a12e5 100644 --- a/pageserver/src/remote_storage/storage_sync/download.rs +++ b/pageserver/src/remote_storage/storage_sync/download.rs @@ -3,16 +3,16 @@ use std::{borrow::Cow, collections::BTreeSet, path::PathBuf, sync::Arc}; use anyhow::{ensure, Context}; -use tokio::{fs, sync::RwLock}; +use tokio::fs; use tracing::{debug, error, trace, warn}; -use zenith_utils::{lsn::Lsn, zid::ZTenantId}; +use zenith_utils::zid::ZTenantId; use crate::{ config::PageServerConf, layered_repository::metadata::{metadata_path, TimelineMetadata}, remote_storage::{ storage_sync::{ - compression, index::TimelineIndexEntry, sync_queue, update_index_description, SyncKind, + compression, fetch_full_index, index::TimelineIndexEntryInner, sync_queue, SyncKind, SyncTask, }, RemoteStorage, ZTenantTimelineId, @@ -20,8 +20,8 @@ use crate::{ }; use super::{ - index::{ArchiveId, RemoteTimeline, RemoteTimelineIndex}, - TimelineDownload, + index::{ArchiveId, RemoteTimeline}, + RemoteIndex, TimelineDownload, }; /// Timeline download result, with extra data, needed for downloading. @@ -30,10 +30,10 @@ pub(super) enum DownloadedTimeline { Abort, /// Remote timeline data is found, its latest checkpoint's metadata contents (disk_consistent_lsn) is known. /// Initial download failed due to some error, the download task is rescheduled for another retry. - FailedAndRescheduled { disk_consistent_lsn: Lsn }, + FailedAndRescheduled, /// Remote timeline data is found, its latest checkpoint's metadata contents (disk_consistent_lsn) is known. /// Initial download successful. - Successful { disk_consistent_lsn: Lsn }, + Successful, } /// Attempts to download and uncompress files from all remote archives for the timeline given. @@ -47,7 +47,7 @@ pub(super) async fn download_timeline< S: RemoteStorage + Send + Sync + 'static, >( conf: &'static PageServerConf, - remote_assets: Arc<(S, RwLock)>, + remote_assets: Arc<(S, RemoteIndex)>, sync_id: ZTenantTimelineId, mut download: TimelineDownload, retries: u32, @@ -58,19 +58,26 @@ pub(super) async fn download_timeline< tenant_id, timeline_id, } = sync_id; - let index_read = remote_assets.1.read().await; + let index = &remote_assets.1; + + let index_read = index.read().await; let remote_timeline = match index_read.timeline_entry(&sync_id) { None => { - error!("Cannot download: no timeline is present in the index for given ids"); + error!("Cannot download: no timeline is present in the index for given id"); return DownloadedTimeline::Abort; } - Some(index_entry) => match index_entry { - TimelineIndexEntry::Full(remote_timeline) => Cow::Borrowed(remote_timeline), - TimelineIndexEntry::Description(_) => { + + Some(index_entry) => match index_entry.inner() { + TimelineIndexEntryInner::Full(remote_timeline) => Cow::Borrowed(remote_timeline), + TimelineIndexEntryInner::Description(_) => { + // we do not check here for awaits_download because it is ok + // to call this function while the download is in progress + // so it is not a concurrent download, it is the same one + let remote_disk_consistent_lsn = index_entry.disk_consistent_lsn(); drop(index_read); debug!("Found timeline description for the given ids, downloading the full index"); - match update_index_description( + match fetch_full_index( remote_assets.as_ref(), &conf.timeline_path(&timeline_id, &tenant_id), sync_id, @@ -80,16 +87,15 @@ pub(super) async fn download_timeline< Ok(remote_timeline) => Cow::Owned(remote_timeline), Err(e) => { error!("Failed to download full timeline index: {:?}", e); + return match remote_disk_consistent_lsn { - Some(disk_consistent_lsn) => { + Some(_) => { sync_queue::push(SyncTask::new( sync_id, retries, SyncKind::Download(download), )); - DownloadedTimeline::FailedAndRescheduled { - disk_consistent_lsn, - } + DownloadedTimeline::FailedAndRescheduled } None => { error!("Cannot download: no disk consistent Lsn is present for the index entry"); @@ -101,12 +107,9 @@ pub(super) async fn download_timeline< } }, }; - let disk_consistent_lsn = match remote_timeline.checkpoints().max() { - Some(lsn) => lsn, - None => { - debug!("Cannot download: no disk consistent Lsn is present for the remote timeline"); - return DownloadedTimeline::Abort; - } + if remote_timeline.checkpoints().max().is_none() { + debug!("Cannot download: no disk consistent Lsn is present for the remote timeline"); + return DownloadedTimeline::Abort; }; debug!("Downloading timeline archives"); @@ -125,7 +128,7 @@ pub(super) async fn download_timeline< conf, sync_id, Arc::clone(&remote_assets), - remote_timeline.as_ref(), + &remote_timeline, archive_id, Arc::clone(&download.files_to_skip), ) @@ -142,9 +145,7 @@ pub(super) async fn download_timeline< retries, SyncKind::Download(download), )); - return DownloadedTimeline::FailedAndRescheduled { - disk_consistent_lsn, - }; + return DownloadedTimeline::FailedAndRescheduled; } Ok(()) => { debug!("Successfully downloaded archive {:?}", archive_id); @@ -154,9 +155,7 @@ pub(super) async fn download_timeline< } debug!("Finished downloading all timeline's archives"); - DownloadedTimeline::Successful { - disk_consistent_lsn, - } + DownloadedTimeline::Successful } async fn try_download_archive< @@ -168,7 +167,7 @@ async fn try_download_archive< tenant_id, timeline_id, }: ZTenantTimelineId, - remote_assets: Arc<(S, RwLock)>, + remote_assets: Arc<(S, RemoteIndex)>, remote_timeline: &RemoteTimeline, archive_id: ArchiveId, files_to_skip: Arc>, @@ -226,8 +225,8 @@ async fn read_local_metadata( let local_metadata_bytes = fs::read(&local_metadata_path) .await .context("Failed to read local metadata file bytes")?; - Ok(TimelineMetadata::from_bytes(&local_metadata_bytes) - .context("Failed to read local metadata files bytes")?) + TimelineMetadata::from_bytes(&local_metadata_bytes) + .context("Failed to read local metadata files bytes") } #[cfg(test)] @@ -256,14 +255,14 @@ mod tests { let repo_harness = RepoHarness::create("test_download_timeline")?; let sync_id = ZTenantTimelineId::new(repo_harness.tenant_id, TIMELINE_ID); let storage = LocalFs::new(tempdir()?.path().to_owned(), &repo_harness.conf.workdir)?; - let index = RwLock::new(RemoteTimelineIndex::try_parse_descriptions_from_paths( + let index = RemoteIndex::try_parse_descriptions_from_paths( repo_harness.conf, storage .list() .await? .into_iter() .map(|storage_path| storage.local_path(&storage_path).unwrap()), - )); + ); let remote_assets = Arc::new((storage, index)); let storage = &remote_assets.0; let index = &remote_assets.1; @@ -313,7 +312,7 @@ mod tests { .await; assert_index_descriptions( index, - RemoteTimelineIndex::try_parse_descriptions_from_paths( + &RemoteIndex::try_parse_descriptions_from_paths( repo_harness.conf, remote_assets .0 diff --git a/pageserver/src/remote_storage/storage_sync/index.rs b/pageserver/src/remote_storage/storage_sync/index.rs index 81c99754c9..861b78fa3b 100644 --- a/pageserver/src/remote_storage/storage_sync/index.rs +++ b/pageserver/src/remote_storage/storage_sync/index.rs @@ -7,11 +7,13 @@ use std::{ collections::{BTreeMap, BTreeSet, HashMap}, path::{Path, PathBuf}, + sync::Arc, }; use anyhow::{bail, ensure, Context}; use serde::{Deserialize, Serialize}; -use tracing::debug; +use tokio::sync::RwLock; +use tracing::*; use zenith_utils::{ lsn::Lsn, zid::{ZTenantId, ZTimelineId}, @@ -52,10 +54,19 @@ impl RelativePath { /// Currently, timeline archive files are tracked only. #[derive(Debug, Clone)] pub struct RemoteTimelineIndex { - timeline_files: HashMap, + timeline_entries: HashMap, } -impl RemoteTimelineIndex { +/// A wrapper to synchrnize access to the index, should be created and used before dealing with any [`RemoteTimelineIndex`]. +pub struct RemoteIndex(Arc>); + +impl RemoteIndex { + pub fn empty() -> Self { + Self(Arc::new(RwLock::new(RemoteTimelineIndex { + timeline_entries: HashMap::new(), + }))) + } + /// Attempts to parse file paths (not checking the file contents) and find files /// that can be tracked wiht the index. /// On parse falures, logs the error and continues, so empty index can be created from not suitable paths. @@ -63,8 +74,8 @@ impl RemoteTimelineIndex { conf: &'static PageServerConf, paths: impl Iterator, ) -> Self { - let mut index = Self { - timeline_files: HashMap::new(), + let mut index = RemoteTimelineIndex { + timeline_entries: HashMap::new(), }; for path in paths { if let Err(e) = try_parse_index_entry(&mut index, conf, path.as_ref()) { @@ -75,44 +86,121 @@ impl RemoteTimelineIndex { ); } } - index + + Self(Arc::new(RwLock::new(index))) } + pub async fn read(&self) -> tokio::sync::RwLockReadGuard<'_, RemoteTimelineIndex> { + self.0.read().await + } + + pub async fn write(&self) -> tokio::sync::RwLockWriteGuard<'_, RemoteTimelineIndex> { + self.0.write().await + } +} + +impl Clone for RemoteIndex { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } +} + +impl RemoteTimelineIndex { pub fn timeline_entry(&self, id: &ZTenantTimelineId) -> Option<&TimelineIndexEntry> { - self.timeline_files.get(id) + self.timeline_entries.get(id) } pub fn timeline_entry_mut( &mut self, id: &ZTenantTimelineId, ) -> Option<&mut TimelineIndexEntry> { - self.timeline_files.get_mut(id) + self.timeline_entries.get_mut(id) } pub fn add_timeline_entry(&mut self, id: ZTenantTimelineId, entry: TimelineIndexEntry) { - self.timeline_files.insert(id, entry); + self.timeline_entries.insert(id, entry); + } + + pub fn upgrade_timeline_entry( + &mut self, + id: &ZTenantTimelineId, + remote_timeline: RemoteTimeline, + ) -> anyhow::Result<()> { + let mut entry = self.timeline_entries.get_mut(id).ok_or(anyhow::anyhow!( + "timeline is unexpectedly missing from remote index" + ))?; + + if !matches!(entry.inner, TimelineIndexEntryInner::Description(_)) { + anyhow::bail!("timeline entry is not a description entry") + }; + + entry.inner = TimelineIndexEntryInner::Full(remote_timeline); + + Ok(()) } pub fn all_sync_ids(&self) -> impl Iterator + '_ { - self.timeline_files.keys().copied() + self.timeline_entries.keys().copied() + } + + pub fn set_awaits_download( + &mut self, + id: &ZTenantTimelineId, + awaits_download: bool, + ) -> anyhow::Result<()> { + self.timeline_entry_mut(id) + .ok_or_else(|| anyhow::anyhow!("unknown timeline sync {}", id))? + .set_awaits_download(awaits_download); + Ok(()) } } +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct DescriptionTimelineIndexEntry { + pub description: BTreeMap, + pub awaits_download: bool, +} + #[derive(Debug, Clone, PartialEq, Eq)] -pub enum TimelineIndexEntry { - /// An archive found on the remote storage, but not yet downloaded, only a metadata from its storage path is available, without archive contents. +pub struct FullTimelineIndexEntry { + pub remote_timeline: RemoteTimeline, + pub awaits_download: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TimelineIndexEntryInner { Description(BTreeMap), - /// Full archive metadata, including the file list, parsed from the archive header. Full(RemoteTimeline), } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TimelineIndexEntry { + inner: TimelineIndexEntryInner, + awaits_download: bool, +} + impl TimelineIndexEntry { + pub fn new(inner: TimelineIndexEntryInner, awaits_download: bool) -> Self { + Self { + inner, + awaits_download, + } + } + + pub fn inner(&self) -> &TimelineIndexEntryInner { + &self.inner + } + + pub fn inner_mut(&mut self) -> &mut TimelineIndexEntryInner { + &mut self.inner + } + pub fn uploaded_checkpoints(&self) -> BTreeSet { - match self { - Self::Description(description) => { + match &self.inner { + TimelineIndexEntryInner::Description(description) => { description.keys().map(|archive_id| archive_id.0).collect() } - Self::Full(remote_timeline) => remote_timeline + TimelineIndexEntryInner::Full(remote_timeline) => remote_timeline .checkpoint_archives .keys() .map(|archive_id| archive_id.0) @@ -122,17 +210,25 @@ impl TimelineIndexEntry { /// Gets latest uploaded checkpoint's disk consisten Lsn for the corresponding timeline. pub fn disk_consistent_lsn(&self) -> Option { - match self { - Self::Description(description) => { + match &self.inner { + TimelineIndexEntryInner::Description(description) => { description.keys().map(|archive_id| archive_id.0).max() } - Self::Full(remote_timeline) => remote_timeline + TimelineIndexEntryInner::Full(remote_timeline) => remote_timeline .checkpoint_archives .keys() .map(|archive_id| archive_id.0) .max(), } } + + pub fn get_awaits_download(&self) -> bool { + self.awaits_download + } + + pub fn set_awaits_download(&mut self, awaits_download: bool) { + self.awaits_download = awaits_download; + } } /// Checkpoint archive's id, corresponding to the `disk_consistent_lsn` from the timeline's metadata file during checkpointing. @@ -181,7 +277,7 @@ impl RemoteTimeline { .map(CheckpointArchive::disk_consistent_lsn) } - /// Lists all relish files in the given remote timeline. Omits the metadata file. + /// Lists all layer files in the given remote timeline. Omits the metadata file. pub fn stored_files(&self, timeline_dir: &Path) -> BTreeSet { self.timeline_files .values() @@ -331,13 +427,15 @@ fn try_parse_index_entry( tenant_id, timeline_id, }; - let timeline_index_entry = index - .timeline_files - .entry(sync_id) - .or_insert_with(|| TimelineIndexEntry::Description(BTreeMap::new())); - match timeline_index_entry { - TimelineIndexEntry::Description(descriptions) => { - descriptions.insert( + let timeline_index_entry = index.timeline_entries.entry(sync_id).or_insert_with(|| { + TimelineIndexEntry::new( + TimelineIndexEntryInner::Description(BTreeMap::default()), + false, + ) + }); + match timeline_index_entry.inner_mut() { + TimelineIndexEntryInner::Description(description) => { + description.insert( ArchiveId(disk_consistent_lsn), ArchiveDescription { header_size, @@ -346,7 +444,7 @@ fn try_parse_index_entry( }, ); } - TimelineIndexEntry::Full(_) => { + TimelineIndexEntryInner::Full(_) => { bail!("Cannot add parsed archive description to its full context in index with sync id {}", sync_id) } } diff --git a/pageserver/src/remote_storage/storage_sync/upload.rs b/pageserver/src/remote_storage/storage_sync/upload.rs index d064039ecc..f955e04474 100644 --- a/pageserver/src/remote_storage/storage_sync/upload.rs +++ b/pageserver/src/remote_storage/storage_sync/upload.rs @@ -2,23 +2,21 @@ use std::{borrow::Cow, collections::BTreeSet, path::PathBuf, sync::Arc}; -use anyhow::ensure; -use tokio::sync::RwLock; use tracing::{debug, error, warn}; use crate::{ config::PageServerConf, remote_storage::{ storage_sync::{ - compression, - index::{RemoteTimeline, TimelineIndexEntry}, - sync_queue, update_index_description, SyncKind, SyncTask, + compression, fetch_full_index, + index::{RemoteTimeline, TimelineIndexEntry, TimelineIndexEntryInner}, + sync_queue, SyncKind, SyncTask, }, RemoteStorage, ZTenantTimelineId, }, }; -use super::{compression::ArchiveHeader, index::RemoteTimelineIndex, NewCheckpoint}; +use super::{compression::ArchiveHeader, NewCheckpoint, RemoteIndex}; /// Attempts to compress and upload given checkpoint files. /// No extra checks for overlapping files is made: download takes care of that, ensuring no non-metadata local timeline files are overwritten. @@ -30,7 +28,7 @@ pub(super) async fn upload_timeline_checkpoint< S: RemoteStorage + Send + Sync + 'static, >( config: &'static PageServerConf, - remote_assets: Arc<(S, RwLock)>, + remote_assets: Arc<(S, RemoteIndex)>, sync_id: ZTenantTimelineId, new_checkpoint: NewCheckpoint, retries: u32, @@ -49,22 +47,24 @@ pub(super) async fn upload_timeline_checkpoint< let index_read = index.read().await; let remote_timeline = match index_read.timeline_entry(&sync_id) { None => None, - Some(TimelineIndexEntry::Full(remote_timeline)) => Some(Cow::Borrowed(remote_timeline)), - Some(TimelineIndexEntry::Description(_)) => { - debug!("Found timeline description for the given ids, downloading the full index"); - match update_index_description(remote_assets.as_ref(), &timeline_dir, sync_id).await { - Ok(remote_timeline) => Some(Cow::Owned(remote_timeline)), - Err(e) => { - error!("Failed to download full timeline index: {:?}", e); - sync_queue::push(SyncTask::new( - sync_id, - retries, - SyncKind::Upload(new_checkpoint), - )); - return Some(false); + Some(entry) => match entry.inner() { + TimelineIndexEntryInner::Full(remote_timeline) => Some(Cow::Borrowed(remote_timeline)), + TimelineIndexEntryInner::Description(_) => { + debug!("Found timeline description for the given ids, downloading the full index"); + match fetch_full_index(remote_assets.as_ref(), &timeline_dir, sync_id).await { + Ok(remote_timeline) => Some(Cow::Owned(remote_timeline)), + Err(e) => { + error!("Failed to download full timeline index: {:?}", e); + sync_queue::push(SyncTask::new( + sync_id, + retries, + SyncKind::Upload(new_checkpoint), + )); + return Some(false); + } } } - } + }, }; let already_contains_upload_lsn = remote_timeline @@ -93,30 +93,48 @@ pub(super) async fn upload_timeline_checkpoint< ) .await { - Ok((archive_header, header_size)) => { + Some(Ok((archive_header, header_size))) => { let mut index_write = index.write().await; - match index_write.timeline_entry_mut(&sync_id) { - Some(TimelineIndexEntry::Full(remote_timeline)) => { - remote_timeline.update_archive_contents( - new_checkpoint.metadata.disk_consistent_lsn(), - archive_header, - header_size, - ); - } - None | Some(TimelineIndexEntry::Description(_)) => { + match index_write + .timeline_entry_mut(&sync_id) + .map(|e| e.inner_mut()) + { + None => { let mut new_timeline = RemoteTimeline::empty(); new_timeline.update_archive_contents( new_checkpoint.metadata.disk_consistent_lsn(), archive_header, header_size, ); - index_write.add_timeline_entry(sync_id, TimelineIndexEntry::Full(new_timeline)); + index_write.add_timeline_entry( + sync_id, + TimelineIndexEntry::new(TimelineIndexEntryInner::Full(new_timeline), false), + ) + } + Some(TimelineIndexEntryInner::Full(remote_timeline)) => { + remote_timeline.update_archive_contents( + new_checkpoint.metadata.disk_consistent_lsn(), + archive_header, + header_size, + ); + } + Some(TimelineIndexEntryInner::Description(_)) => { + let mut new_timeline = RemoteTimeline::empty(); + new_timeline.update_archive_contents( + new_checkpoint.metadata.disk_consistent_lsn(), + archive_header, + header_size, + ); + index_write.add_timeline_entry( + sync_id, + TimelineIndexEntry::new(TimelineIndexEntryInner::Full(new_timeline), false), + ) } } debug!("Checkpoint uploaded successfully"); Some(true) } - Err(e) => { + Some(Err(e)) => { error!( "Failed to upload checkpoint: {:?}, requeueing the upload", e @@ -128,6 +146,7 @@ pub(super) async fn upload_timeline_checkpoint< )); Some(false) } + None => Some(true), } } @@ -136,11 +155,11 @@ async fn try_upload_checkpoint< S: RemoteStorage + Send + Sync + 'static, >( config: &'static PageServerConf, - remote_assets: Arc<(S, RwLock)>, + remote_assets: Arc<(S, RemoteIndex)>, sync_id: ZTenantTimelineId, new_checkpoint: &NewCheckpoint, files_to_skip: BTreeSet, -) -> anyhow::Result<(ArchiveHeader, u64)> { +) -> Option> { let ZTenantTimelineId { tenant_id, timeline_id, @@ -152,7 +171,7 @@ async fn try_upload_checkpoint< .iter() .filter(|&path_to_upload| { if files_to_skip.contains(path_to_upload) { - error!( + warn!( "Skipping file upload '{}', since it was already uploaded", path_to_upload.display() ); @@ -162,9 +181,16 @@ async fn try_upload_checkpoint< } }) .collect::>(); - ensure!(!files_to_upload.is_empty(), "No files to upload"); - compression::archive_files_as_stream( + if files_to_upload.is_empty() { + warn!( + "No files to upload. Upload request was: {:?}, already uploaded files: {:?}", + new_checkpoint.layers, files_to_skip + ); + return None; + } + + let upload_result = compression::archive_files_as_stream( &timeline_dir, files_to_upload.into_iter(), &new_checkpoint.metadata, @@ -175,12 +201,15 @@ async fn try_upload_checkpoint< .upload( archive_streamer, &remote_storage.storage_path(&timeline_dir.join(&archive_name))?, + None, ) .await }, ) .await - .map(|(header, header_size, _)| (header, header_size)) + .map(|(header, header_size, _)| (header, header_size)); + + Some(upload_result) } #[cfg(test)] @@ -209,14 +238,14 @@ mod tests { let repo_harness = RepoHarness::create("reupload_timeline")?; let sync_id = ZTenantTimelineId::new(repo_harness.tenant_id, TIMELINE_ID); let storage = LocalFs::new(tempdir()?.path().to_owned(), &repo_harness.conf.workdir)?; - let index = RwLock::new(RemoteTimelineIndex::try_parse_descriptions_from_paths( + let index = RemoteIndex::try_parse_descriptions_from_paths( repo_harness.conf, storage .list() .await? .into_iter() .map(|storage_path| storage.local_path(&storage_path).unwrap()), - )); + ); let remote_assets = Arc::new((storage, index)); let index = &remote_assets.1; @@ -405,14 +434,14 @@ mod tests { let repo_harness = RepoHarness::create("reupload_timeline_rejected")?; let sync_id = ZTenantTimelineId::new(repo_harness.tenant_id, TIMELINE_ID); let storage = LocalFs::new(tempdir()?.path().to_owned(), &repo_harness.conf.workdir)?; - let index = RwLock::new(RemoteTimelineIndex::try_parse_descriptions_from_paths( + let index = RemoteIndex::try_parse_descriptions_from_paths( repo_harness.conf, storage .list() .await? .into_iter() .map(|storage_path| storage.local_path(&storage_path).unwrap()), - )); + ); let remote_assets = Arc::new((storage, index)); let storage = &remote_assets.0; let index = &remote_assets.1; @@ -431,7 +460,7 @@ mod tests { first_checkpoint, ) .await; - let after_first_uploads = RemoteTimelineIndex::try_parse_descriptions_from_paths( + let after_first_uploads = RemoteIndex::try_parse_descriptions_from_paths( repo_harness.conf, remote_assets .0 @@ -462,7 +491,7 @@ mod tests { 0, ) .await; - assert_index_descriptions(index, after_first_uploads.clone()).await; + assert_index_descriptions(index, &after_first_uploads).await; let checkpoint_with_uploaded_lsn = create_local_timeline( &repo_harness, @@ -478,7 +507,7 @@ mod tests { 0, ) .await; - assert_index_descriptions(index, after_first_uploads.clone()).await; + assert_index_descriptions(index, &after_first_uploads).await; Ok(()) } diff --git a/pageserver/src/repository.rs b/pageserver/src/repository.rs index 4eb352f68d..02334d3229 100644 --- a/pageserver/src/repository.rs +++ b/pageserver/src/repository.rs @@ -1,10 +1,13 @@ -use crate::keyspace::KeyPartitioning; +use crate::layered_repository::metadata::TimelineMetadata; +use crate::remote_storage::RemoteIndex; use crate::walrecord::ZenithWalRecord; use crate::CheckpointConfig; use anyhow::{bail, Result}; +use byteorder::{ByteOrder, BE}; use bytes::Bytes; use serde::{Deserialize, Serialize}; use std::fmt; +use std::fmt::Display; use std::ops::{AddAssign, Range}; use std::sync::{Arc, RwLockReadGuard}; use std::time::Duration; @@ -25,6 +28,8 @@ pub struct Key { pub field6: u32, } +pub const KEY_SIZE: usize = 18; + impl Key { pub fn next(&self) -> Key { self.add(1) @@ -59,7 +64,7 @@ impl Key { key } - pub fn from_array(b: [u8; 18]) -> Self { + pub fn from_slice(b: &[u8]) -> Self { Key { field1: b[0], field2: u32::from_be_bytes(b[1..5].try_into().unwrap()), @@ -69,6 +74,15 @@ impl Key { field6: u32::from_be_bytes(b[14..18].try_into().unwrap()), } } + + pub fn write_to_byte_slice(&self, buf: &mut [u8]) { + buf[0] = self.field1; + BE::write_u32(&mut buf[1..5], self.field2); + BE::write_u32(&mut buf[5..9], self.field3); + BE::write_u32(&mut buf[9..13], self.field4); + buf[13] = self.field5; + BE::write_u32(&mut buf[14..18], self.field6); + } } pub fn key_range_size(key_range: &Range) -> u32 { @@ -141,12 +155,15 @@ impl Key { } } -// -// There are two kinds of values: incremental and non-incremental -// +/// A 'value' stored for a one Key. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Value { + /// An Image value contains a full copy of the value Image(Bytes), + /// A WalRecord value contains a WAL record that needs to be + /// replayed get the full value. Replaying the WAL record + /// might need a previous version of the value (if will_init() + /// returns false), or it may be replayed stand-alone (true). WalRecord(ZenithWalRecord), } @@ -163,32 +180,45 @@ impl Value { } } +#[derive(Clone, Copy, Debug)] +pub enum TimelineSyncStatusUpdate { + Uploaded, + Downloaded, +} + +impl Display for TimelineSyncStatusUpdate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + TimelineSyncStatusUpdate::Uploaded => "Uploaded", + TimelineSyncStatusUpdate::Downloaded => "Downloaded", + }; + f.write_str(s) + } +} /// /// A repository corresponds to one .zenith directory. One repository holds multiple /// timelines, forked off from the same initial call to 'initdb'. pub trait Repository: Send + Sync { type Timeline: Timeline; - fn detach_timeline(&self, timeline_id: ZTimelineId) -> Result<()>; - - /// Updates timeline based on the new sync state, received from the remote storage synchronization. + /// Updates timeline based on the `TimelineSyncStatusUpdate`, received from the remote storage synchronization. /// See [`crate::remote_storage`] for more details about the synchronization. - fn set_timeline_state( + fn apply_timeline_remote_sync_status_update( &self, timeline_id: ZTimelineId, - new_state: TimelineSyncState, + timeline_sync_status_update: TimelineSyncStatusUpdate, ) -> Result<()>; - /// Gets current synchronization state of the timeline. - /// See [`crate::remote_storage`] for more details about the synchronization. - fn get_timeline_state(&self, timeline_id: ZTimelineId) -> Option; - /// Get Timeline handle for given zenith timeline ID. - fn get_timeline(&self, timelineid: ZTimelineId) -> Result>; + /// This function is idempotent. It doesnt change internal state in any way. + fn get_timeline(&self, timelineid: ZTimelineId) -> Option>; + + /// Get Timeline handle for locally available timeline. Load it into memory if it is not loaded. + fn get_timeline_load(&self, timelineid: ZTimelineId) -> Result>; /// Lists timelines the repository contains. /// Up to repository's implementation to omit certain timelines that ar not considered ready for use. - fn list_timelines(&self) -> Result>>; + fn list_timelines(&self) -> Vec<(ZTimelineId, RepositoryTimeline)>; /// Create a new, empty timeline. The caller is responsible for loading data into it /// Initdb lsn is provided for timeline impl to be able to perform checks for some operations against it. @@ -225,69 +255,44 @@ pub trait Repository: Send + Sync { /// perform one compaction iteration. /// this function is periodically called by compactor thread. fn compaction_iteration(&self) -> Result<()>; + + /// detaches locally available timeline by stopping all threads and removing all the data. + fn detach_timeline(&self, timeline_id: ZTimelineId) -> Result<()>; + + // Allows to retrieve remote timeline index from the repo. Used in walreceiver to grab remote consistent lsn. + fn get_remote_index(&self) -> &RemoteIndex; } /// A timeline, that belongs to the current repository. pub enum RepositoryTimeline { /// Timeline, with its files present locally in pageserver's working directory. /// Loaded into pageserver's memory and ready to be used. - Local { id: ZTimelineId, timeline: Arc }, - /// Timeline, found on the pageserver's remote storage, but not yet downloaded locally. - Remote { - id: ZTimelineId, - /// metadata contents of the latest successfully uploaded checkpoint - disk_consistent_lsn: Lsn, + Loaded(Arc), + + /// All the data is available locally, but not loaded into memory, so loading have to be done before actually using the timeline + Unloaded { + // It is ok to keep metadata here, because it is not changed when timeline is unloaded. + // FIXME can s3 sync actually change it? It can change it when timeline is in awaiting download state. + // but we currently do not download something for the timeline once it is local (even if there are new checkpoints) is it correct? + // also it is not that good to keep TimelineMetadata here, because it is layered repo implementation detail + metadata: TimelineMetadata, }, } -impl RepositoryTimeline { - pub fn local_timeline(&self) -> Option> { - if let Self::Local { timeline, .. } = self { - Some(Arc::clone(timeline)) - } else { - None - } - } - - pub fn id(&self) -> ZTimelineId { - match self { - Self::Local { id, .. } => *id, - Self::Remote { id, .. } => *id, - } - } -} - -/// A state of the timeline synchronization with the remote storage. -/// Contains `disk_consistent_lsn` of the corresponding remote timeline (latest checkpoint's disk_consistent_lsn). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum TimelineSyncState { - /// No further downloads from the remote storage are needed. - /// The timeline state is up-to-date or ahead of the remote storage one, - /// ready to be used in any pageserver operation. - Ready(Lsn), - /// Timeline is scheduled for downloading, but its current local state is not up to date with the remote storage. - /// The timeline is not ready to be used in any pageserver operations, otherwise it might diverge its local state from the remote version, - /// making it impossible to sync it further. - AwaitsDownload(Lsn), - /// Timeline was not in the pageserver's local working directory, but was found on the remote storage, ready to be downloaded. - /// Cannot be used in any pageserver operations due to complete absence locally. - CloudOnly(Lsn), - /// Timeline was evicted from the pageserver's local working directory due to conflicting remote and local states or too many errors during the synchronization. - /// Such timelines cannot have their state synchronized further and may not have the data about remote timeline's disk_consistent_lsn, since eviction may happen - /// due to errors before the remote timeline contents is known. - Evicted(Option), +pub enum LocalTimelineState { + // timeline is loaded into memory (with layer map and all the bits), + Loaded, + // timeline is on disk locally and ready to be loaded into memory. + Unloaded, } -impl TimelineSyncState { - pub fn remote_disk_consistent_lsn(&self) -> Option { - Some(match self { - TimelineSyncState::Evicted(None) => return None, - TimelineSyncState::Ready(lsn) => lsn, - TimelineSyncState::AwaitsDownload(lsn) => lsn, - TimelineSyncState::CloudOnly(lsn) => lsn, - TimelineSyncState::Evicted(Some(lsn)) => lsn, - }) - .copied() +impl<'a, T> From<&'a RepositoryTimeline> for LocalTimelineState { + fn from(local_timeline_entry: &'a RepositoryTimeline) -> Self { + match local_timeline_entry { + RepositoryTimeline::Loaded(_) => LocalTimelineState::Loaded, + RepositoryTimeline::Unloaded { .. } => LocalTimelineState::Unloaded, + } } } @@ -378,19 +383,6 @@ pub trait Timeline: Send + Sync { /// know anything about them here in the repository. fn checkpoint(&self, cconf: CheckpointConfig) -> Result<()>; - /// - /// Tell the implementation how the keyspace should be partitioned. - /// - /// FIXME: This is quite a hack. The code in pgdatadir_mapping.rs knows - /// which keys exist and what is the logical grouping of them. That's why - /// the code there (and in keyspace.rs) decides the partitioning, not the - /// layered_repository.rs implementation. That's a layering violation: - /// the Repository implementation ought to be responsible for the physical - /// layout, but currently it's more convenient to do it in pgdatadir_mapping.rs - /// rather than in layered_repository.rs. - /// - fn hint_partitioning(&self, partitioning: KeyPartitioning, lsn: Lsn) -> Result<()>; - /// /// Check that it is valid to request operations with that lsn. fn check_lsn_is_in_scope( @@ -433,7 +425,7 @@ pub mod repo_harness { use crate::RepositoryImpl; use crate::{ config::PageServerConf, - layered_repository::{LayeredRepository, TIMELINES_SEGMENT_NAME}, + layered_repository::LayeredRepository, walredo::{WalRedoError, WalRedoManager}, }; @@ -487,7 +479,6 @@ pub mod repo_harness { let repo_dir = PageServerConf::test_repo_dir(test_name); let _ = fs::remove_dir_all(&repo_dir); fs::create_dir_all(&repo_dir)?; - fs::create_dir_all(&repo_dir.join(TIMELINES_SEGMENT_NAME))?; let conf = PageServerConf::dummy_conf(repo_dir); // Make a static copy of the config. This can never be free'd, but that's @@ -496,6 +487,7 @@ pub mod repo_harness { let tenant_id = ZTenantId::generate(); fs::create_dir_all(conf.tenant_path(&tenant_id))?; + fs::create_dir_all(conf.timelines_path(&tenant_id))?; Ok(Self { conf, @@ -505,9 +497,39 @@ pub mod repo_harness { } pub fn load(&self) -> RepositoryImpl { + self.try_load().expect("failed to load test repo") + } + + pub fn try_load(&self) -> Result { let walredo_mgr = Arc::new(TestRedoManager); - LayeredRepository::new(self.conf, walredo_mgr, self.tenant_id, false) + let repo = LayeredRepository::new( + self.conf, + walredo_mgr, + self.tenant_id, + RemoteIndex::empty(), + false, + ); + // populate repo with locally available timelines + for timeline_dir_entry in fs::read_dir(self.conf.timelines_path(&self.tenant_id)) + .expect("should be able to read timelines dir") + { + let timeline_dir_entry = timeline_dir_entry.unwrap(); + let timeline_id: ZTimelineId = timeline_dir_entry + .path() + .file_name() + .unwrap() + .to_string_lossy() + .parse() + .unwrap(); + + repo.apply_timeline_remote_sync_status_update( + timeline_id, + TimelineSyncStatusUpdate::Downloaded, + )?; + } + + Ok(repo) } pub fn timeline_path(&self, timeline_id: &ZTimelineId) -> PathBuf { @@ -559,7 +581,7 @@ mod tests { use lazy_static::lazy_static; lazy_static! { - static ref TEST_KEY: Key = Key::from_array(hex!("112222222233333333444444445500000001")); + static ref TEST_KEY: Key = Key::from_slice(&hex!("112222222233333333444444445500000001")); } #[test] @@ -620,10 +642,9 @@ mod tests { // Branch the history, modify relation differently on the new timeline repo.branch_timeline(TIMELINE_ID, NEW_TIMELINE_ID, Lsn(0x30))?; - let newtline = match repo.get_timeline(NEW_TIMELINE_ID)?.local_timeline() { - Some(timeline) => timeline, - None => panic!("Should have a local timeline"), - }; + let newtline = repo + .get_timeline_load(NEW_TIMELINE_ID) + .expect("Should have a local timeline"); let new_writer = newtline.writer(); new_writer.put(TEST_KEY_A, Lsn(0x40), test_value("bar at 0x40"))?; new_writer.finish_write(Lsn(0x40)); @@ -769,11 +790,9 @@ mod tests { make_some_layers(tline.as_ref(), Lsn(0x20))?; repo.branch_timeline(TIMELINE_ID, NEW_TIMELINE_ID, Lsn(0x40))?; - let newtline = match repo.get_timeline(NEW_TIMELINE_ID)?.local_timeline() { - Some(timeline) => timeline, - None => panic!("Should have a local timeline"), - }; - + let newtline = repo + .get_timeline_load(NEW_TIMELINE_ID) + .expect("Should have a local timeline"); // this removes layers before lsn 40 (50 minus 10), so there are two remaining layers, image and delta for 31-50 repo.gc_iteration(Some(TIMELINE_ID), 0x10, false)?; assert!(newtline.get(*TEST_KEY, Lsn(0x25)).is_ok()); @@ -787,10 +806,9 @@ mod tests { make_some_layers(tline.as_ref(), Lsn(0x20))?; repo.branch_timeline(TIMELINE_ID, NEW_TIMELINE_ID, Lsn(0x40))?; - let newtline = match repo.get_timeline(NEW_TIMELINE_ID)?.local_timeline() { - Some(timeline) => timeline, - None => panic!("Should have a local timeline"), - }; + let newtline = repo + .get_timeline_load(NEW_TIMELINE_ID) + .expect("Should have a local timeline"); make_some_layers(newtline.as_ref(), Lsn(0x60))?; @@ -805,4 +823,81 @@ mod tests { Ok(()) } + + #[test] + fn timeline_load() -> Result<()> { + const TEST_NAME: &str = "timeline_load"; + let harness = RepoHarness::create(TEST_NAME)?; + { + let repo = harness.load(); + let tline = repo.create_empty_timeline(TIMELINE_ID, Lsn(0x8000))?; + make_some_layers(tline.as_ref(), Lsn(0x8000))?; + tline.checkpoint(CheckpointConfig::Forced)?; + } + + let repo = harness.load(); + let tline = repo + .get_timeline(TIMELINE_ID) + .expect("cannot load timeline"); + assert!(matches!(tline, RepositoryTimeline::Unloaded { .. })); + + assert!(repo.get_timeline_load(TIMELINE_ID).is_ok()); + + let tline = repo + .get_timeline(TIMELINE_ID) + .expect("cannot load timeline"); + assert!(matches!(tline, RepositoryTimeline::Loaded(_))); + + Ok(()) + } + + #[test] + fn timeline_load_with_ancestor() -> Result<()> { + const TEST_NAME: &str = "timeline_load_with_ancestor"; + let harness = RepoHarness::create(TEST_NAME)?; + // create two timelines + { + let repo = harness.load(); + let tline = repo.create_empty_timeline(TIMELINE_ID, Lsn(0))?; + + make_some_layers(tline.as_ref(), Lsn(0x20))?; + tline.checkpoint(CheckpointConfig::Forced)?; + + repo.branch_timeline(TIMELINE_ID, NEW_TIMELINE_ID, Lsn(0x40))?; + + let newtline = repo + .get_timeline_load(NEW_TIMELINE_ID) + .expect("Should have a local timeline"); + + make_some_layers(newtline.as_ref(), Lsn(0x60))?; + tline.checkpoint(CheckpointConfig::Forced)?; + } + + // check that both of them are initially unloaded + let repo = harness.load(); + { + let tline = repo.get_timeline(TIMELINE_ID).expect("cannot get timeline"); + assert!(matches!(tline, RepositoryTimeline::Unloaded { .. })); + + let tline = repo + .get_timeline(NEW_TIMELINE_ID) + .expect("cannot get timeline"); + assert!(matches!(tline, RepositoryTimeline::Unloaded { .. })); + } + // load only child timeline + let _ = repo + .get_timeline_load(NEW_TIMELINE_ID) + .expect("cannot load timeline"); + + // check that both, child and ancestor are loaded + let tline = repo + .get_timeline(NEW_TIMELINE_ID) + .expect("cannot get timeline"); + assert!(matches!(tline, RepositoryTimeline::Loaded(_))); + + let tline = repo.get_timeline(TIMELINE_ID).expect("cannot get timeline"); + assert!(matches!(tline, RepositoryTimeline::Loaded(_))); + + Ok(()) + } } diff --git a/pageserver/src/tenant_mgr.rs b/pageserver/src/tenant_mgr.rs index 38ac1a8bc4..aeff718803 100644 --- a/pageserver/src/tenant_mgr.rs +++ b/pageserver/src/tenant_mgr.rs @@ -3,17 +3,20 @@ use crate::config::PageServerConf; use crate::layered_repository::LayeredRepository; -use crate::repository::Repository; -use crate::repository::TimelineSyncState; +use crate::remote_storage::RemoteIndex; +use crate::repository::{Repository, TimelineSyncStatusUpdate}; use crate::thread_mgr; use crate::thread_mgr::ThreadKind; use crate::timelines; +use crate::timelines::CreateRepo; use crate::walredo::PostgresRedoManager; use crate::{DatadirTimelineImpl, RepositoryImpl}; use anyhow::{Context, Result}; use lazy_static::lazy_static; use log::*; use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; +use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fmt; use std::sync::{Arc, Mutex, MutexGuard}; @@ -60,81 +63,68 @@ fn access_tenants() -> MutexGuard<'static, HashMap> { TENANTS.lock().unwrap() } -/// Updates tenants' repositories, changing their timelines state in memory. -pub fn set_timeline_states( +// Sets up wal redo manager and repository for tenant. Reduces code duplocation. +// Used during pageserver startup, or when new tenant is attached to pageserver. +pub fn load_local_repo( conf: &'static PageServerConf, - timeline_states: HashMap>, -) { - if timeline_states.is_empty() { - debug!("no timeline state updates to perform"); - return; - } - - info!("Updating states for {} timelines", timeline_states.len()); - trace!("States: {:?}", timeline_states); - + tenant_id: ZTenantId, + remote_index: &RemoteIndex, +) -> Arc { let mut m = access_tenants(); - for (tenant_id, timeline_states) in timeline_states { - let tenant = m.entry(tenant_id).or_insert_with(|| { - // TODO (rodionov) reuse one of the initialisation routines - // Set up a WAL redo manager, for applying WAL records. - let walredo_mgr = PostgresRedoManager::new(conf, tenant_id); + let tenant = m.entry(tenant_id).or_insert_with(|| { + // Set up a WAL redo manager, for applying WAL records. + let walredo_mgr = PostgresRedoManager::new(conf, tenant_id); - // Set up an object repository, for actual data storage. - let repo = LayeredRepository::new( - conf, - Arc::new(walredo_mgr), - tenant_id, - conf.remote_storage_config.is_some(), - ); - - Tenant { - state: TenantState::Idle, - repo: Arc::new(repo), - timelines: HashMap::new(), - } - }); - if let Err(e) = put_timelines_into_tenant(tenant, tenant_id, timeline_states) { - error!( - "Failed to update timeline states for tenant {}: {:?}", - tenant_id, e - ); + // Set up an object repository, for actual data storage. + let repo: Arc = Arc::new(LayeredRepository::new( + conf, + Arc::new(walredo_mgr), + tenant_id, + remote_index.clone(), + conf.remote_storage_config.is_some(), + )); + Tenant { + state: TenantState::Idle, + repo, + timelines: HashMap::new(), } - } + }); + Arc::clone(&tenant.repo) } -fn put_timelines_into_tenant( - tenant: &mut Tenant, - tenant_id: ZTenantId, - timeline_states: HashMap, -) -> anyhow::Result<()> { - for (timeline_id, timeline_state) in timeline_states { - // If the timeline is being put into any other state than Ready, - // stop any threads operating on it. - // - // FIXME: This is racy. A page service thread could just get - // handle on the Timeline, before we call set_timeline_state() - if !matches!(timeline_state, TimelineSyncState::Ready(_)) { - thread_mgr::shutdown_threads(None, Some(tenant_id), Some(timeline_id)); - - // Should we run a final checkpoint to flush all the data to - // disk? Doesn't seem necessary; all of the states other than - // Ready imply that the data on local disk is corrupt or incomplete, - // and we don't want to flush that to disk. - } - - tenant - .repo - .set_timeline_state(timeline_id, timeline_state) - .with_context(|| { - format!( - "Failed to update timeline {} state to {:?}", - timeline_id, timeline_state - ) - })?; +/// Updates tenants' repositories, changing their timelines state in memory. +pub fn apply_timeline_sync_status_updates( + conf: &'static PageServerConf, + remote_index: RemoteIndex, + sync_status_updates: HashMap>, +) { + if sync_status_updates.is_empty() { + debug!("no sync status updates to apply"); + return; } + info!( + "Applying sync status updates for {} timelines", + sync_status_updates.len() + ); + trace!("Sync status updates: {:?}", sync_status_updates); - Ok(()) + for (tenant_id, tenant_timelines_sync_status_updates) in sync_status_updates { + let repo = load_local_repo(conf, tenant_id, &remote_index); + + for (timeline_id, timeline_sync_status_update) in tenant_timelines_sync_status_updates { + match repo.apply_timeline_remote_sync_status_update(timeline_id, timeline_sync_status_update) + { + Ok(_) => debug!( + "successfully applied timeline sync status update: {} -> {}", + timeline_id, timeline_sync_status_update + ), + Err(e) => error!( + "Failed to apply timeline sync status update for tenant {}. timeline {} update {} Error: {:#}", + tenant_id, timeline_id, timeline_sync_status_update, e + ), + } + } + } } /// @@ -184,25 +174,31 @@ pub fn shutdown_all_tenants() { pub fn create_tenant_repository( conf: &'static PageServerConf, - new_tenant_id: Option, + tenantid: ZTenantId, + remote_index: RemoteIndex, ) -> Result> { - let new_tenant_id = new_tenant_id.unwrap_or_else(ZTenantId::generate); - let wal_redo_manager = Arc::new(PostgresRedoManager::new(conf, new_tenant_id)); - match timelines::create_repo(conf, new_tenant_id, wal_redo_manager)? { - Some(repo) => { - access_tenants() - .entry(new_tenant_id) - .or_insert_with(|| Tenant { - state: TenantState::Idle, - repo, - timelines: HashMap::new(), - }); - Ok(Some(new_tenant_id)) - } - None => { - debug!("repository already exists for tenant {}", new_tenant_id); + match access_tenants().entry(tenantid) { + Entry::Occupied(_) => { + debug!("tenant {} already exists", tenantid); Ok(None) } + Entry::Vacant(v) => { + let wal_redo_manager = Arc::new(PostgresRedoManager::new(conf, tenantid)); + let repo = timelines::create_repo( + conf, + tenantid, + CreateRepo::Real { + wal_redo_manager, + remote_index, + }, + )?; + v.insert(Tenant { + state: TenantState::Idle, + repo, + timelines: HashMap::new(), + }); + Ok(Some(tenantid)) + } } } @@ -214,13 +210,13 @@ pub fn get_tenant_state(tenantid: ZTenantId) -> Option { /// Change the state of a tenant to Active and launch its compactor and GC /// threads. If the tenant was already in Active state or Stopping, does nothing. /// -pub fn activate_tenant(conf: &'static PageServerConf, tenantid: ZTenantId) -> Result<()> { +pub fn activate_tenant(conf: &'static PageServerConf, tenant_id: ZTenantId) -> Result<()> { let mut m = access_tenants(); let tenant = m - .get_mut(&tenantid) - .with_context(|| format!("Tenant not found for id {}", tenantid))?; + .get_mut(&tenant_id) + .with_context(|| format!("Tenant not found for id {}", tenant_id))?; - info!("activating tenant {}", tenantid); + info!("activating tenant {}", tenant_id); match tenant.state { // If the tenant is already active, nothing to do. @@ -230,22 +226,31 @@ pub fn activate_tenant(conf: &'static PageServerConf, tenantid: ZTenantId) -> Re TenantState::Idle => { thread_mgr::spawn( ThreadKind::Compactor, - Some(tenantid), + Some(tenant_id), None, "Compactor thread", - move || crate::tenant_threads::compact_loop(tenantid, conf), + true, + move || crate::tenant_threads::compact_loop(tenant_id, conf), )?; - // FIXME: if we fail to launch the GC thread, but already launched the - // compactor, we're in a strange state. - - thread_mgr::spawn( + let gc_spawn_result = thread_mgr::spawn( ThreadKind::GarbageCollector, - Some(tenantid), + Some(tenant_id), None, "GC thread", - move || crate::tenant_threads::gc_loop(tenantid, conf), - )?; + true, + move || crate::tenant_threads::gc_loop(tenant_id, conf), + ) + .with_context(|| format!("Failed to launch GC thread for tenant {}", tenant_id)); + + if let Err(e) = &gc_spawn_result { + error!( + "Failed to start GC thread for tenant {}, stopping its checkpointer thread: {:?}", + tenant_id, e + ); + thread_mgr::shutdown_threads(Some(ThreadKind::Compactor), Some(tenant_id), None); + return gc_spawn_result; + } tenant.state = TenantState::Active; } @@ -261,19 +266,20 @@ pub fn get_repository_for_tenant(tenantid: ZTenantId) -> Result Result> { let mut m = access_tenants(); let tenant = m .get_mut(&tenantid) - .with_context(|| format!("Tenant not found for tenant {}", tenantid))?; + .with_context(|| format!("Tenant {} not found", tenantid))?; if let Some(page_tline) = tenant.timelines.get(&timelineid) { return Ok(Arc::clone(page_tline)); @@ -281,9 +287,8 @@ pub fn get_timeline_for_tenant( // First access to this timeline. Create a DatadirTimeline wrapper for it let tline = tenant .repo - .get_timeline(timelineid)? - .local_timeline() - .with_context(|| format!("cannot fetch timeline {}", timelineid))?; + .get_timeline_load(timelineid) + .with_context(|| format!("Timeline {} not found for tenant {}", timelineid, tenantid))?; let repartition_distance = tenant.repo.conf.checkpoint_distance / 10; @@ -293,9 +298,10 @@ pub fn get_timeline_for_tenant( Ok(page_tline) } +#[serde_as] #[derive(Serialize, Deserialize, Clone)] pub struct TenantInfo { - #[serde(with = "hex")] + #[serde_as(as = "DisplayFromStr")] pub id: ZTenantId, pub state: TenantState, } diff --git a/pageserver/src/tenant_threads.rs b/pageserver/src/tenant_threads.rs index 6c2ba479db..0d9a94cc5b 100644 --- a/pageserver/src/tenant_threads.rs +++ b/pageserver/src/tenant_threads.rs @@ -57,7 +57,7 @@ pub fn gc_loop(tenantid: ZTenantId, conf: &'static PageServerConf) -> Result<()> // Garbage collect old files that are not needed for PITR anymore if conf.gc_horizon > 0 { let repo = tenant_mgr::get_repository_for_tenant(tenantid)?; - repo.gc_iteration(None, conf.gc_horizon, false).unwrap(); + repo.gc_iteration(None, conf.gc_horizon, false)?; } // TODO Write it in more adequate way using diff --git a/pageserver/src/thread_mgr.rs b/pageserver/src/thread_mgr.rs index 7a4e82ad86..4484bb1db1 100644 --- a/pageserver/src/thread_mgr.rs +++ b/pageserver/src/thread_mgr.rs @@ -43,12 +43,14 @@ use std::thread::JoinHandle; use tokio::sync::watch; -use tracing::{info, warn}; +use tracing::{debug, error, info, warn}; use lazy_static::lazy_static; use zenith_utils::zid::{ZTenantId, ZTimelineId}; +use crate::shutdown_pageserver; + lazy_static! { /// Each thread that we track is associated with a "thread ID". It's just /// an increasing number that we assign, not related to any system thread @@ -101,7 +103,7 @@ pub enum ThreadKind { // Thread that flushes frozen in-memory layers to disk LayerFlushThread, - // Thread for synchronizing pageserver relish data with the remote storage. + // Thread for synchronizing pageserver layer files with the remote storage. // Shared by all tenants. StorageSync, } @@ -128,15 +130,16 @@ struct PageServerThread { } /// Launch a new thread -pub fn spawn( +pub fn spawn( kind: ThreadKind, tenant_id: Option, timeline_id: Option, name: &str, + fail_on_error: bool, f: F, ) -> std::io::Result<()> where - F: FnOnce() -> Result<(), E> + Send + 'static, + F: FnOnce() -> anyhow::Result<()> + Send + 'static, { let (shutdown_tx, shutdown_rx) = watch::channel(()); let thread_id = NEXT_THREAD_ID.fetch_add(1, Ordering::Relaxed); @@ -163,12 +166,22 @@ where .insert(thread_id, Arc::clone(&thread_rc)); let thread_rc2 = Arc::clone(&thread_rc); + let thread_name = name.to_string(); let join_handle = match thread::Builder::new() .name(name.to_string()) - .spawn(move || thread_wrapper(thread_id, thread_rc2, shutdown_rx, f)) - { + .spawn(move || { + thread_wrapper( + thread_name, + thread_id, + thread_rc2, + shutdown_rx, + fail_on_error, + f, + ) + }) { Ok(handle) => handle, Err(err) => { + error!("Failed to spawn thread '{}': {}", name, err); // Could not spawn the thread. Remove the entry THREADS.lock().unwrap().remove(&thread_id); return Err(err); @@ -183,13 +196,15 @@ where /// This wrapper function runs in a newly-spawned thread. It initializes the /// thread-local variables and calls the payload function -fn thread_wrapper( +fn thread_wrapper( + thread_name: String, thread_id: u64, thread: Arc, shutdown_rx: watch::Receiver<()>, + fail_on_error: bool, f: F, ) where - F: FnOnce() -> Result<(), E> + Send + 'static, + F: FnOnce() -> anyhow::Result<()> + Send + 'static, { SHUTDOWN_RX.with(|rx| { *rx.borrow_mut() = Some(shutdown_rx); @@ -198,6 +213,8 @@ fn thread_wrapper( *ct.borrow_mut() = Some(thread); }); + debug!("Starting thread '{}'", thread_name); + // We use AssertUnwindSafe here so that the payload function // doesn't need to be UnwindSafe. We don't do anything after the // unwinding that would expose us to unwind-unsafe behavior. @@ -206,9 +223,26 @@ fn thread_wrapper( // Remove our entry from the global hashmap. THREADS.lock().unwrap().remove(&thread_id); - // If the thread payload panic'd, exit with the panic. - if let Err(err) = result { - panic::resume_unwind(err); + match result { + Ok(Ok(())) => debug!("Thread '{}' exited normally", thread_name), + Ok(Err(err)) => { + if fail_on_error { + error!( + "Shutting down: thread '{}' exited with error: {:?}", + thread_name, err + ); + shutdown_pageserver(); + } else { + error!("Thread '{}' exited with error: {:?}", thread_name, err); + } + } + Err(err) => { + error!( + "Shutting down: thread '{}' panicked: {:?}", + thread_name, err + ); + shutdown_pageserver(); + } } } @@ -253,7 +287,7 @@ pub fn shutdown_threads( let _ = join_handle.join(); } else { // The thread had not even fully started yet. Or it was shut down - // concurrently and alrady exited + // concurrently and already exited } } } diff --git a/pageserver/src/timelines.rs b/pageserver/src/timelines.rs index 8ecb5f8e69..105c3c869f 100644 --- a/pageserver/src/timelines.rs +++ b/pageserver/src/timelines.rs @@ -2,8 +2,10 @@ //! Timeline management code // -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{bail, Context, Result}; use postgres_ffi::ControlFileData; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; use std::{ fs, path::Path, @@ -16,110 +18,116 @@ use zenith_utils::lsn::Lsn; use zenith_utils::zid::{ZTenantId, ZTimelineId}; use zenith_utils::{crashsafe_dir, logging}; -use crate::DatadirTimeline; -use crate::RepositoryImpl; -use crate::{config::PageServerConf, repository::Repository}; +use crate::{ + config::PageServerConf, + layered_repository::metadata::TimelineMetadata, + remote_storage::RemoteIndex, + repository::{LocalTimelineState, Repository}, + DatadirTimeline, RepositoryImpl, +}; use crate::{import_datadir, LOG_FILE_NAME}; use crate::{layered_repository::LayeredRepository, walredo::WalRedoManager}; use crate::{repository::RepositoryTimeline, tenant_mgr}; use crate::{repository::Timeline, CheckpointConfig}; -#[derive(Clone)] -pub enum TimelineInfo { - Local { - timeline_id: ZTimelineId, - tenant_id: ZTenantId, - last_record_lsn: Lsn, - prev_record_lsn: Lsn, - ancestor_timeline_id: Option, - ancestor_lsn: Option, - disk_consistent_lsn: Lsn, - current_logical_size: usize, - current_logical_size_non_incremental: Option, - }, - Remote { - timeline_id: ZTimelineId, - tenant_id: ZTenantId, - disk_consistent_lsn: Lsn, - }, +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LocalTimelineInfo { + #[serde_as(as = "Option")] + pub ancestor_timeline_id: Option, + #[serde_as(as = "Option")] + pub ancestor_lsn: Option, + #[serde_as(as = "DisplayFromStr")] + pub last_record_lsn: Lsn, + #[serde_as(as = "Option")] + pub prev_record_lsn: Option, + #[serde_as(as = "DisplayFromStr")] + pub disk_consistent_lsn: Lsn, + pub current_logical_size: Option, // is None when timeline is Unloaded + pub current_logical_size_non_incremental: Option, + pub timeline_state: LocalTimelineState, } -impl TimelineInfo { - pub fn from_ids( - tenant_id: ZTenantId, - timeline_id: ZTimelineId, +impl LocalTimelineInfo { + pub fn from_loaded_timeline( + datadir_tline: &DatadirTimeline, include_non_incremental_logical_size: bool, - ) -> Result { - let repo = tenant_mgr::get_repository_for_tenant(tenant_id)?; - let result = match repo.get_timeline(timeline_id)? { - RepositoryTimeline::Local { id, timeline } => { - let ancestor_timeline_id = timeline.get_ancestor_timeline_id(); - let ancestor_lsn = if ancestor_timeline_id.is_some() { - Some(timeline.get_ancestor_lsn()) - } else { - None - }; - - let tline = tenant_mgr::get_timeline_for_tenant(tenant_id, timeline_id)?; - let current_logical_size = tline.get_current_logical_size(); - let current_logical_size_non_incremental = get_current_logical_size_non_incremental( - include_non_incremental_logical_size, - tline.as_ref(), - ); - - Self::Local { - timeline_id: id, - tenant_id, - last_record_lsn: timeline.get_last_record_lsn(), - prev_record_lsn: timeline.get_prev_record_lsn(), - ancestor_timeline_id, - ancestor_lsn, - disk_consistent_lsn: timeline.get_disk_consistent_lsn(), - current_logical_size, - current_logical_size_non_incremental, + ) -> anyhow::Result { + let last_record_lsn = datadir_tline.tline.get_last_record_lsn(); + let info = LocalTimelineInfo { + ancestor_timeline_id: datadir_tline.tline.get_ancestor_timeline_id(), + ancestor_lsn: { + match datadir_tline.tline.get_ancestor_lsn() { + Lsn(0) => None, + lsn @ Lsn(_) => Some(lsn), } - } - RepositoryTimeline::Remote { - id, - disk_consistent_lsn, - } => Self::Remote { - timeline_id: id, - tenant_id, - disk_consistent_lsn, + }, + disk_consistent_lsn: datadir_tline.tline.get_disk_consistent_lsn(), + last_record_lsn, + prev_record_lsn: Some(datadir_tline.tline.get_prev_record_lsn()), + timeline_state: LocalTimelineState::Loaded, + current_logical_size: Some(datadir_tline.get_current_logical_size()), + current_logical_size_non_incremental: if include_non_incremental_logical_size { + Some(datadir_tline.get_current_logical_size_non_incremental(last_record_lsn)?) + } else { + None }, }; - Ok(result) + Ok(info) } - pub fn timeline_id(&self) -> ZTimelineId { - match *self { - TimelineInfo::Local { timeline_id, .. } => timeline_id, - TimelineInfo::Remote { timeline_id, .. } => timeline_id, + pub fn from_unloaded_timeline(metadata: &TimelineMetadata) -> Self { + LocalTimelineInfo { + ancestor_timeline_id: metadata.ancestor_timeline(), + ancestor_lsn: { + match metadata.ancestor_lsn() { + Lsn(0) => None, + lsn @ Lsn(_) => Some(lsn), + } + }, + disk_consistent_lsn: metadata.disk_consistent_lsn(), + last_record_lsn: metadata.disk_consistent_lsn(), + prev_record_lsn: metadata.prev_record_lsn(), + timeline_state: LocalTimelineState::Unloaded, + current_logical_size: None, + current_logical_size_non_incremental: None, } } - pub fn tenant_id(&self) -> ZTenantId { - match *self { - TimelineInfo::Local { tenant_id, .. } => tenant_id, - TimelineInfo::Remote { tenant_id, .. } => tenant_id, + pub fn from_repo_timeline( + tenant_id: ZTenantId, + timeline_id: ZTimelineId, + repo_timeline: &RepositoryTimeline, + include_non_incremental_logical_size: bool, + ) -> anyhow::Result { + match repo_timeline { + RepositoryTimeline::Loaded(_) => { + let datadir_tline = + tenant_mgr::get_timeline_for_tenant_load(tenant_id, timeline_id)?; + Self::from_loaded_timeline(&datadir_tline, include_non_incremental_logical_size) + } + RepositoryTimeline::Unloaded { metadata } => Ok(Self::from_unloaded_timeline(metadata)), } } } -fn get_current_logical_size_non_incremental( - include_non_incremental_logical_size: bool, - timeline: &DatadirTimeline, -) -> Option { - if !include_non_incremental_logical_size { - return None; - } - match timeline.get_current_logical_size_non_incremental(timeline.get_last_record_lsn()) { - Ok(size) => Some(size), - Err(e) => { - error!("Failed to get non-incremental logical size: {:?}", e); - None - } - } +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RemoteTimelineInfo { + #[serde_as(as = "Option")] + pub remote_consistent_lsn: Option, + pub awaits_download: bool, +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TimelineInfo { + #[serde_as(as = "DisplayFromStr")] + pub tenant_id: ZTenantId, + #[serde_as(as = "DisplayFromStr")] + pub timeline_id: ZTimelineId, + pub local: Option, + pub remote: Option, } #[derive(Debug, Clone, Copy)] @@ -137,25 +145,12 @@ pub fn init_pageserver( // use true as daemonize parameter because otherwise we pollute zenith cli output with a few pages long output of info messages let _log_file = logging::init(LOG_FILE_NAME, true)?; - // We don't use the real WAL redo manager, because we don't want to spawn the WAL redo - // process during repository initialization. - // - // FIXME: That caused trouble, because the WAL redo manager spawned a thread that launched - // initdb in the background, and it kept running even after the "zenith init" had exited. - // In tests, we started the page server immediately after that, so that initdb was still - // running in the background, and we failed to run initdb again in the same directory. This - // has been solved for the rapid init+start case now, but the general race condition remains - // if you restart the server quickly. The WAL redo manager doesn't use a separate thread - // anymore, but I think that could still happen. - let dummy_redo_mgr = Arc::new(crate::walredo::DummyRedoManager {}); - crashsafe_dir::create_dir_all(conf.tenants_path())?; if let Some(tenant_id) = create_tenant { println!("initializing tenantid {}", tenant_id); - let repo = create_repo(conf, tenant_id, dummy_redo_mgr) - .context("failed to create repo")? - .ok_or_else(|| anyhow!("For newely created pageserver, found already existing repository for tenant {}", tenant_id))?; + let repo = + create_repo(conf, tenant_id, CreateRepo::Dummy).context("failed to create repo")?; let new_timeline_id = initial_timeline_id.unwrap_or_else(ZTimelineId::generate); bootstrap_timeline(conf, tenant_id, new_timeline_id, repo.as_ref()) .context("failed to create initial timeline")?; @@ -168,15 +163,44 @@ pub fn init_pageserver( Ok(()) } +pub enum CreateRepo { + Real { + wal_redo_manager: Arc, + remote_index: RemoteIndex, + }, + Dummy, +} + pub fn create_repo( conf: &'static PageServerConf, tenant_id: ZTenantId, - wal_redo_manager: Arc, -) -> Result>> { + create_repo: CreateRepo, +) -> Result> { + let (wal_redo_manager, remote_index) = match create_repo { + CreateRepo::Real { + wal_redo_manager, + remote_index, + } => (wal_redo_manager, remote_index), + CreateRepo::Dummy => { + // We don't use the real WAL redo manager, because we don't want to spawn the WAL redo + // process during repository initialization. + // + // FIXME: That caused trouble, because the WAL redo manager spawned a thread that launched + // initdb in the background, and it kept running even after the "zenith init" had exited. + // In tests, we started the page server immediately after that, so that initdb was still + // running in the background, and we failed to run initdb again in the same directory. This + // has been solved for the rapid init+start case now, but the general race condition remains + // if you restart the server quickly. The WAL redo manager doesn't use a separate thread + // anymore, but I think that could still happen. + let wal_redo_manager = Arc::new(crate::walredo::DummyRedoManager {}); + + (wal_redo_manager as _, RemoteIndex::empty()) + } + }; + let repo_dir = conf.tenant_path(&tenant_id); if repo_dir.exists() { - debug!("repo for {} already exists", tenant_id); - return Ok(None); + bail!("tenant {} directory already exists", tenant_id); } // top-level dir may exist if we are creating it through CLI @@ -185,12 +209,13 @@ pub fn create_repo( crashsafe_dir::create_dir(conf.timelines_path(&tenant_id))?; info!("created directory structure in {}", repo_dir.display()); - Ok(Some(Arc::new(LayeredRepository::new( + Ok(Arc::new(LayeredRepository::new( conf, wal_redo_manager, tenant_id, + remote_index, conf.remote_storage_config.is_some(), - )))) + ))) } // Returns checkpoint LSN from controlfile @@ -211,7 +236,7 @@ fn run_initdb(conf: &'static PageServerConf, initdbpath: &Path) -> Result<()> { let initdb_path = conf.pg_bin_dir().join("initdb"); let initdb_output = Command::new(initdb_path) - .args(&["-D", initdbpath.to_str().unwrap()]) + .args(&["-D", &initdbpath.to_string_lossy()]) .args(&["-U", &conf.superuser]) .args(&["-E", "utf8"]) .arg("--no-instructions") @@ -219,8 +244,8 @@ fn run_initdb(conf: &'static PageServerConf, initdbpath: &Path) -> Result<()> { // so no need to fsync it .arg("--no-sync") .env_clear() - .env("LD_LIBRARY_PATH", conf.pg_lib_dir().to_str().unwrap()) - .env("DYLD_LIBRARY_PATH", conf.pg_lib_dir().to_str().unwrap()) + .env("LD_LIBRARY_PATH", conf.pg_lib_dir()) + .env("DYLD_LIBRARY_PATH", conf.pg_lib_dir()) .stdout(Stdio::null()) .output() .context("failed to execute initdb")?; @@ -275,33 +300,27 @@ fn bootstrap_timeline( Ok(()) } -pub(crate) fn get_timelines( +pub(crate) fn get_local_timelines( tenant_id: ZTenantId, include_non_incremental_logical_size: bool, -) -> Result> { +) -> Result> { let repo = tenant_mgr::get_repository_for_tenant(tenant_id) .with_context(|| format!("Failed to get repo for tenant {}", tenant_id))?; + let repo_timelines = repo.list_timelines(); - let mut result = Vec::new(); - for timeline in repo - .list_timelines() - .with_context(|| format!("Failed to list timelines for tenant {}", tenant_id))? - { - match timeline { - RepositoryTimeline::Local { - timeline: _, - id: timeline_id, - } => { - result.push(TimelineInfo::from_ids( - tenant_id, - timeline_id, - include_non_incremental_logical_size, - )?); - } - RepositoryTimeline::Remote { .. } => continue, - } + let mut local_timeline_info = Vec::with_capacity(repo_timelines.len()); + for (timeline_id, repository_timeline) in repo_timelines { + local_timeline_info.push(( + timeline_id, + LocalTimelineInfo::from_repo_timeline( + tenant_id, + timeline_id, + &repository_timeline, + include_non_incremental_logical_size, + )?, + )) } - Ok(result) + Ok(local_timeline_info) } pub(crate) fn create_timeline( @@ -315,32 +334,17 @@ pub(crate) fn create_timeline( let repo = tenant_mgr::get_repository_for_tenant(tenant_id)?; if conf.timeline_path(&new_timeline_id, &tenant_id).exists() { - match repo.get_timeline(new_timeline_id)? { - RepositoryTimeline::Local { id, .. } => { - debug!("timeline {} already exists", id); - return Ok(None); - } - RepositoryTimeline::Remote { id, .. } => bail!( - "timeline {} already exists in pageserver's remote storage", - id - ), - } + debug!("timeline {} already exists", new_timeline_id); + return Ok(None); } let mut start_lsn = ancestor_start_lsn.unwrap_or(Lsn(0)); - match ancestor_timeline_id { + let new_timeline_info = match ancestor_timeline_id { Some(ancestor_timeline_id) => { let ancestor_timeline = repo - .get_timeline(ancestor_timeline_id) - .with_context(|| format!("Cannot get ancestor timeline {}", ancestor_timeline_id))? - .local_timeline() - .with_context(|| { - format!( - "Cannot branch off the timeline {} that's not present locally", - ancestor_timeline_id - ) - })?; + .get_timeline_load(ancestor_timeline_id) + .context("Cannot branch off the timeline that's not present locally")?; if start_lsn == Lsn(0) { // Find end of WAL on the old timeline @@ -369,13 +373,25 @@ pub(crate) fn create_timeline( ); } repo.branch_timeline(ancestor_timeline_id, new_timeline_id, start_lsn)?; + // load the timeline into memory + let loaded_timeline = + tenant_mgr::get_timeline_for_tenant_load(tenant_id, new_timeline_id)?; + LocalTimelineInfo::from_loaded_timeline(&loaded_timeline, false) + .context("cannot fill timeline info")? } None => { bootstrap_timeline(conf, tenant_id, new_timeline_id, repo.as_ref())?; + // load the timeline into memory + let new_timeline = + tenant_mgr::get_timeline_for_tenant_load(tenant_id, new_timeline_id)?; + LocalTimelineInfo::from_loaded_timeline(&new_timeline, false) + .context("cannot fill timeline info")? } - } - - let new_timeline_info = TimelineInfo::from_ids(tenant_id, new_timeline_id, false)?; - - Ok(Some(new_timeline_info)) + }; + Ok(Some(TimelineInfo { + tenant_id, + timeline_id: new_timeline_id, + local: Some(new_timeline_info), + remote: None, + })) } diff --git a/pageserver/src/virtual_file.rs b/pageserver/src/virtual_file.rs index 73671dcf4e..64f9db2338 100644 --- a/pageserver/src/virtual_file.rs +++ b/pageserver/src/virtual_file.rs @@ -65,6 +65,7 @@ lazy_static! { /// currently open, the 'handle' can still point to the slot where it was last kept. The /// 'tag' field is used to detect whether the handle still is valid or not. /// +#[derive(Debug)] pub struct VirtualFile { /// Lazy handle to the global file descriptor cache. The slot that this points to /// might contain our File, or it may be empty, or it may contain a File that @@ -88,7 +89,7 @@ pub struct VirtualFile { timelineid: String, } -#[derive(PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Clone, Copy)] struct SlotHandle { /// Index into OPEN_FILES.slots index: usize, @@ -226,7 +227,8 @@ impl VirtualFile { path: &Path, open_options: &OpenOptions, ) -> Result { - let parts = path.to_str().unwrap().split('/').collect::>(); + let path_str = path.to_string_lossy(); + let parts = path_str.split('/').collect::>(); let tenantid; let timelineid; if parts.len() > 5 && parts[parts.len() - 5] == "tenants" { diff --git a/pageserver/src/walingest.rs b/pageserver/src/walingest.rs index 4b9d73a2b7..98f93d9692 100644 --- a/pageserver/src/walingest.rs +++ b/pageserver/src/walingest.rs @@ -21,7 +21,6 @@ //! redo Postgres process, but some records it can handle directly with //! bespoken Rust code. -use chrono::format::format; use postgres_ffi::nonrelfile_utils::clogpage_precedes; use postgres_ffi::nonrelfile_utils::slru_may_delete_clogsegment; @@ -32,9 +31,8 @@ use tracing::*; use std::collections::HashMap; use crate::pgdatadir_mapping::*; -use crate::relish::*; +use crate::reltag::{RelTag, SlruKind}; use crate::repository::Repository; -use crate::wal_metadata::WalEntryMetadata; use crate::walrecord::*; use postgres_ffi::nonrelfile_utils::mx_offset_to_member_segment; use postgres_ffi::xlog_utils::*; @@ -82,7 +80,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { recdata: Bytes, lsn: Lsn, ) -> Result<()> { - let mut writer = timeline.begin_record(lsn); + let mut modification = timeline.begin_modification(lsn); let recdata_len = recdata.len(); let mut decoded = decode_wal_record(recdata); @@ -99,7 +97,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { if decoded.xl_rmid == pg_constants::RM_HEAP_ID || decoded.xl_rmid == pg_constants::RM_HEAP2_ID { - self.ingest_heapam_record(&mut buf, &mut writer, &mut decoded)?; + self.ingest_heapam_record(&mut buf, &mut modification, &mut decoded)?; } // Handle other special record types if decoded.xl_rmid == pg_constants::RM_SMGR_ID @@ -107,26 +105,26 @@ impl<'a, R: Repository> WalIngest<'a, R> { == pg_constants::XLOG_SMGR_CREATE { let create = XlSmgrCreate::decode(&mut buf); - self.ingest_xlog_smgr_create(&mut writer, &create)?; + self.ingest_xlog_smgr_create(&mut modification, &create)?; } else if decoded.xl_rmid == pg_constants::RM_SMGR_ID && (decoded.xl_info & pg_constants::XLR_RMGR_INFO_MASK) == pg_constants::XLOG_SMGR_TRUNCATE { let truncate = XlSmgrTruncate::decode(&mut buf); - self.ingest_xlog_smgr_truncate(&mut writer, &truncate)?; + self.ingest_xlog_smgr_truncate(&mut modification, &truncate)?; } else if decoded.xl_rmid == pg_constants::RM_DBASE_ID { if (decoded.xl_info & pg_constants::XLR_RMGR_INFO_MASK) == pg_constants::XLOG_DBASE_CREATE { let createdb = XlCreateDatabase::decode(&mut buf); - self.ingest_xlog_dbase_create(&mut writer, &createdb)?; + self.ingest_xlog_dbase_create(&mut modification, &createdb)?; } else if (decoded.xl_info & pg_constants::XLR_RMGR_INFO_MASK) == pg_constants::XLOG_DBASE_DROP { let dropdb = XlDropDatabase::decode(&mut buf); for tablespace_id in dropdb.tablespace_ids { trace!("Drop db {}, {}", tablespace_id, dropdb.db_id); - writer.drop_dbdir(tablespace_id, dropdb.db_id)?; + modification.drop_dbdir(tablespace_id, dropdb.db_id)?; } } } else if decoded.xl_rmid == pg_constants::RM_TBLSPC_ID { @@ -138,7 +136,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { let segno = pageno / pg_constants::SLRU_PAGES_PER_SEGMENT; let rpageno = pageno % pg_constants::SLRU_PAGES_PER_SEGMENT; self.put_slru_page_image( - &mut writer, + &mut modification, SlruKind::Clog, segno, rpageno, @@ -147,7 +145,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { } else { assert!(info == pg_constants::CLOG_TRUNCATE); let xlrec = XlClogTruncate::decode(&mut buf); - self.ingest_clog_truncate_record(&mut writer, &xlrec)?; + self.ingest_clog_truncate_record(&mut modification, &xlrec)?; } } else if decoded.xl_rmid == pg_constants::RM_XACT_ID { let info = decoded.xl_info & pg_constants::XLOG_XACT_OPMASK; @@ -155,7 +153,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { let parsed_xact = XlXactParsedRecord::decode(&mut buf, decoded.xl_xid, decoded.xl_info); self.ingest_xact_record( - &mut writer, + &mut modification, &parsed_xact, info == pg_constants::XLOG_XACT_COMMIT, )?; @@ -165,7 +163,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { let parsed_xact = XlXactParsedRecord::decode(&mut buf, decoded.xl_xid, decoded.xl_info); self.ingest_xact_record( - &mut writer, + &mut modification, &parsed_xact, info == pg_constants::XLOG_XACT_COMMIT_PREPARED, )?; @@ -176,9 +174,9 @@ impl<'a, R: Repository> WalIngest<'a, R> { parsed_xact.xid, lsn, ); - writer.drop_twophase_file(parsed_xact.xid)?; + modification.drop_twophase_file(parsed_xact.xid)?; } else if info == pg_constants::XLOG_XACT_PREPARE { - writer.put_twophase_file(decoded.xl_xid, Bytes::copy_from_slice(&buf[..]))?; + modification.put_twophase_file(decoded.xl_xid, Bytes::copy_from_slice(&buf[..]))?; } } else if decoded.xl_rmid == pg_constants::RM_MULTIXACT_ID { let info = decoded.xl_info & pg_constants::XLR_RMGR_INFO_MASK; @@ -188,7 +186,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { let segno = pageno / pg_constants::SLRU_PAGES_PER_SEGMENT; let rpageno = pageno % pg_constants::SLRU_PAGES_PER_SEGMENT; self.put_slru_page_image( - &mut writer, + &mut modification, SlruKind::MultiXactOffsets, segno, rpageno, @@ -199,7 +197,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { let segno = pageno / pg_constants::SLRU_PAGES_PER_SEGMENT; let rpageno = pageno % pg_constants::SLRU_PAGES_PER_SEGMENT; self.put_slru_page_image( - &mut writer, + &mut modification, SlruKind::MultiXactMembers, segno, rpageno, @@ -207,14 +205,14 @@ impl<'a, R: Repository> WalIngest<'a, R> { )?; } else if info == pg_constants::XLOG_MULTIXACT_CREATE_ID { let xlrec = XlMultiXactCreate::decode(&mut buf); - self.ingest_multixact_create_record(&mut writer, &xlrec)?; + self.ingest_multixact_create_record(&mut modification, &xlrec)?; } else if info == pg_constants::XLOG_MULTIXACT_TRUNCATE_ID { let xlrec = XlMultiXactTruncate::decode(&mut buf); - self.ingest_multixact_truncate_record(&mut writer, &xlrec)?; + self.ingest_multixact_truncate_record(&mut modification, &xlrec)?; } } else if decoded.xl_rmid == pg_constants::RM_RELMAP_ID { let xlrec = XlRelmapUpdate::decode(&mut buf); - self.ingest_relmap_page(&mut writer, &xlrec, &decoded)?; + self.ingest_relmap_page(&mut modification, &xlrec, &decoded)?; } else if decoded.xl_rmid == pg_constants::RM_XLOG_ID { let info = decoded.xl_info & pg_constants::XLR_RMGR_INFO_MASK; if info == pg_constants::XLOG_NEXTOID { @@ -228,7 +226,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { { let mut checkpoint_bytes = [0u8; SIZEOF_CHECKPOINT]; buf.copy_to_slice(&mut checkpoint_bytes); - let xlog_checkpoint = CheckPoint::decode(&checkpoint_bytes).unwrap(); + let xlog_checkpoint = CheckPoint::decode(&checkpoint_bytes)?; trace!( "xlog_checkpoint.oldestXid={}, checkpoint.oldestXid={}", xlog_checkpoint.oldestXid, @@ -249,31 +247,11 @@ impl<'a, R: Repository> WalIngest<'a, R> { // Iterate through all the blocks that the record modifies, and // "put" a separate copy of the record for each block. for blk in decoded.blocks.iter() { - - let lsn_hex = { - use bytes::BufMut; - let mut bytes = BytesMut::new(); - bytes.put_u64(lsn.0); - hex::encode(bytes.freeze()) - }; - let page_hex = { - let foo: DecodedBkpBlock; - use bytes::BufMut; - let mut page = BytesMut::new(); - page.put_u32(blk.rnode_spcnode); - page.put_u32(blk.rnode_dbnode); - page.put_u32(blk.rnode_relnode); - page.put_u8(blk.forknum); - page.put_u32(blk.blkno); - hex::encode(page.freeze()) - }; - println!("wal-at-lsn-modified-page {} {} {}", lsn_hex, page_hex, recdata_len); - - self.ingest_decoded_block(&mut writer, lsn, &decoded, blk)?; + self.ingest_decoded_block(&mut modification, lsn, &decoded, blk)?; } // Emit wal entry metadata, if configured to do so - crate::wal_metadata::write(WalEntryMetadata { + crate::wal_metadata::write(crate::wal_metadata::WalEntryMetadata { lsn, size: recdata_len, affected_pages: decoded.blocks.iter().map(|blk| blk.into()).collect() @@ -283,20 +261,20 @@ impl<'a, R: Repository> WalIngest<'a, R> { if self.checkpoint_modified { let new_checkpoint_bytes = self.checkpoint.encode(); - writer.put_checkpoint(new_checkpoint_bytes)?; + modification.put_checkpoint(new_checkpoint_bytes)?; self.checkpoint_modified = false; } // Now that this record has been fully handled, including updating the // checkpoint data, let the repository know that it is up-to-date to this LSN - writer.finish()?; + modification.commit()?; Ok(()) } fn ingest_decoded_block( &mut self, - timeline: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, lsn: Lsn, decoded: &DecodedWALRecord, blk: &DecodedBkpBlock, @@ -336,13 +314,13 @@ impl<'a, R: Repository> WalIngest<'a, R> { image[0..4].copy_from_slice(&((lsn.0 >> 32) as u32).to_le_bytes()); image[4..8].copy_from_slice(&(lsn.0 as u32).to_le_bytes()); assert_eq!(image.len(), pg_constants::BLCKSZ as usize); - self.put_rel_page_image(timeline, rel, blk.blkno, image.freeze())?; + self.put_rel_page_image(modification, rel, blk.blkno, image.freeze())?; } else { let rec = ZenithWalRecord::Postgres { will_init: blk.will_init || blk.apply_image, rec: decoded.record.clone(), }; - self.put_rel_wal_record(timeline, rel, blk.blkno, rec)?; + self.put_rel_wal_record(modification, rel, blk.blkno, rec)?; } Ok(()) } @@ -350,7 +328,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { fn ingest_heapam_record( &mut self, buf: &mut Bytes, - timeline: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, decoded: &mut DecodedWALRecord, ) -> Result<()> { // Handle VM bit updates that are implicitly part of heap records. @@ -448,7 +426,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { // An UPDATE record that needs to clear the bits for both old and the // new page, both of which reside on the same VM page. self.put_rel_wal_record( - timeline, + modification, vm_rel, new_vm_blk.unwrap(), ZenithWalRecord::ClearVisibilityMapFlags { @@ -462,7 +440,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { // different VM pages. if let Some(new_vm_blk) = new_vm_blk { self.put_rel_wal_record( - timeline, + modification, vm_rel, new_vm_blk, ZenithWalRecord::ClearVisibilityMapFlags { @@ -474,7 +452,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { } if let Some(old_vm_blk) = old_vm_blk { self.put_rel_wal_record( - timeline, + modification, vm_rel, old_vm_blk, ZenithWalRecord::ClearVisibilityMapFlags { @@ -494,7 +472,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { /// Subroutine of ingest_record(), to handle an XLOG_DBASE_CREATE record. fn ingest_xlog_dbase_create( &mut self, - timeline: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, rec: &XlCreateDatabase, ) -> Result<()> { let db_id = rec.db_id; @@ -507,15 +485,19 @@ impl<'a, R: Repository> WalIngest<'a, R> { // cannot pass 'lsn' to the Timeline.get_* functions, or they will block waiting for // the last valid LSN to advance up to it. So we use the previous record's LSN in the // get calls instead. - let req_lsn = timeline.get_last_record_lsn(); + let req_lsn = modification.tline.get_last_record_lsn(); - let rels = timeline.list_rels(src_tablespace_id, src_db_id, req_lsn)?; + let rels = modification + .tline + .list_rels(src_tablespace_id, src_db_id, req_lsn)?; debug!("ingest_xlog_dbase_create: {} rels", rels.len()); // Copy relfilemap - let filemap = timeline.get_relmap_file(src_tablespace_id, src_db_id, req_lsn)?; - timeline.put_relmap_file(tablespace_id, db_id, filemap)?; + let filemap = modification + .tline + .get_relmap_file(src_tablespace_id, src_db_id, req_lsn)?; + modification.put_relmap_file(tablespace_id, db_id, filemap)?; let mut num_rels_copied = 0; let mut num_blocks_copied = 0; @@ -523,7 +505,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { assert_eq!(src_rel.spcnode, src_tablespace_id); assert_eq!(src_rel.dbnode, src_db_id); - let nblocks = timeline.get_rel_size(src_rel, req_lsn)?; + let nblocks = modification.tline.get_rel_size(src_rel, req_lsn)?; let dst_rel = RelTag { spcnode: tablespace_id, dbnode: db_id, @@ -531,15 +513,17 @@ impl<'a, R: Repository> WalIngest<'a, R> { forknum: src_rel.forknum, }; - timeline.put_rel_creation(dst_rel, nblocks)?; + modification.put_rel_creation(dst_rel, nblocks)?; // Copy content debug!("copying rel {} to {}, {} blocks", src_rel, dst_rel, nblocks); for blknum in 0..nblocks { debug!("copying block {} from {} to {}", blknum, src_rel, dst_rel); - let content = timeline.get_rel_page_at_lsn(src_rel, blknum, req_lsn)?; - timeline.put_rel_page_image(dst_rel, blknum, content)?; + let content = modification + .tline + .get_rel_page_at_lsn(src_rel, blknum, req_lsn)?; + modification.put_rel_page_image(dst_rel, blknum, content)?; num_blocks_copied += 1; } @@ -555,7 +539,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { fn ingest_xlog_smgr_create( &mut self, - writer: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, rec: &XlSmgrCreate, ) -> Result<()> { let rel = RelTag { @@ -564,7 +548,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { relnode: rec.rnode.relnode, forknum: rec.forknum, }; - self.put_rel_creation(writer, rel)?; + self.put_rel_creation(modification, rel)?; Ok(()) } @@ -573,7 +557,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { /// This is the same logic as in PostgreSQL's smgr_redo() function. fn ingest_xlog_smgr_truncate( &mut self, - writer: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, rec: &XlSmgrTruncate, ) -> Result<()> { let spcnode = rec.rnode.spcnode; @@ -587,7 +571,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { relnode, forknum: pg_constants::MAIN_FORKNUM, }; - self.put_rel_truncation(writer, rel, rec.blkno)?; + self.put_rel_truncation(modification, rel, rec.blkno)?; } if (rec.flags & pg_constants::SMGR_TRUNCATE_FSM) != 0 { let rel = RelTag { @@ -610,7 +594,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { info!("Partial truncation of FSM is not supported"); } let num_fsm_blocks = 0; - self.put_rel_truncation(writer, rel, num_fsm_blocks)?; + self.put_rel_truncation(modification, rel, num_fsm_blocks)?; } if (rec.flags & pg_constants::SMGR_TRUNCATE_VM) != 0 { let rel = RelTag { @@ -629,7 +613,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { info!("Partial truncation of VM is not supported"); } let num_vm_blocks = 0; - self.put_rel_truncation(writer, rel, num_vm_blocks)?; + self.put_rel_truncation(modification, rel, num_vm_blocks)?; } Ok(()) } @@ -638,7 +622,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { /// fn ingest_xact_record( &mut self, - writer: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, parsed: &XlXactParsedRecord, is_commit: bool, ) -> Result<()> { @@ -654,7 +638,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { // This subxact goes to different page. Write the record // for all the XIDs on the previous page, and continue // accumulating XIDs on this new page. - writer.put_slru_wal_record( + modification.put_slru_wal_record( SlruKind::Clog, segno, rpageno, @@ -671,7 +655,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { rpageno = pageno % pg_constants::SLRU_PAGES_PER_SEGMENT; page_xids.push(*subxact); } - writer.put_slru_wal_record( + modification.put_slru_wal_record( SlruKind::Clog, segno, rpageno, @@ -691,8 +675,8 @@ impl<'a, R: Repository> WalIngest<'a, R> { relnode: xnode.relnode, }; let last_lsn = self.timeline.get_last_record_lsn(); - if writer.get_rel_exists(rel, last_lsn)? { - self.put_rel_drop(writer, rel)?; + if modification.tline.get_rel_exists(rel, last_lsn)? { + self.put_rel_drop(modification, rel)?; } } } @@ -701,7 +685,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { fn ingest_clog_truncate_record( &mut self, - timeline: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, xlrec: &XlClogTruncate, ) -> Result<()> { info!( @@ -742,11 +726,14 @@ impl<'a, R: Repository> WalIngest<'a, R> { // will block waiting for the last valid LSN to advance up to // it. So we use the previous record's LSN in the get calls // instead. - let req_lsn = timeline.get_last_record_lsn(); - for segno in timeline.list_slru_segments(SlruKind::Clog, req_lsn)? { + let req_lsn = modification.tline.get_last_record_lsn(); + for segno in modification + .tline + .list_slru_segments(SlruKind::Clog, req_lsn)? + { let segpage = segno * pg_constants::SLRU_PAGES_PER_SEGMENT; if slru_may_delete_clogsegment(segpage, xlrec.pageno) { - timeline.drop_slru_segment(SlruKind::Clog, segno)?; + modification.drop_slru_segment(SlruKind::Clog, segno)?; trace!("Drop CLOG segment {:>04X}", segno); } } @@ -756,7 +743,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { fn ingest_multixact_create_record( &mut self, - timeline: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, xlrec: &XlMultiXactCreate, ) -> Result<()> { // Create WAL record for updating the multixact-offsets page @@ -764,7 +751,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { let segno = pageno / pg_constants::SLRU_PAGES_PER_SEGMENT; let rpageno = pageno % pg_constants::SLRU_PAGES_PER_SEGMENT; - timeline.put_slru_wal_record( + modification.put_slru_wal_record( SlruKind::MultiXactOffsets, segno, rpageno, @@ -798,7 +785,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { } let n_this_page = this_page_members.len(); - timeline.put_slru_wal_record( + modification.put_slru_wal_record( SlruKind::MultiXactMembers, pageno / pg_constants::SLRU_PAGES_PER_SEGMENT, pageno % pg_constants::SLRU_PAGES_PER_SEGMENT, @@ -835,7 +822,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { fn ingest_multixact_truncate_record( &mut self, - timeline: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, xlrec: &XlMultiXactTruncate, ) -> Result<()> { self.checkpoint.oldestMulti = xlrec.end_trunc_off; @@ -851,7 +838,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { // Delete all the segments except the last one. The last segment can still // contain, possibly partially, valid data. while segment != endsegment { - timeline.drop_slru_segment(SlruKind::MultiXactMembers, segment as u32)?; + modification.drop_slru_segment(SlruKind::MultiXactMembers, segment as u32)?; /* move to next segment, handling wraparound correctly */ if segment == maxsegment { @@ -869,7 +856,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { fn ingest_relmap_page( &mut self, - timeline: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, xlrec: &XlRelmapUpdate, decoded: &DecodedWALRecord, ) -> Result<()> { @@ -878,58 +865,62 @@ impl<'a, R: Repository> WalIngest<'a, R> { // skip xl_relmap_update buf.advance(12); - timeline.put_relmap_file(xlrec.tsid, xlrec.dbid, Bytes::copy_from_slice(&buf[..]))?; + modification.put_relmap_file(xlrec.tsid, xlrec.dbid, Bytes::copy_from_slice(&buf[..]))?; Ok(()) } fn put_rel_creation( &mut self, - writer: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, rel: RelTag, ) -> Result<()> { self.relsize_cache.insert(rel, 0); - writer.put_rel_creation(rel, 0)?; + modification.put_rel_creation(rel, 0)?; Ok(()) } fn put_rel_page_image( &mut self, - writer: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, rel: RelTag, blknum: BlockNumber, img: Bytes, ) -> Result<()> { - self.handle_rel_extend(writer, rel, blknum)?; - writer.put_rel_page_image(rel, blknum, img)?; + self.handle_rel_extend(modification, rel, blknum)?; + modification.put_rel_page_image(rel, blknum, img)?; Ok(()) } fn put_rel_wal_record( &mut self, - writer: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, rel: RelTag, blknum: BlockNumber, rec: ZenithWalRecord, ) -> Result<()> { - self.handle_rel_extend(writer, rel, blknum)?; - writer.put_rel_wal_record(rel, blknum, rec)?; + self.handle_rel_extend(modification, rel, blknum)?; + modification.put_rel_wal_record(rel, blknum, rec)?; Ok(()) } fn put_rel_truncation( &mut self, - writer: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, rel: RelTag, nblocks: BlockNumber, ) -> Result<()> { - writer.put_rel_truncation(rel, nblocks)?; + modification.put_rel_truncation(rel, nblocks)?; self.relsize_cache.insert(rel, nblocks); Ok(()) } - fn put_rel_drop(&mut self, writer: &mut DatadirTimelineWriter, rel: RelTag) -> Result<()> { - writer.put_rel_drop(rel)?; + fn put_rel_drop( + &mut self, + modification: &mut DatadirModification, + rel: RelTag, + ) -> Result<()> { + modification.put_rel_drop(rel)?; self.relsize_cache.remove(&rel); Ok(()) } @@ -951,7 +942,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { fn handle_rel_extend( &mut self, - writer: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, rel: RelTag, blknum: BlockNumber, ) -> Result<()> { @@ -965,7 +956,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { let last_lsn = self.timeline.get_last_record_lsn(); let nblocks = if !self.timeline.get_rel_exists(rel, last_lsn)? { // create it with 0 size initially, the logic below will extend it - writer.put_rel_creation(rel, 0)?; + modification.put_rel_creation(rel, 0)?; 0 } else { self.timeline.get_rel_size(rel, last_lsn)? @@ -976,11 +967,11 @@ impl<'a, R: Repository> WalIngest<'a, R> { if new_nblocks > old_nblocks { //info!("extending {} {} to {}", rel, old_nblocks, new_nblocks); - writer.put_rel_extend(rel, new_nblocks)?; + modification.put_rel_extend(rel, new_nblocks)?; // fill the gap with zeros for gap_blknum in old_nblocks..blknum { - writer.put_rel_page_image(rel, gap_blknum, ZERO_PAGE.clone())?; + modification.put_rel_page_image(rel, gap_blknum, ZERO_PAGE.clone())?; } self.relsize_cache.insert(rel, new_nblocks); } @@ -989,20 +980,20 @@ impl<'a, R: Repository> WalIngest<'a, R> { fn put_slru_page_image( &mut self, - writer: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, kind: SlruKind, segno: u32, blknum: BlockNumber, img: Bytes, ) -> Result<()> { - self.handle_slru_extend(writer, kind, segno, blknum)?; - writer.put_slru_page_image(kind, segno, blknum, img)?; + self.handle_slru_extend(modification, kind, segno, blknum)?; + modification.put_slru_page_image(kind, segno, blknum, img)?; Ok(()) } fn handle_slru_extend( &mut self, - writer: &mut DatadirTimelineWriter, + modification: &mut DatadirModification, kind: SlruKind, segno: u32, blknum: BlockNumber, @@ -1021,7 +1012,7 @@ impl<'a, R: Repository> WalIngest<'a, R> { .get_slru_segment_exists(kind, segno, last_lsn)? { // create it with 0 size initially, the logic below will extend it - writer.put_slru_segment_creation(kind, segno, 0)?; + modification.put_slru_segment_creation(kind, segno, 0)?; 0 } else { self.timeline.get_slru_segment_size(kind, segno, last_lsn)? @@ -1035,11 +1026,11 @@ impl<'a, R: Repository> WalIngest<'a, R> { old_nblocks, new_nblocks ); - writer.put_slru_extend(kind, segno, new_nblocks)?; + modification.put_slru_extend(kind, segno, new_nblocks)?; // fill the gap with zeros for gap_blknum in old_nblocks..blknum { - writer.put_slru_page_image(kind, segno, gap_blknum, ZERO_PAGE.clone())?; + modification.put_slru_page_image(kind, segno, gap_blknum, ZERO_PAGE.clone())?; } } Ok(()) @@ -1072,10 +1063,10 @@ mod tests { static ZERO_CHECKPOINT: Bytes = Bytes::from_static(&[0u8; SIZEOF_CHECKPOINT]); fn init_walingest_test(tline: &DatadirTimeline) -> Result> { - let mut writer = tline.begin_record(Lsn(0x10)); - writer.put_checkpoint(ZERO_CHECKPOINT.clone())?; - writer.put_relmap_file(0, 111, Bytes::from(""))?; // dummy relmapper file - writer.finish()?; + let mut m = tline.begin_modification(Lsn(0x10)); + m.put_checkpoint(ZERO_CHECKPOINT.clone())?; + m.put_relmap_file(0, 111, Bytes::from(""))?; // dummy relmapper file + m.commit()?; let walingest = WalIngest::new(tline, Lsn(0x10))?; Ok(walingest) @@ -1087,27 +1078,25 @@ mod tests { let tline = create_test_timeline(repo, TIMELINE_ID)?; let mut walingest = init_walingest_test(&tline)?; - let mut writer = tline.begin_record(Lsn(0x20)); - walingest.put_rel_creation(&mut writer, TESTREL_A)?; - walingest.put_rel_page_image(&mut writer, TESTREL_A, 0, TEST_IMG("foo blk 0 at 2"))?; - writer.finish()?; - let mut writer = tline.begin_record(Lsn(0x30)); - walingest.put_rel_page_image(&mut writer, TESTREL_A, 0, TEST_IMG("foo blk 0 at 3"))?; - writer.finish()?; - let mut writer = tline.begin_record(Lsn(0x40)); - walingest.put_rel_page_image(&mut writer, TESTREL_A, 1, TEST_IMG("foo blk 1 at 4"))?; - writer.finish()?; - let mut writer = tline.begin_record(Lsn(0x50)); - walingest.put_rel_page_image(&mut writer, TESTREL_A, 2, TEST_IMG("foo blk 2 at 5"))?; - writer.finish()?; + let mut m = tline.begin_modification(Lsn(0x20)); + walingest.put_rel_creation(&mut m, TESTREL_A)?; + walingest.put_rel_page_image(&mut m, TESTREL_A, 0, TEST_IMG("foo blk 0 at 2"))?; + m.commit()?; + let mut m = tline.begin_modification(Lsn(0x30)); + walingest.put_rel_page_image(&mut m, TESTREL_A, 0, TEST_IMG("foo blk 0 at 3"))?; + m.commit()?; + let mut m = tline.begin_modification(Lsn(0x40)); + walingest.put_rel_page_image(&mut m, TESTREL_A, 1, TEST_IMG("foo blk 1 at 4"))?; + m.commit()?; + let mut m = tline.begin_modification(Lsn(0x50)); + walingest.put_rel_page_image(&mut m, TESTREL_A, 2, TEST_IMG("foo blk 2 at 5"))?; + m.commit()?; assert_current_logical_size(&tline, Lsn(0x50)); // The relation was created at LSN 2, not visible at LSN 1 yet. assert_eq!(tline.get_rel_exists(TESTREL_A, Lsn(0x10))?, false); - - // FIXME: should error out? - //assert!(tline.get_rel_size(TESTREL_A, Lsn(0x10))?.is_none()); + assert!(tline.get_rel_size(TESTREL_A, Lsn(0x10)).is_err()); assert_eq!(tline.get_rel_exists(TESTREL_A, Lsn(0x20))?, true); assert_eq!(tline.get_rel_size(TESTREL_A, Lsn(0x20))?, 1); @@ -1147,9 +1136,9 @@ mod tests { ); // Truncate last block - let mut writer = tline.begin_record(Lsn(0x60)); - walingest.put_rel_truncation(&mut writer, TESTREL_A, 2)?; - writer.finish()?; + let mut m = tline.begin_modification(Lsn(0x60)); + walingest.put_rel_truncation(&mut m, TESTREL_A, 2)?; + m.commit()?; assert_current_logical_size(&tline, Lsn(0x60)); // Check reported size and contents after truncation @@ -1171,15 +1160,15 @@ mod tests { ); // Truncate to zero length - let mut writer = tline.begin_record(Lsn(0x68)); - walingest.put_rel_truncation(&mut writer, TESTREL_A, 0)?; - writer.finish()?; + let mut m = tline.begin_modification(Lsn(0x68)); + walingest.put_rel_truncation(&mut m, TESTREL_A, 0)?; + m.commit()?; assert_eq!(tline.get_rel_size(TESTREL_A, Lsn(0x68))?, 0); // Extend from 0 to 2 blocks, leaving a gap - let mut writer = tline.begin_record(Lsn(0x70)); - walingest.put_rel_page_image(&mut writer, TESTREL_A, 1, TEST_IMG("foo blk 1"))?; - writer.finish()?; + let mut m = tline.begin_modification(Lsn(0x70)); + walingest.put_rel_page_image(&mut m, TESTREL_A, 1, TEST_IMG("foo blk 1"))?; + m.commit()?; assert_eq!(tline.get_rel_size(TESTREL_A, Lsn(0x70))?, 2); assert_eq!( tline.get_rel_page_at_lsn(TESTREL_A, 0, Lsn(0x70))?, @@ -1191,9 +1180,9 @@ mod tests { ); // Extend a lot more, leaving a big gap that spans across segments - let mut writer = tline.begin_record(Lsn(0x80)); - walingest.put_rel_page_image(&mut writer, TESTREL_A, 1500, TEST_IMG("foo blk 1500"))?; - writer.finish()?; + let mut m = tline.begin_modification(Lsn(0x80)); + walingest.put_rel_page_image(&mut m, TESTREL_A, 1500, TEST_IMG("foo blk 1500"))?; + m.commit()?; assert_eq!(tline.get_rel_size(TESTREL_A, Lsn(0x80))?, 1501); for blk in 2..1500 { assert_eq!( @@ -1217,18 +1206,18 @@ mod tests { let tline = create_test_timeline(repo, TIMELINE_ID)?; let mut walingest = init_walingest_test(&tline)?; - let mut writer = tline.begin_record(Lsn(0x20)); - walingest.put_rel_page_image(&mut writer, TESTREL_A, 0, TEST_IMG("foo blk 0 at 2"))?; - writer.finish()?; + let mut m = tline.begin_modification(Lsn(0x20)); + walingest.put_rel_page_image(&mut m, TESTREL_A, 0, TEST_IMG("foo blk 0 at 2"))?; + m.commit()?; // Check that rel exists and size is correct assert_eq!(tline.get_rel_exists(TESTREL_A, Lsn(0x20))?, true); assert_eq!(tline.get_rel_size(TESTREL_A, Lsn(0x20))?, 1); // Drop rel - let mut writer = tline.begin_record(Lsn(0x30)); - walingest.put_rel_drop(&mut writer, TESTREL_A)?; - writer.finish()?; + let mut m = tline.begin_modification(Lsn(0x30)); + walingest.put_rel_drop(&mut m, TESTREL_A)?; + m.commit()?; // Check that rel is not visible anymore assert_eq!(tline.get_rel_exists(TESTREL_A, Lsn(0x30))?, false); @@ -1237,9 +1226,9 @@ mod tests { //assert!(tline.get_rel_size(TESTREL_A, Lsn(0x30))?.is_none()); // Re-create it - let mut writer = tline.begin_record(Lsn(0x40)); - walingest.put_rel_page_image(&mut writer, TESTREL_A, 0, TEST_IMG("foo blk 0 at 4"))?; - writer.finish()?; + let mut m = tline.begin_modification(Lsn(0x40)); + walingest.put_rel_page_image(&mut m, TESTREL_A, 0, TEST_IMG("foo blk 0 at 4"))?; + m.commit()?; // Check that rel exists and size is correct assert_eq!(tline.get_rel_exists(TESTREL_A, Lsn(0x40))?, true); @@ -1257,23 +1246,18 @@ mod tests { let tline = create_test_timeline(repo, TIMELINE_ID)?; let mut walingest = init_walingest_test(&tline)?; - //from storage_layer.rs - const RELISH_SEG_SIZE: u32 = 10 * 1024 * 1024 / 8192; - let relsize = RELISH_SEG_SIZE * 2; - - // Create relation with relsize blocks - let mut writer = tline.begin_record(Lsn(0x20)); + // Create a 20 MB relation (the size is arbitrary) + let relsize = 20 * 1024 * 1024 / 8192; + let mut m = tline.begin_modification(Lsn(0x20)); for blkno in 0..relsize { let data = format!("foo blk {} at {}", blkno, Lsn(0x20)); - walingest.put_rel_page_image(&mut writer, TESTREL_A, blkno, TEST_IMG(&data))?; + walingest.put_rel_page_image(&mut m, TESTREL_A, blkno, TEST_IMG(&data))?; } - writer.finish()?; + m.commit()?; // The relation was created at LSN 20, not visible at LSN 1 yet. assert_eq!(tline.get_rel_exists(TESTREL_A, Lsn(0x10))?, false); - - // FIXME: should fail - // assert!(tline.get_rel_size(TESTREL_A, Lsn(0x10))?.is_none()); + assert!(tline.get_rel_size(TESTREL_A, Lsn(0x10)).is_err()); assert_eq!(tline.get_rel_exists(TESTREL_A, Lsn(0x20))?, true); assert_eq!(tline.get_rel_size(TESTREL_A, Lsn(0x20))?, relsize); @@ -1290,9 +1274,9 @@ mod tests { // Truncate relation so that second segment was dropped // - only leave one page - let mut writer = tline.begin_record(Lsn(0x60)); - walingest.put_rel_truncation(&mut writer, TESTREL_A, 1)?; - writer.finish()?; + let mut m = tline.begin_modification(Lsn(0x60)); + walingest.put_rel_truncation(&mut m, TESTREL_A, 1)?; + m.commit()?; // Check reported size and contents after truncation assert_eq!(tline.get_rel_size(TESTREL_A, Lsn(0x60))?, 1); @@ -1320,12 +1304,12 @@ mod tests { // Extend relation again. // Add enough blocks to create second segment let lsn = Lsn(0x80); - let mut writer = tline.begin_record(lsn); + let mut m = tline.begin_modification(lsn); for blkno in 0..relsize { let data = format!("foo blk {} at {}", blkno, lsn); - walingest.put_rel_page_image(&mut writer, TESTREL_A, blkno, TEST_IMG(&data))?; + walingest.put_rel_page_image(&mut m, TESTREL_A, blkno, TEST_IMG(&data))?; } - writer.finish()?; + m.commit()?; assert_eq!(tline.get_rel_exists(TESTREL_A, Lsn(0x80))?, true); assert_eq!(tline.get_rel_size(TESTREL_A, Lsn(0x80))?, relsize); @@ -1353,10 +1337,10 @@ mod tests { let mut lsn = 0x10; for blknum in 0..pg_constants::RELSEG_SIZE + 1 { lsn += 0x10; - let mut writer = tline.begin_record(Lsn(lsn)); + let mut m = tline.begin_modification(Lsn(lsn)); let img = TEST_IMG(&format!("foo blk {} at {}", blknum, Lsn(lsn))); - walingest.put_rel_page_image(&mut writer, TESTREL_A, blknum as BlockNumber, img)?; - writer.finish()?; + walingest.put_rel_page_image(&mut m, TESTREL_A, blknum as BlockNumber, img)?; + m.commit()?; } assert_current_logical_size(&tline, Lsn(lsn)); @@ -1368,9 +1352,9 @@ mod tests { // Truncate one block lsn += 0x10; - let mut writer = tline.begin_record(Lsn(lsn)); - walingest.put_rel_truncation(&mut writer, TESTREL_A, pg_constants::RELSEG_SIZE)?; - writer.finish()?; + let mut m = tline.begin_modification(Lsn(lsn)); + walingest.put_rel_truncation(&mut m, TESTREL_A, pg_constants::RELSEG_SIZE)?; + m.commit()?; assert_eq!( tline.get_rel_size(TESTREL_A, Lsn(lsn))?, pg_constants::RELSEG_SIZE @@ -1379,9 +1363,9 @@ mod tests { // Truncate another block lsn += 0x10; - let mut writer = tline.begin_record(Lsn(lsn)); - walingest.put_rel_truncation(&mut writer, TESTREL_A, pg_constants::RELSEG_SIZE - 1)?; - writer.finish()?; + let mut m = tline.begin_modification(Lsn(lsn)); + walingest.put_rel_truncation(&mut m, TESTREL_A, pg_constants::RELSEG_SIZE - 1)?; + m.commit()?; assert_eq!( tline.get_rel_size(TESTREL_A, Lsn(lsn))?, pg_constants::RELSEG_SIZE - 1 @@ -1393,9 +1377,9 @@ mod tests { let mut size: i32 = 3000; while size >= 0 { lsn += 0x10; - let mut writer = tline.begin_record(Lsn(lsn)); - walingest.put_rel_truncation(&mut writer, TESTREL_A, size as BlockNumber)?; - writer.finish()?; + let mut m = tline.begin_modification(Lsn(lsn)); + walingest.put_rel_truncation(&mut m, TESTREL_A, size as BlockNumber)?; + m.commit()?; assert_eq!( tline.get_rel_size(TESTREL_A, Lsn(lsn))?, size as BlockNumber diff --git a/pageserver/src/walreceiver.rs b/pageserver/src/walreceiver.rs index b57d498c7c..6de0b87478 100644 --- a/pageserver/src/walreceiver.rs +++ b/pageserver/src/walreceiver.rs @@ -32,6 +32,7 @@ use tracing::*; use zenith_utils::lsn::Lsn; use zenith_utils::pq_proto::ZenithFeedback; use zenith_utils::zid::ZTenantId; +use zenith_utils::zid::ZTenantTimelineId; use zenith_utils::zid::ZTimelineId; // @@ -69,7 +70,7 @@ pub fn launch_wal_receiver( match receivers.get_mut(&(tenantid, timelineid)) { Some(receiver) => { - info!("wal receiver already running, updating connection string"); + debug!("wal receiver already running, updating connection string"); receiver.wal_producer_connstr = wal_producer_connstr.into(); } None => { @@ -78,9 +79,11 @@ pub fn launch_wal_receiver( Some(tenantid), Some(timelineid), "WAL receiver thread", + false, move || { IS_WAL_RECEIVER.with(|c| c.set(true)); - thread_main(conf, tenantid, timelineid) + thread_main(conf, tenantid, timelineid); + Ok(()) }, )?; @@ -110,20 +113,16 @@ fn get_wal_producer_connstr(tenantid: ZTenantId, timelineid: ZTimelineId) -> Str // // This is the entry point for the WAL receiver thread. // -fn thread_main( - conf: &'static PageServerConf, - tenantid: ZTenantId, - timelineid: ZTimelineId, -) -> Result<()> { - let _enter = info_span!("WAL receiver", timeline = %timelineid, tenant = %tenantid).entered(); +fn thread_main(conf: &'static PageServerConf, tenant_id: ZTenantId, timeline_id: ZTimelineId) { + let _enter = info_span!("WAL receiver", timeline = %timeline_id, tenant = %tenant_id).entered(); info!("WAL receiver thread started"); // Look up the current WAL producer address - let wal_producer_connstr = get_wal_producer_connstr(tenantid, timelineid); + let wal_producer_connstr = get_wal_producer_connstr(tenant_id, timeline_id); // Make a connection to the WAL safekeeper, or directly to the primary PostgreSQL server, // and start streaming WAL from it. - let res = walreceiver_main(conf, tenantid, timelineid, &wal_producer_connstr); + let res = walreceiver_main(conf, tenant_id, timeline_id, &wal_producer_connstr); // TODO cleanup info messages if let Err(e) = res { @@ -131,22 +130,21 @@ fn thread_main( } else { info!( "walreceiver disconnected tenant {}, timelineid {}", - tenantid, timelineid + tenant_id, timeline_id ); } // Drop it from list of active WAL_RECEIVERS // so that next callmemaybe request launched a new thread - drop_wal_receiver(tenantid, timelineid); - Ok(()) + drop_wal_receiver(tenant_id, timeline_id); } fn walreceiver_main( _conf: &PageServerConf, - tenantid: ZTenantId, - timelineid: ZTimelineId, + tenant_id: ZTenantId, + timeline_id: ZTimelineId, wal_producer_connstr: &str, -) -> Result<(), Error> { +) -> anyhow::Result<(), Error> { // Connect to the database in replication mode. info!("connecting to {:?}", wal_producer_connstr); let connect_cfg = format!( @@ -183,13 +181,16 @@ fn walreceiver_main( let end_of_wal = Lsn::from(u64::from(identify.xlogpos)); let mut caught_up = false; + let repo = tenant_mgr::get_repository_for_tenant(tenant_id) + .with_context(|| format!("no repository found for tenant {}", tenant_id))?; let timeline = - tenant_mgr::get_timeline_for_tenant(tenantid, timelineid).with_context(|| { + tenant_mgr::get_timeline_for_tenant_load(tenant_id, timeline_id).with_context(|| { format!( - "Can not start the walrecever for a remote tenant {}, timeline {}", - tenantid, timelineid, + "local timeline {} not found for tenant {}", + timeline_id, tenant_id ) })?; + let remote_index = repo.get_remote_index(); // // Start streaming the WAL, from where we left off previously. @@ -252,7 +253,7 @@ fn walreceiver_main( // It is important to deal with the aligned records as lsn in getPage@LSN is // aligned and can be several bytes bigger. Without this alignment we are // at risk of hitting a deadlock. - assert!(lsn.is_aligned()); + anyhow::ensure!(lsn.is_aligned()); walingest.ingest_record(&timeline, recdata, lsn)?; @@ -294,11 +295,19 @@ fn walreceiver_main( }; if let Some(last_lsn) = status_update { - let timeline_synced_disk_consistent_lsn = - tenant_mgr::get_repository_for_tenant(tenantid)? - .get_timeline_state(timelineid) - .and_then(|state| state.remote_disk_consistent_lsn()) - .unwrap_or(Lsn(0)); + let timeline_remote_consistent_lsn = runtime.block_on(async { + remote_index + .read() + .await + // here we either do not have this timeline in remote index + // or there were no checkpoints for it yet + .timeline_entry(&ZTenantTimelineId { + tenant_id, + timeline_id, + }) + .and_then(|e| e.disk_consistent_lsn()) + .unwrap_or(Lsn(0)) // no checkpoint was uploaded + }); // The last LSN we processed. It is not guaranteed to survive pageserver crash. let write_lsn = u64::from(last_lsn); @@ -306,7 +315,7 @@ fn walreceiver_main( let flush_lsn = u64::from(timeline.tline.get_disk_consistent_lsn()); // The last LSN that is synced to remote storage and is guaranteed to survive pageserver crash // Used by safekeepers to remove WAL preceding `remote_consistent_lsn`. - let apply_lsn = u64::from(timeline_synced_disk_consistent_lsn); + let apply_lsn = u64::from(timeline_remote_consistent_lsn); let ts = SystemTime::now(); // Send zenith feedback message. diff --git a/pageserver/src/walredo.rs b/pageserver/src/walredo.rs index 893efc5fba..ae22f1eead 100644 --- a/pageserver/src/walredo.rs +++ b/pageserver/src/walredo.rs @@ -43,7 +43,7 @@ use zenith_utils::zid::ZTenantId; use crate::config::PageServerConf; use crate::pgdatadir_mapping::{key_to_rel_block, key_to_slru_block}; -use crate::relish::*; +use crate::reltag::{RelTag, SlruKind}; use crate::repository::Key; use crate::walrecord::ZenithWalRecord; use postgres_ffi::nonrelfile_utils::mx_offset_to_flags_bitshift; @@ -344,13 +344,16 @@ impl PostgresRedoManager { ZenithWalRecord::Postgres { will_init: _, rec: _, - } => panic!("tried to pass postgres wal record to zenith WAL redo"), + } => { + error!("tried to pass postgres wal record to zenith WAL redo"); + return Err(WalRedoError::InvalidRequest); + } ZenithWalRecord::ClearVisibilityMapFlags { new_heap_blkno, old_heap_blkno, flags, } => { - // sanity check that this is modifying the correct relish + // sanity check that this is modifying the correct relation let (rel, blknum) = key_to_rel_block(key).or(Err(WalRedoError::InvalidRecord))?; assert!( rel.forknum == pg_constants::VISIBILITYMAP_FORKNUM, @@ -563,20 +566,23 @@ impl PostgresRedoProcess { } info!("running initdb in {:?}", datadir.display()); let initdb = Command::new(conf.pg_bin_dir().join("initdb")) - .args(&["-D", datadir.to_str().unwrap()]) + .args(&["-D", &datadir.to_string_lossy()]) .arg("-N") .env_clear() - .env("LD_LIBRARY_PATH", conf.pg_lib_dir().to_str().unwrap()) - .env("DYLD_LIBRARY_PATH", conf.pg_lib_dir().to_str().unwrap()) + .env("LD_LIBRARY_PATH", conf.pg_lib_dir()) + .env("DYLD_LIBRARY_PATH", conf.pg_lib_dir()) .output() - .expect("failed to execute initdb"); + .map_err(|e| Error::new(e.kind(), format!("failed to execute initdb: {}", e)))?; if !initdb.status.success() { - panic!( - "initdb failed: {}\nstderr:\n{}", - std::str::from_utf8(&initdb.stdout).unwrap(), - std::str::from_utf8(&initdb.stderr).unwrap() - ); + return Err(Error::new( + ErrorKind::Other, + format!( + "initdb failed\nstdout: {}\nstderr:\n{}", + String::from_utf8_lossy(&initdb.stdout), + String::from_utf8_lossy(&initdb.stderr) + ), + )); } else { // Limit shared cache for wal-redo-postres let mut config = OpenOptions::new() @@ -594,11 +600,16 @@ impl PostgresRedoProcess { .stderr(Stdio::piped()) .stdout(Stdio::piped()) .env_clear() - .env("LD_LIBRARY_PATH", conf.pg_lib_dir().to_str().unwrap()) - .env("DYLD_LIBRARY_PATH", conf.pg_lib_dir().to_str().unwrap()) + .env("LD_LIBRARY_PATH", conf.pg_lib_dir()) + .env("DYLD_LIBRARY_PATH", conf.pg_lib_dir()) .env("PGDATA", &datadir) .spawn() - .expect("postgres --wal-redo command failed to start"); + .map_err(|e| { + Error::new( + e.kind(), + format!("postgres --wal-redo command failed to start: {}", e), + ) + })?; info!( "launched WAL redo postgres process on {:?}", @@ -658,7 +669,10 @@ impl PostgresRedoProcess { { build_apply_record_msg(*lsn, postgres_rec, &mut writebuf); } else { - panic!("tried to pass zenith wal record to postgres WAL redo"); + return Err(Error::new( + ErrorKind::Other, + "tried to pass zenith wal record to postgres WAL redo", + )); } } build_get_page_msg(tag, &mut writebuf); diff --git a/postgres_ffi/Cargo.toml b/postgres_ffi/Cargo.toml index 17f1ecd666..e8d471cb12 100644 --- a/postgres_ffi/Cargo.toml +++ b/postgres_ffi/Cargo.toml @@ -17,8 +17,8 @@ log = "0.4.14" memoffset = "0.6.2" thiserror = "1.0" serde = { version = "1.0", features = ["derive"] } -workspace_hack = { path = "../workspace_hack" } zenith_utils = { path = "../zenith_utils" } +workspace_hack = { version = "0.1", path = "../workspace_hack" } [build-dependencies] bindgen = "0.59.1" diff --git a/postgres_ffi/src/xlog_utils.rs b/postgres_ffi/src/xlog_utils.rs index d2b2b5c122..89fdbbf7ac 100644 --- a/postgres_ffi/src/xlog_utils.rs +++ b/postgres_ffi/src/xlog_utils.rs @@ -495,7 +495,13 @@ mod tests { .env("DYLD_LIBRARY_PATH", &lib_path) .output() .unwrap(); - assert!(initdb_output.status.success()); + assert!( + initdb_output.status.success(), + "initdb failed. Status: '{}', stdout: '{}', stderr: '{}'", + initdb_output.status, + String::from_utf8_lossy(&initdb_output.stdout), + String::from_utf8_lossy(&initdb_output.stderr), + ); // 2. Pick WAL generated by initdb let wal_dir = data_dir.join("pg_wal"); diff --git a/proxy/Cargo.toml b/proxy/Cargo.toml index dda018a1d8..dc20695884 100644 --- a/proxy/Cargo.toml +++ b/proxy/Cargo.toml @@ -22,13 +22,15 @@ rustls = "0.19.1" scopeguard = "1.1.0" serde = "1" serde_json = "1" +socket2 = "0.4.4" thiserror = "1.0" -tokio = { version = "1.11", features = ["macros"] } +tokio = { version = "1.17", features = ["macros"] } tokio-postgres = { git = "https://github.com/zenithdb/rust-postgres.git", rev="2949d98df52587d562986aad155dd4e889e408b7" } tokio-rustls = "0.22.0" zenith_utils = { path = "../zenith_utils" } zenith_metrics = { path = "../zenith_metrics" } +workspace_hack = { version = "0.1", path = "../workspace_hack" } [dev-dependencies] tokio-postgres-rustls = "0.8.0" diff --git a/proxy/src/compute.rs b/proxy/src/compute.rs index 64ce5d0a5a..3c0eee29bc 100644 --- a/proxy/src/compute.rs +++ b/proxy/src/compute.rs @@ -24,7 +24,7 @@ pub enum ConnectionError { impl UserFacingError for ConnectionError {} /// Compute node connection params. -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Default)] pub struct DatabaseInfo { pub host: String, pub port: u16, @@ -33,6 +33,16 @@ pub struct DatabaseInfo { pub password: Option, } +// Manually implement debug to omit personal and sensitive info +impl std::fmt::Debug for DatabaseInfo { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + fmt.debug_struct("DatabaseInfo") + .field("host", &self.host) + .field("port", &self.port) + .finish() + } +} + /// PostgreSQL version as [`String`]. pub type Version = String; @@ -41,6 +51,7 @@ impl DatabaseInfo { let host_port = format!("{}:{}", self.host, self.port); let socket = TcpStream::connect(host_port).await?; let socket_addr = socket.peer_addr()?; + socket2::SockRef::from(&socket).set_keepalive(true)?; Ok((socket_addr, socket)) } diff --git a/proxy/src/mgmt.rs b/proxy/src/mgmt.rs index e53542dfd2..ab6fdff040 100644 --- a/proxy/src/mgmt.rs +++ b/proxy/src/mgmt.rs @@ -107,7 +107,7 @@ impl postgres_backend::Handler for MgmtHandler { } fn try_process_query(pgb: &mut PostgresBackend, query_string: &str) -> anyhow::Result<()> { - println!("Got mgmt query: '{}'", query_string); + println!("Got mgmt query [redacted]"); // Content contains password, don't print it let resp: PsqlSessionResponse = serde_json::from_str(query_string)?; diff --git a/proxy/src/proxy.rs b/proxy/src/proxy.rs index 3c7f59bc26..81581b5cf1 100644 --- a/proxy/src/proxy.rs +++ b/proxy/src/proxy.rs @@ -50,6 +50,10 @@ pub async fn thread_main( println!("proxy has shut down"); } + // When set for the server socket, the keepalive setting + // will be inherited by all accepted client sockets. + socket2::SockRef::from(&listener).set_keepalive(true)?; + let cancel_map = Arc::new(CancelMap::default()); loop { let (socket, peer_addr) = listener.accept().await?; @@ -367,4 +371,24 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn keepalive_is_inherited() -> anyhow::Result<()> { + use tokio::net::{TcpListener, TcpStream}; + + let listener = TcpListener::bind("127.0.0.1:0").await?; + let port = listener.local_addr()?.port(); + socket2::SockRef::from(&listener).set_keepalive(true)?; + + let t = tokio::spawn(async move { + let (client, _) = listener.accept().await?; + let keepalive = socket2::SockRef::from(&client).keepalive()?; + anyhow::Ok(keepalive) + }); + + let _ = TcpStream::connect(("127.0.0.1", port)).await?; + assert!(t.await??, "keepalive should be inherited"); + + Ok(()) + } } diff --git a/scripts/pysync b/scripts/pysync index e548973dea..12fa08beca 100755 --- a/scripts/pysync +++ b/scripts/pysync @@ -4,4 +4,10 @@ # It is intended to be a primary endpoint for all the people who want to # just setup test environment without going into details of python package management -poetry install --no-root # this installs dev dependencies by default +poetry config --list + +if [ -z "${CI}" ]; then + poetry install --no-root --no-interaction --ansi +else + poetry install --no-root +fi diff --git a/test_runner/README.md b/test_runner/README.md index a56c2df2c0..ee171ae6a0 100644 --- a/test_runner/README.md +++ b/test_runner/README.md @@ -10,6 +10,8 @@ Prerequisites: below to run from other directories. - The zenith git repo, including the postgres submodule (for some tests, e.g. `pg_regress`) +- Some tests (involving storage nodes coordination) require etcd installed. Follow + [`the guide`](https://etcd.io/docs/v3.5/install/) to obtain it. ### Test Organization diff --git a/test_runner/batch_others/test_gc_aggressive.py b/test_runner/batch_others/test_gc_aggressive.py index 9de6ba9f59..e4e4aa9f4a 100644 --- a/test_runner/batch_others/test_gc_aggressive.py +++ b/test_runner/batch_others/test_gc_aggressive.py @@ -1,10 +1,7 @@ -from contextlib import closing - import asyncio -import asyncpg import random -from fixtures.zenith_fixtures import ZenithEnv, Postgres, Safekeeper +from fixtures.zenith_fixtures import ZenithEnv, Postgres from fixtures.log_helper import log # Test configuration @@ -76,5 +73,5 @@ def test_gc_aggressive(zenith_simple_env: ZenithEnv): asyncio.run(update_and_gc(env, pg, timeline)) - row = cur.execute('SELECT COUNT(*), SUM(counter) FROM foo') + cur.execute('SELECT COUNT(*), SUM(counter) FROM foo') assert cur.fetchone() == (num_rows, updates_to_perform) diff --git a/test_runner/batch_others/test_next_xid.py b/test_runner/batch_others/test_next_xid.py index fd0f761409..03c27bcd70 100644 --- a/test_runner/batch_others/test_next_xid.py +++ b/test_runner/batch_others/test_next_xid.py @@ -1,9 +1,6 @@ -import pytest -import random import time from fixtures.zenith_fixtures import ZenithEnvBuilder -from fixtures.log_helper import log # Test restarting page server, while safekeeper and compute node keep diff --git a/test_runner/batch_others/test_old_request_lsn.py b/test_runner/batch_others/test_old_request_lsn.py index d09fb24913..e7400cff96 100644 --- a/test_runner/batch_others/test_old_request_lsn.py +++ b/test_runner/batch_others/test_old_request_lsn.py @@ -1,5 +1,3 @@ -from contextlib import closing - from fixtures.zenith_fixtures import ZenithEnv from fixtures.log_helper import log diff --git a/test_runner/batch_others/test_pageserver_api.py b/test_runner/batch_others/test_pageserver_api.py index 2aa3686904..13f6ef358e 100644 --- a/test_runner/batch_others/test_pageserver_api.py +++ b/test_runner/batch_others/test_pageserver_api.py @@ -1,6 +1,6 @@ from uuid import uuid4, UUID import pytest -from fixtures.zenith_fixtures import ZenithEnv, ZenithEnvBuilder, ZenithPageserverHttpClient, zenith_binpath +from fixtures.zenith_fixtures import ZenithEnv, ZenithEnvBuilder, ZenithPageserverHttpClient # test that we cannot override node id @@ -39,10 +39,14 @@ def check_client(client: ZenithPageserverHttpClient, initial_tenant: UUID): timeline_id_str = str(timeline['timeline_id']) timeline_details = client.timeline_detail(tenant_id=tenant_id, timeline_id=UUID(timeline_id_str)) - assert timeline_details['kind'] == 'Local' + assert timeline_details['tenant_id'] == tenant_id.hex assert timeline_details['timeline_id'] == timeline_id_str + local_timeline_details = timeline_details.get('local') + assert local_timeline_details is not None + assert local_timeline_details['timeline_state'] == 'Loaded' + def test_pageserver_http_api_client(zenith_simple_env: ZenithEnv): env = zenith_simple_env diff --git a/test_runner/batch_others/test_pageserver_catchup.py b/test_runner/batch_others/test_pageserver_catchup.py index 7093a1bdb3..758b018046 100644 --- a/test_runner/batch_others/test_pageserver_catchup.py +++ b/test_runner/batch_others/test_pageserver_catchup.py @@ -1,11 +1,4 @@ -import pytest -import random -import time - -from contextlib import closing -from multiprocessing import Process, Value from fixtures.zenith_fixtures import ZenithEnvBuilder -from fixtures.log_helper import log # Test safekeeper sync and pageserver catch up @@ -17,7 +10,9 @@ def test_pageserver_catchup_while_compute_down(zenith_env_builder: ZenithEnvBuil env = zenith_env_builder.init_start() env.zenith_cli.create_branch('test_pageserver_catchup_while_compute_down') - pg = env.postgres.create_start('test_pageserver_catchup_while_compute_down') + # Make shared_buffers large to ensure we won't query pageserver while it is down. + pg = env.postgres.create_start('test_pageserver_catchup_while_compute_down', + config_lines=['shared_buffers=512MB']) pg_conn = pg.connect() cur = pg_conn.cursor() diff --git a/test_runner/batch_others/test_pageserver_restart.py b/test_runner/batch_others/test_pageserver_restart.py index 57f9db8f96..20e6f4467e 100644 --- a/test_runner/batch_others/test_pageserver_restart.py +++ b/test_runner/batch_others/test_pageserver_restart.py @@ -1,9 +1,3 @@ -import pytest -import random -import time - -from contextlib import closing -from multiprocessing import Process, Value from fixtures.zenith_fixtures import ZenithEnvBuilder from fixtures.log_helper import log diff --git a/test_runner/batch_others/test_read_validation.py b/test_runner/batch_others/test_read_validation.py new file mode 100644 index 0000000000..ee41e6511c --- /dev/null +++ b/test_runner/batch_others/test_read_validation.py @@ -0,0 +1,183 @@ +from contextlib import closing + +from fixtures.zenith_fixtures import ZenithEnv +from fixtures.log_helper import log + +from psycopg2.errors import UndefinedTable +from psycopg2.errors import IoError + +pytest_plugins = ("fixtures.zenith_fixtures") + +extensions = ["pageinspect", "zenith_test_utils", "pg_buffercache"] + + +# +# Validation of reading different page versions +# +def test_read_validation(zenith_simple_env: ZenithEnv): + env = zenith_simple_env + env.zenith_cli.create_branch("test_read_validation", "empty") + + pg = env.postgres.create_start("test_read_validation") + log.info("postgres is running on 'test_read_validation' branch") + + with closing(pg.connect()) as con: + with con.cursor() as c: + + for e in extensions: + c.execute("create extension if not exists {};".format(e)) + + c.execute("create table foo (c int) with (autovacuum_enabled = false)") + c.execute("insert into foo values (1)") + + c.execute("select lsn, lower, upper from page_header(get_raw_page('foo', 'main', 0));") + first = c.fetchone() + + c.execute("select relfilenode from pg_class where relname = 'foo'") + relfilenode = c.fetchone()[0] + + c.execute("insert into foo values (2);") + c.execute("select lsn, lower, upper from page_header(get_raw_page('foo', 'main', 0));") + second = c.fetchone() + + assert first != second, "Failed to update page" + + log.info("Test table is populated, validating buffer cache") + + c.execute( + "select count(*) from pg_buffercache where relfilenode = {}".format(relfilenode)) + assert c.fetchone()[0] > 0, "No buffers cached for the test relation" + + c.execute( + "select reltablespace, reldatabase, relfilenode from pg_buffercache where relfilenode = {}" + .format(relfilenode)) + reln = c.fetchone() + + log.info("Clear buffer cache to ensure no stale pages are brought into the cache") + + c.execute("select clear_buffer_cache()") + + c.execute( + "select count(*) from pg_buffercache where relfilenode = {}".format(relfilenode)) + assert c.fetchone()[0] == 0, "Failed to clear buffer cache" + + log.info("Cache is clear, reading stale page version") + + c.execute( + "select lsn, lower, upper from page_header(get_raw_page_at_lsn('foo', 'main', 0, '{}'))" + .format(first[0])) + direct_first = c.fetchone() + assert first == direct_first, "Failed fetch page at historic lsn" + + c.execute( + "select count(*) from pg_buffercache where relfilenode = {}".format(relfilenode)) + assert c.fetchone()[0] == 0, "relation buffers detected after invalidation" + + log.info("Cache is clear, reading latest page version without cache") + + c.execute( + "select lsn, lower, upper from page_header(get_raw_page_at_lsn('foo', 'main', 0, NULL))" + ) + direct_latest = c.fetchone() + assert second == direct_latest, "Failed fetch page at latest lsn" + + c.execute( + "select count(*) from pg_buffercache where relfilenode = {}".format(relfilenode)) + assert c.fetchone()[0] == 0, "relation buffers detected after invalidation" + + log.info( + "Cache is clear, reading stale page version without cache using relation identifiers" + ) + + c.execute( + "select lsn, lower, upper from page_header(get_raw_page_at_lsn( {}, {}, {}, 0, 0, '{}' ))" + .format(reln[0], reln[1], reln[2], first[0])) + direct_first = c.fetchone() + assert first == direct_first, "Failed fetch page at historic lsn using oid" + + log.info( + "Cache is clear, reading latest page version without cache using relation identifiers" + ) + + c.execute( + "select lsn, lower, upper from page_header(get_raw_page_at_lsn( {}, {}, {}, 0, 0, NULL ))" + .format(reln[0], reln[1], reln[2])) + direct_latest = c.fetchone() + assert second == direct_latest, "Failed fetch page at latest lsn" + + c.execute('drop table foo;') + + log.info( + "Relation dropped, attempting reading stale page version without cache using relation identifiers" + ) + + c.execute( + "select lsn, lower, upper from page_header(get_raw_page_at_lsn( {}, {}, {}, 0, 0, '{}' ))" + .format(reln[0], reln[1], reln[2], first[0])) + direct_first = c.fetchone() + assert first == direct_first, "Failed fetch page at historic lsn using oid" + + log.info("Validation page inspect won't allow reading pages of dropped relations") + try: + c.execute("select * from page_header(get_raw_page('foo', 'main', 0));") + assert False, "query should have failed" + except UndefinedTable as e: + log.info("Caught an expected failure: {}".format(e)) + + +def test_read_validation_neg(zenith_simple_env: ZenithEnv): + env = zenith_simple_env + env.zenith_cli.create_branch("test_read_validation_neg", "empty") + + pg = env.postgres.create_start("test_read_validation_neg") + log.info("postgres is running on 'test_read_validation_neg' branch") + + with closing(pg.connect()) as con: + with con.cursor() as c: + + for e in extensions: + c.execute("create extension if not exists {};".format(e)) + + log.info("read a page of a missing relation") + try: + c.execute( + "select lsn, lower, upper from page_header(get_raw_page_at_lsn('Unknown', 'main', 0, '0/0'))" + ) + assert False, "query should have failed" + except UndefinedTable as e: + log.info("Caught an expected failure: {}".format(e)) + + c.execute("create table foo (c int) with (autovacuum_enabled = false)") + c.execute("insert into foo values (1)") + + log.info("read a page at lsn 0") + try: + c.execute( + "select lsn, lower, upper from page_header(get_raw_page_at_lsn('foo', 'main', 0, '0/0'))" + ) + assert False, "query should have failed" + except IoError as e: + log.info("Caught an expected failure: {}".format(e)) + + log.info("Pass NULL as an input") + expected = (None, None, None) + c.execute( + "select lsn, lower, upper from page_header(get_raw_page_at_lsn(NULL, 'main', 0, '0/0'))" + ) + assert c.fetchone() == expected, "Expected null output" + + c.execute( + "select lsn, lower, upper from page_header(get_raw_page_at_lsn('foo', NULL, 0, '0/0'))" + ) + assert c.fetchone() == expected, "Expected null output" + + c.execute( + "select lsn, lower, upper from page_header(get_raw_page_at_lsn('foo', 'main', NULL, '0/0'))" + ) + assert c.fetchone() == expected, "Expected null output" + + # This check is currently failing, reading beyond EOF is returning a 0-page + log.info("Read beyond EOF") + c.execute( + "select lsn, lower, upper from page_header(get_raw_page_at_lsn('foo', 'main', 1, NULL))" + ) diff --git a/test_runner/batch_others/test_remote_storage.py b/test_runner/batch_others/test_remote_storage.py index edcc768819..e762f8589a 100644 --- a/test_runner/batch_others/test_remote_storage.py +++ b/test_runner/batch_others/test_remote_storage.py @@ -1,12 +1,13 @@ # It's possible to run any regular test with the local fs remote storage via # env ZENITH_PAGESERVER_OVERRIDES="remote_storage={local_path='/tmp/zenith_zzz/'}" poetry ...... -import time, shutil, os +import shutil, os from contextlib import closing from pathlib import Path from uuid import UUID -from fixtures.zenith_fixtures import ZenithEnvBuilder +from fixtures.zenith_fixtures import ZenithEnvBuilder, assert_local, wait_for, wait_for_last_record_lsn, wait_for_upload from fixtures.log_helper import log +from fixtures.utils import lsn_from_hex import pytest @@ -26,7 +27,6 @@ import pytest # * queries the specific data, ensuring that it matches the one stored before # # The tests are done for all types of remote storage pageserver supports. -@pytest.mark.skip(reason="will be fixed with https://github.com/zenithdb/zenith/issues/1193") @pytest.mark.parametrize('storage_type', ['local_fs', 'mock_s3']) def test_remote_storage_backup_and_restore(zenith_env_builder: ZenithEnvBuilder, storage_type: str): zenith_env_builder.rust_log_override = 'debug' @@ -45,6 +45,8 @@ def test_remote_storage_backup_and_restore(zenith_env_builder: ZenithEnvBuilder, env = zenith_env_builder.init_start() pg = env.postgres.create_start('main') + client = env.pageserver.http_client() + tenant_id = pg.safe_psql("show zenith.zenith_tenant")[0][0] timeline_id = pg.safe_psql("show zenith.zenith_timeline")[0][0] @@ -54,13 +56,21 @@ def test_remote_storage_backup_and_restore(zenith_env_builder: ZenithEnvBuilder, CREATE TABLE t1(id int primary key, secret text); INSERT INTO t1 VALUES ({data_id}, '{data_secret}'); ''') + cur.execute("SELECT pg_current_wal_flush_lsn()") + current_lsn = lsn_from_hex(cur.fetchone()[0]) + + # wait until pageserver receives that data + wait_for_last_record_lsn(client, UUID(tenant_id), UUID(timeline_id), current_lsn) # run checkpoint manually to be sure that data landed in remote storage with closing(env.pageserver.connect()) as psconn: with psconn.cursor() as pscur: - pscur.execute(f"do_gc {tenant_id} {timeline_id}") - log.info("waiting for upload") # TODO api to check if upload is done - time.sleep(2) + pscur.execute(f"checkpoint {tenant_id} {timeline_id}") + + log.info("waiting for upload") + # wait until pageserver successfully uploaded a checkpoint to remote storage + wait_for_upload(client, UUID(tenant_id), UUID(timeline_id), current_lsn) + log.info("upload is done") ##### Stop the first pageserver instance, erase all its data env.postgres.stop_all() @@ -73,26 +83,12 @@ def test_remote_storage_backup_and_restore(zenith_env_builder: ZenithEnvBuilder, ##### Second start, restore the data and ensure it's the same env.pageserver.start() - client = env.pageserver.http_client() client.timeline_attach(UUID(tenant_id), UUID(timeline_id)) - # FIXME cannot handle duplicate download requests (which might be caused by repeated timeline detail calls) - # subject to fix in https://github.com/zenithdb/zenith/issues/997 - time.sleep(5) log.info("waiting for timeline redownload") - attempts = 0 - while True: - timeline_details = client.timeline_detail(UUID(tenant_id), UUID(timeline_id)) - assert timeline_details['timeline_id'] == timeline_id - assert timeline_details['tenant_id'] == tenant_id - if timeline_details['kind'] == 'Local': - log.info("timeline downloaded, checking its data") - break - attempts += 1 - if attempts > 10: - raise Exception("timeline redownload failed") - log.debug("still waiting") - time.sleep(1) + wait_for(number_of_iterations=10, + interval=1, + func=lambda: assert_local(client, UUID(tenant_id), UUID(timeline_id))) pg = env.postgres.create_start('main') with closing(pg.connect()) as conn: diff --git a/test_runner/batch_others/test_tenant_relocation.py b/test_runner/batch_others/test_tenant_relocation.py index 7a9d478f16..8213d2526b 100644 --- a/test_runner/batch_others/test_tenant_relocation.py +++ b/test_runner/batch_others/test_tenant_relocation.py @@ -5,15 +5,15 @@ import subprocess import threading from uuid import UUID from fixtures.log_helper import log -import time import signal import pytest -from fixtures.zenith_fixtures import PgProtocol, PortDistributor, Postgres, ZenithEnvBuilder, ZenithPageserverHttpClient, zenith_binpath, pg_distrib_dir +from fixtures.zenith_fixtures import PgProtocol, PortDistributor, Postgres, ZenithEnvBuilder, ZenithPageserverHttpClient, assert_local, wait_for, wait_for_last_record_lsn, wait_for_upload, zenith_binpath, pg_distrib_dir +from fixtures.utils import lsn_from_hex def assert_abs_margin_ratio(a: float, b: float, margin_ratio: float): - assert abs(a - b) / a < margin_ratio, (a, b, margin_ratio) + assert abs(a - b) / a < margin_ratio, abs(a - b) / a @contextmanager @@ -34,6 +34,7 @@ def new_pageserver_helper(new_pageserver_dir: pathlib.Path, f"-c listen_pg_addr='localhost:{pg_port}'", f"-c listen_http_addr='localhost:{http_port}'", f"-c pg_distrib_dir='{pg_distrib_dir}'", + f"-c id=2", f"-c remote_storage={{local_path='{remote_storage_mock_path}'}}", ] @@ -57,20 +58,6 @@ def new_pageserver_helper(new_pageserver_dir: pathlib.Path, os.kill(pid, signal.SIGQUIT) -def wait_for(number_of_iterations: int, interval: int, func): - last_exception = None - for i in range(number_of_iterations): - try: - res = func() - except Exception as e: - log.info("waiting for %s iteration %s failed", func, i + 1) - last_exception = e - time.sleep(interval) - continue - return res - raise Exception("timed out while waiting for %s" % func) from last_exception - - @contextmanager def pg_cur(pg): with closing(pg.connect()) as conn: @@ -108,13 +95,6 @@ def load(pg: Postgres, stop_event: threading.Event, load_ok_event: threading.Eve log.info('load thread stopped') -def assert_local(pageserver_http_client: ZenithPageserverHttpClient, tenant: UUID, timeline: str): - timeline_detail = pageserver_http_client.timeline_detail(tenant, UUID(timeline)) - assert timeline_detail.get('type') == "Local", timeline_detail - return timeline_detail - - -@pytest.mark.skip(reason="will be fixed with https://github.com/zenithdb/zenith/issues/1193") @pytest.mark.parametrize('with_load', ['with_load', 'without_load']) def test_tenant_relocation(zenith_env_builder: ZenithEnvBuilder, port_distributor: PortDistributor, @@ -129,7 +109,7 @@ def test_tenant_relocation(zenith_env_builder: ZenithEnvBuilder, tenant = env.zenith_cli.create_tenant(UUID("74ee8b079a0e437eb0afea7d26a07209")) log.info("tenant to relocate %s", tenant) - + env.zenith_cli.create_root_branch('main', tenant_id=tenant) env.zenith_cli.create_branch('test_tenant_relocation', tenant_id=tenant) tenant_pg = env.postgres.create_start(branch_name='main', @@ -141,8 +121,8 @@ def test_tenant_relocation(zenith_env_builder: ZenithEnvBuilder, with conn.cursor() as cur: # save timeline for later gc call cur.execute("SHOW zenith.zenith_timeline") - timeline = cur.fetchone()[0] - log.info("timeline to relocate %s", timeline) + timeline = UUID(cur.fetchone()[0]) + log.info("timeline to relocate %s", timeline.hex) # we rely upon autocommit after each statement # as waiting for acceptors happens there @@ -150,6 +130,15 @@ def test_tenant_relocation(zenith_env_builder: ZenithEnvBuilder, cur.execute("INSERT INTO t SELECT generate_series(1,1000), 'some payload'") cur.execute("SELECT sum(key) FROM t") assert cur.fetchone() == (500500, ) + cur.execute("SELECT pg_current_wal_flush_lsn()") + + current_lsn = lsn_from_hex(cur.fetchone()[0]) + + pageserver_http = env.pageserver.http_client() + + # wait until pageserver receives that data + wait_for_last_record_lsn(pageserver_http, tenant, timeline, current_lsn) + timeline_detail = assert_local(pageserver_http, tenant, timeline) if with_load == 'with_load': # create load table @@ -165,12 +154,10 @@ def test_tenant_relocation(zenith_env_builder: ZenithEnvBuilder, # run checkpoint manually to be sure that data landed in remote storage with closing(env.pageserver.connect()) as psconn: with psconn.cursor() as pscur: - pscur.execute(f"do_gc {tenant.hex} {timeline}") + pscur.execute(f"checkpoint {tenant.hex} {timeline.hex}") - # ensure upload is completed - pageserver_http_client = env.pageserver.http_client() - timeline_detail = pageserver_http_client.timeline_detail(tenant, UUID(timeline)) - assert timeline_detail['disk_consistent_lsn'] == timeline_detail['timeline_state']['Ready'] + # wait until pageserver successfully uploaded a checkpoint to remote storage + wait_for_upload(pageserver_http, tenant, timeline, current_lsn) log.info("inititalizing new pageserver") # bootstrap second pageserver @@ -182,8 +169,7 @@ def test_tenant_relocation(zenith_env_builder: ZenithEnvBuilder, log.info("new pageserver ports pg %s http %s", new_pageserver_pg_port, new_pageserver_http_port) pageserver_bin = pathlib.Path(zenith_binpath) / 'pageserver' - new_pageserver_http_client = ZenithPageserverHttpClient(port=new_pageserver_http_port, - auth_token=None) + new_pageserver_http = ZenithPageserverHttpClient(port=new_pageserver_http_port, auth_token=None) with new_pageserver_helper(new_pageserver_dir, pageserver_bin, @@ -192,25 +178,18 @@ def test_tenant_relocation(zenith_env_builder: ZenithEnvBuilder, new_pageserver_http_port): # call to attach timeline to new pageserver - new_pageserver_http_client.timeline_attach(tenant, UUID(timeline)) - # FIXME cannot handle duplicate download requests, subject to fix in https://github.com/zenithdb/zenith/issues/997 - time.sleep(5) - # new pageserver should in sync (modulo wal tail or vacuum activity) with the old one because there was no new writes since checkpoint + new_pageserver_http.timeline_attach(tenant, timeline) + # new pageserver should be in sync (modulo wal tail or vacuum activity) with the old one because there was no new writes since checkpoint new_timeline_detail = wait_for( number_of_iterations=5, interval=1, - func=lambda: assert_local(new_pageserver_http_client, tenant, timeline)) - assert new_timeline_detail['timeline_state'].get('Ready'), new_timeline_detail + func=lambda: assert_local(new_pageserver_http, tenant, timeline)) + # when load is active these checks can break because lsns are not static # so lets check with some margin - if with_load == 'without_load': - # TODO revisit this once https://github.com/zenithdb/zenith/issues/1049 is fixed - assert_abs_margin_ratio(new_timeline_detail['disk_consistent_lsn'], - timeline_detail['disk_consistent_lsn'], - 0.01) - assert_abs_margin_ratio(new_timeline_detail['timeline_state']['Ready'], - timeline_detail['timeline_state']['Ready'], - 0.01) + assert_abs_margin_ratio(lsn_from_hex(new_timeline_detail['local']['disk_consistent_lsn']), + lsn_from_hex(timeline_detail['local']['disk_consistent_lsn']), + 0.03) # callmemaybe to start replication from safekeeper to the new pageserver # when there is no load there is a clean checkpoint and no wal delta @@ -219,7 +198,9 @@ def test_tenant_relocation(zenith_env_builder: ZenithEnvBuilder, with pg_cur(PgProtocol(host='localhost', port=new_pageserver_pg_port)) as cur: # "callmemaybe {} {} host={} port={} options='-c ztimelineid={} ztenantid={}'" safekeeper_connstring = f"host=localhost port={env.safekeepers[0].port.pg} options='-c ztimelineid={timeline} ztenantid={tenant} pageserver_connstr=postgresql://no_user:@localhost:{new_pageserver_pg_port}'" - cur.execute("callmemaybe {} {} {}".format(tenant, timeline, safekeeper_connstring)) + cur.execute("callmemaybe {} {} {}".format(tenant.hex, + timeline.hex, + safekeeper_connstring)) tenant_pg.stop() @@ -239,7 +220,7 @@ def test_tenant_relocation(zenith_env_builder: ZenithEnvBuilder, # detach tenant from old pageserver before we check # that all the data is there to be sure that old pageserver # is no longer involved, and if it is, we will see the errors - pageserver_http_client.timeline_detach(tenant, UUID(timeline)) + pageserver_http.timeline_detach(tenant, timeline) with pg_cur(tenant_pg) as cur: # check that data is still there @@ -251,10 +232,10 @@ def test_tenant_relocation(zenith_env_builder: ZenithEnvBuilder, assert cur.fetchone() == (2001000, ) if with_load == 'with_load': - assert load_ok_event.wait(1) + assert load_ok_event.wait(3) log.info('stopping load thread') load_stop_event.set() - load_thread.join() + load_thread.join(timeout=10) log.info('load thread stopped') # bring old pageserver back for clean shutdown via zenith cli diff --git a/test_runner/batch_others/test_timeline_size.py b/test_runner/batch_others/test_timeline_size.py index 7d8ab551b0..db33493d61 100644 --- a/test_runner/batch_others/test_timeline_size.py +++ b/test_runner/batch_others/test_timeline_size.py @@ -1,8 +1,7 @@ from contextlib import closing -from uuid import UUID import psycopg2.extras import psycopg2.errors -from fixtures.zenith_fixtures import ZenithEnv, ZenithEnvBuilder, Postgres +from fixtures.zenith_fixtures import ZenithEnv, ZenithEnvBuilder, Postgres, assert_local from fixtures.log_helper import log import time @@ -13,8 +12,9 @@ def test_timeline_size(zenith_simple_env: ZenithEnv): new_timeline_id = env.zenith_cli.create_branch('test_timeline_size', 'empty') client = env.pageserver.http_client() - res = client.timeline_detail(tenant_id=env.initial_tenant, timeline_id=new_timeline_id) - assert res["current_logical_size"] == res["current_logical_size_non_incremental"] + timeline_details = assert_local(client, env.initial_tenant, new_timeline_id) + assert timeline_details['local']['current_logical_size'] == timeline_details['local'][ + 'current_logical_size_non_incremental'] pgmain = env.postgres.create_start("test_timeline_size") log.info("postgres is running on 'test_timeline_size' branch") @@ -31,12 +31,16 @@ def test_timeline_size(zenith_simple_env: ZenithEnv): FROM generate_series(1, 10) g """) - res = client.timeline_detail(tenant_id=env.initial_tenant, timeline_id=new_timeline_id) - assert res["current_logical_size"] == res["current_logical_size_non_incremental"] + res = assert_local(client, env.initial_tenant, new_timeline_id) + local_details = res['local'] + assert local_details["current_logical_size"] == local_details[ + "current_logical_size_non_incremental"] cur.execute("TRUNCATE foo") - res = client.timeline_detail(tenant_id=env.initial_tenant, timeline_id=new_timeline_id) - assert res["current_logical_size"] == res["current_logical_size_non_incremental"] + res = assert_local(client, env.initial_tenant, new_timeline_id) + local_details = res['local'] + assert local_details["current_logical_size"] == local_details[ + "current_logical_size_non_incremental"] # wait until received_lsn_lag is 0 @@ -71,8 +75,9 @@ def test_timeline_size_quota(zenith_env_builder: ZenithEnvBuilder): new_timeline_id = env.zenith_cli.create_branch('test_timeline_size_quota') client = env.pageserver.http_client() - res = client.timeline_detail(tenant_id=env.initial_tenant, timeline_id=new_timeline_id) - assert res["current_logical_size"] == res["current_logical_size_non_incremental"] + res = assert_local(client, env.initial_tenant, new_timeline_id) + assert res['local']["current_logical_size"] == res['local'][ + "current_logical_size_non_incremental"] pgmain = env.postgres.create_start( "test_timeline_size_quota", diff --git a/test_runner/batch_others/test_wal_acceptor.py b/test_runner/batch_others/test_wal_acceptor.py index bdc4c4f63c..8f87ff041f 100644 --- a/test_runner/batch_others/test_wal_acceptor.py +++ b/test_runner/batch_others/test_wal_acceptor.py @@ -13,7 +13,7 @@ from dataclasses import dataclass, field from multiprocessing import Process, Value from pathlib import Path from fixtures.zenith_fixtures import PgBin, Postgres, Safekeeper, ZenithEnv, ZenithEnvBuilder, PortDistributor, SafekeeperPort, zenith_binpath, PgProtocol -from fixtures.utils import lsn_to_hex, mkdir_if_needed, lsn_from_hex +from fixtures.utils import etcd_path, lsn_to_hex, mkdir_if_needed, lsn_from_hex from fixtures.log_helper import log from typing import List, Optional, Any @@ -22,6 +22,7 @@ from typing import List, Optional, Any # succeed and data is written def test_normal_work(zenith_env_builder: ZenithEnvBuilder): zenith_env_builder.num_safekeepers = 3 + zenith_env_builder.broker = True env = zenith_env_builder.init_start() env.zenith_cli.create_branch('test_wal_acceptors_normal_work') @@ -89,29 +90,33 @@ def test_many_timelines(zenith_env_builder: ZenithEnvBuilder): sk_metrics = [sk.http_client().get_metrics() for sk in env.safekeepers] timeline_metrics = [] - with env.pageserver.http_client() as pageserver_http: - for timeline_detail in timeline_details: - timeline_id: str = timeline_detail["timeline_id"] + for timeline_detail in timeline_details: + timeline_id: str = timeline_detail["timeline_id"] - m = TimelineMetrics( - timeline_id=timeline_id, - last_record_lsn=lsn_from_hex(timeline_detail["last_record_lsn"]), - ) - for sk_m in sk_metrics: - m.flush_lsns.append(sk_m.flush_lsn_inexact[(tenant_id.hex, timeline_id)]) - m.commit_lsns.append(sk_m.commit_lsn_inexact[(tenant_id.hex, timeline_id)]) + local_timeline_detail = timeline_detail.get('local') + if local_timeline_detail is None: + log.debug(f"Timeline {timeline_id} is not present locally, skipping") + continue - for flush_lsn, commit_lsn in zip(m.flush_lsns, m.commit_lsns): - # Invariant. May be < when transaction is in progress. - assert commit_lsn <= flush_lsn - # We only call collect_metrics() after a transaction is confirmed by - # the compute node, which only happens after a consensus of safekeepers - # has confirmed the transaction. We assume majority consensus here. - assert (2 * sum(m.last_record_lsn <= lsn - for lsn in m.flush_lsns) > zenith_env_builder.num_safekeepers) - assert (2 * sum(m.last_record_lsn <= lsn - for lsn in m.commit_lsns) > zenith_env_builder.num_safekeepers) - timeline_metrics.append(m) + m = TimelineMetrics( + timeline_id=timeline_id, + last_record_lsn=lsn_from_hex(local_timeline_detail['last_record_lsn']), + ) + for sk_m in sk_metrics: + m.flush_lsns.append(sk_m.flush_lsn_inexact[(tenant_id.hex, timeline_id)]) + m.commit_lsns.append(sk_m.commit_lsn_inexact[(tenant_id.hex, timeline_id)]) + + for flush_lsn, commit_lsn in zip(m.flush_lsns, m.commit_lsns): + # Invariant. May be < when transaction is in progress. + assert commit_lsn <= flush_lsn, f"timeline_id={timeline_id}, timeline_detail={timeline_detail}, sk_metrics={sk_metrics}" + # We only call collect_metrics() after a transaction is confirmed by + # the compute node, which only happens after a consensus of safekeepers + # has confirmed the transaction. We assume majority consensus here. + assert (2 * sum(m.last_record_lsn <= lsn + for lsn in m.flush_lsns) > zenith_env_builder.num_safekeepers), f"timeline_id={timeline_id}, timeline_detail={timeline_detail}, sk_metrics={sk_metrics}" + assert (2 * sum(m.last_record_lsn <= lsn + for lsn in m.commit_lsns) > zenith_env_builder.num_safekeepers), f"timeline_id={timeline_id}, timeline_detail={timeline_detail}, sk_metrics={sk_metrics}" + timeline_metrics.append(m) log.info(f"{message}: {timeline_metrics}") return timeline_metrics @@ -322,6 +327,49 @@ def test_race_conditions(zenith_env_builder: ZenithEnvBuilder, stop_value): proc.join() +# Test that safekeepers push their info to the broker and learn peer status from it +@pytest.mark.skipif(etcd_path() is None, reason="requires etcd which is not present in PATH") +def test_broker(zenith_env_builder: ZenithEnvBuilder): + zenith_env_builder.num_safekeepers = 3 + zenith_env_builder.broker = True + zenith_env_builder.enable_local_fs_remote_storage() + env = zenith_env_builder.init_start() + + env.zenith_cli.create_branch("test_broker", "main") + pg = env.postgres.create_start('test_broker') + pg.safe_psql("CREATE TABLE t(key int primary key, value text)") + + # learn zenith timeline from compute + tenant_id = pg.safe_psql("show zenith.zenith_tenant")[0][0] + timeline_id = pg.safe_psql("show zenith.zenith_timeline")[0][0] + + # wait until remote_consistent_lsn gets advanced on all safekeepers + clients = [sk.http_client() for sk in env.safekeepers] + stat_before = [cli.timeline_status(tenant_id, timeline_id) for cli in clients] + log.info(f"statuses is {stat_before}") + + pg.safe_psql("INSERT INTO t SELECT generate_series(1,100), 'payload'") + # force checkpoint to advance remote_consistent_lsn + with closing(env.pageserver.connect()) as psconn: + with psconn.cursor() as pscur: + pscur.execute(f"checkpoint {tenant_id} {timeline_id}") + # and wait till remote_consistent_lsn propagates to all safekeepers + started_at = time.time() + while True: + stat_after = [cli.timeline_status(tenant_id, timeline_id) for cli in clients] + if all( + lsn_from_hex(s_after.remote_consistent_lsn) > lsn_from_hex( + s_before.remote_consistent_lsn) for s_after, + s_before in zip(stat_after, stat_before)): + break + elapsed = time.time() - started_at + if elapsed > 20: + raise RuntimeError( + f"timed out waiting {elapsed:.0f}s for remote_consistent_lsn propagation: status before {stat_before}, status current {stat_after}" + ) + time.sleep(0.5) + + class ProposerPostgres(PgProtocol): """Object for running postgres without ZenithEnv""" def __init__(self, diff --git a/test_runner/batch_others/test_wal_acceptor_async.py b/test_runner/batch_others/test_wal_acceptor_async.py index 31ace7eab3..aadafc76cf 100644 --- a/test_runner/batch_others/test_wal_acceptor_async.py +++ b/test_runner/batch_others/test_wal_acceptor_async.py @@ -1,9 +1,10 @@ import asyncio +import uuid import asyncpg import random import time -from fixtures.zenith_fixtures import ZenithEnvBuilder, Postgres, Safekeeper +from fixtures.zenith_fixtures import ZenithEnv, ZenithEnvBuilder, Postgres, Safekeeper from fixtures.log_helper import getLogger from fixtures.utils import lsn_from_hex, lsn_to_hex from typing import List @@ -30,10 +31,6 @@ class BankClient(object): await self.conn.execute('DROP TABLE IF EXISTS bank_log') await self.conn.execute('CREATE TABLE bank_log(from_uid int, to_uid int, amount int)') - # TODO: Remove when https://github.com/zenithdb/zenith/issues/644 is fixed - await self.conn.execute('ALTER TABLE bank_accs SET (autovacuum_enabled = false)') - await self.conn.execute('ALTER TABLE bank_log SET (autovacuum_enabled = false)') - async def check_invariant(self): row = await self.conn.fetchrow('SELECT sum(amount) AS sum FROM bank_accs') assert row['sum'] == self.n_accounts * self.init_amount @@ -139,12 +136,15 @@ async def wait_for_lsn(safekeeper: Safekeeper, # On each iteration 1 acceptor is stopped, and 2 others should allow # background workers execute transactions. In the end, state should remain # consistent. -async def run_restarts_under_load(pg: Postgres, acceptors: List[Safekeeper], n_workers=10): +async def run_restarts_under_load(env: ZenithEnv, + pg: Postgres, + acceptors: List[Safekeeper], + n_workers=10): n_accounts = 100 init_amount = 100000 max_transfer = 100 - period_time = 10 - iterations = 6 + period_time = 4 + iterations = 10 # Set timeout for this test at 5 minutes. It should be enough for test to complete # and less than CircleCI's no_output_timeout, taking into account that this timeout @@ -176,6 +176,11 @@ async def run_restarts_under_load(pg: Postgres, acceptors: List[Safekeeper], n_w flush_lsn = lsn_to_hex(flush_lsn) log.info(f'Postgres flush_lsn {flush_lsn}') + pageserver_lsn = env.pageserver.http_client().timeline_detail( + uuid.UUID(tenant_id), uuid.UUID((timeline_id)))["local"]["last_record_lsn"] + sk_ps_lag = lsn_from_hex(flush_lsn) - lsn_from_hex(pageserver_lsn) + log.info(f'Pageserver last_record_lsn={pageserver_lsn} lag={sk_ps_lag / 1024}kb') + # Wait until alive safekeepers catch up with postgres for idx, safekeeper in enumerate(acceptors): if idx != victim_idx: @@ -203,9 +208,8 @@ def test_restarts_under_load(zenith_env_builder: ZenithEnvBuilder): env = zenith_env_builder.init_start() env.zenith_cli.create_branch('test_wal_acceptors_restarts_under_load') - pg = env.postgres.create_start('test_wal_acceptors_restarts_under_load') + # Enable backpressure with 1MB maximal lag, because we don't want to block on `wait_for_lsn()` for too long + pg = env.postgres.create_start('test_wal_acceptors_restarts_under_load', + config_lines=['max_replication_write_lag=1MB']) - asyncio.run(run_restarts_under_load(pg, env.safekeepers)) - - # TODO: Remove when https://github.com/zenithdb/zenith/issues/644 is fixed - pg.stop() + asyncio.run(run_restarts_under_load(env, pg, env.safekeepers)) diff --git a/test_runner/batch_others/test_wal_restore.py b/test_runner/batch_others/test_wal_restore.py new file mode 100644 index 0000000000..a5855f2258 --- /dev/null +++ b/test_runner/batch_others/test_wal_restore.py @@ -0,0 +1,38 @@ +import os +import subprocess + +from fixtures.utils import mkdir_if_needed +from fixtures.zenith_fixtures import (ZenithEnvBuilder, + VanillaPostgres, + PortDistributor, + PgBin, + base_dir, + vanilla_pg, + pg_distrib_dir) +from fixtures.log_helper import log + + +def test_wal_restore(zenith_env_builder: ZenithEnvBuilder, + test_output_dir, + port_distributor: PortDistributor): + zenith_env_builder.num_safekeepers = 1 + env = zenith_env_builder.init_start() + env.zenith_cli.create_branch("test_wal_restore") + pg = env.postgres.create_start('test_wal_restore') + pg.safe_psql("create table t as select generate_series(1,1000000)") + tenant_id = pg.safe_psql("show zenith.zenith_tenant")[0][0] + env.zenith_cli.pageserver_stop() + port = port_distributor.get_port() + data_dir = os.path.join(test_output_dir, 'pgsql.restored') + restored = VanillaPostgres(data_dir, PgBin(test_output_dir), port) + subprocess.call([ + 'bash', + os.path.join(base_dir, 'zenith_utils/scripts/restore_from_wal.sh'), + os.path.join(pg_distrib_dir, 'bin'), + os.path.join(test_output_dir, 'repo/safekeepers/sk1/{}/*'.format(tenant_id)), + data_dir, + str(port) + ]) + restored.start() + assert restored.safe_psql('select count(*) from t') == [(1000000, )] + restored.stop() diff --git a/test_runner/batch_others/test_zenith_cli.py b/test_runner/batch_others/test_zenith_cli.py index 4a62a1430a..091d9ac8ba 100644 --- a/test_runner/batch_others/test_zenith_cli.py +++ b/test_runner/batch_others/test_zenith_cli.py @@ -1,8 +1,6 @@ -import json import uuid import requests -from psycopg2.extensions import cursor as PgCursor from fixtures.zenith_fixtures import ZenithEnv, ZenithEnvBuilder, ZenithPageserverHttpClient from typing import cast diff --git a/test_runner/fixtures/utils.py b/test_runner/fixtures/utils.py index 58f7294eb5..f16fe1d9cf 100644 --- a/test_runner/fixtures/utils.py +++ b/test_runner/fixtures/utils.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Any, List @@ -76,3 +77,8 @@ def print_gc_result(row): log.info( " total: {layers_total}, needed_by_cutoff {layers_needed_by_cutoff}, needed_by_branches: {layers_needed_by_branches}, not_updated: {layers_not_updated}, removed: {layers_removed}" .format_map(row)) + + +# path to etcd binary or None if not present. +def etcd_path(): + return shutil.which("etcd") diff --git a/test_runner/fixtures/zenith_fixtures.py b/test_runner/fixtures/zenith_fixtures.py index 073ef9f6c5..7bab6d65c0 100644 --- a/test_runner/fixtures/zenith_fixtures.py +++ b/test_runner/fixtures/zenith_fixtures.py @@ -33,7 +33,7 @@ from typing_extensions import Literal import requests import backoff # type: ignore -from .utils import (get_self_dir, mkdir_if_needed, subprocess_capture) +from .utils import (etcd_path, get_self_dir, mkdir_if_needed, subprocess_capture, lsn_from_hex) from fixtures.log_helper import log """ This file contains pytest fixtures. A fixture is a test resource that can be @@ -257,7 +257,8 @@ class PgProtocol: dbname: Optional[str] = None, schema: Optional[str] = None, username: Optional[str] = None, - password: Optional[str] = None) -> str: + password: Optional[str] = None, + statement_timeout_ms: Optional[int] = None) -> str: """ Build a libpq connection string for the Postgres instance. """ @@ -277,16 +278,23 @@ class PgProtocol: if schema: res = f"{res} options='-c search_path={schema}'" + if statement_timeout_ms: + res = f"{res} options='-c statement_timeout={statement_timeout_ms}'" + return res # autocommit=True here by default because that's what we need most of the time - def connect(self, - *, - autocommit=True, - dbname: Optional[str] = None, - schema: Optional[str] = None, - username: Optional[str] = None, - password: Optional[str] = None) -> PgConnection: + def connect( + self, + *, + autocommit=True, + dbname: Optional[str] = None, + schema: Optional[str] = None, + username: Optional[str] = None, + password: Optional[str] = None, + # individual statement timeout in seconds, 2 minutes should be enough for our tests + statement_timeout: Optional[int] = 120 + ) -> PgConnection: """ Connect to the node. Returns psycopg2's connection object. @@ -294,12 +302,12 @@ class PgProtocol: """ conn = psycopg2.connect( - self.connstr( - dbname=dbname, - schema=schema, - username=username, - password=password, - )) + self.connstr(dbname=dbname, + schema=schema, + username=username, + password=password, + statement_timeout_ms=statement_timeout * + 1000 if statement_timeout else None)) # WARNING: this setting affects *all* tests! conn.autocommit = autocommit return conn @@ -425,7 +433,8 @@ class ZenithEnvBuilder: num_safekeepers: int = 0, pageserver_auth_enabled: bool = False, rust_log_override: Optional[str] = None, - default_branch_name=DEFAULT_BRANCH_NAME): + default_branch_name=DEFAULT_BRANCH_NAME, + broker: bool = False): self.repo_dir = repo_dir self.rust_log_override = rust_log_override self.port_distributor = port_distributor @@ -434,6 +443,7 @@ class ZenithEnvBuilder: self.num_safekeepers = num_safekeepers self.pageserver_auth_enabled = pageserver_auth_enabled self.default_branch_name = default_branch_name + self.broker = broker self.env: Optional[ZenithEnv] = None self.s3_mock_server: Optional[MockS3Server] = None @@ -509,6 +519,8 @@ class ZenithEnvBuilder: self.env.pageserver.stop(immediate=True) if self.s3_mock_server: self.s3_mock_server.kill() + if self.env.broker is not None: + self.env.broker.stop() class ZenithEnv: @@ -561,6 +573,16 @@ class ZenithEnv: default_tenant_id = '{self.initial_tenant.hex}' """) + self.broker = None + if config.broker: + # keep etcd datadir inside 'repo' + self.broker = Etcd(datadir=os.path.join(self.repo_dir, "etcd"), + port=self.port_distributor.get_port(), + peer_port=self.port_distributor.get_port()) + toml += textwrap.dedent(f""" + broker_endpoints = 'http://127.0.0.1:{self.broker.port}' + """) + # Create config for pageserver pageserver_port = PageserverPort( pg=self.port_distributor.get_port(), @@ -603,12 +625,15 @@ class ZenithEnv: self.zenith_cli.init(toml) def start(self): - # Start up the page server and all the safekeepers + # Start up the page server, all the safekeepers and the broker self.pageserver.start() for safekeeper in self.safekeepers: safekeeper.start() + if self.broker is not None: + self.broker.start() + def get_safekeeper_connstrs(self) -> str: """ Get list of safekeeper endpoints suitable for wal_acceptors GUC """ return ','.join([f'localhost:{wa.port.pg}' for wa in self.safekeepers]) @@ -873,6 +898,30 @@ class ZenithCli: return uuid.UUID(created_timeline_id) + def create_root_branch(self, branch_name: str, tenant_id: Optional[uuid.UUID] = None): + cmd = [ + 'timeline', + 'create', + '--branch-name', + branch_name, + '--tenant-id', + (tenant_id or self.env.initial_tenant).hex, + ] + + res = self.raw_cli(cmd) + res.check_returncode() + + matches = CREATE_TIMELINE_ID_EXTRACTOR.search(res.stdout) + + created_timeline_id = None + if matches is not None: + created_timeline_id = matches.group('timeline_id') + + if created_timeline_id is None: + raise Exception('could not find timeline id after `zenith timeline create` invocation') + else: + return uuid.UUID(created_timeline_id) + def create_branch(self, new_branch_name: str = DEFAULT_BRANCH_NAME, ancestor_branch_name: Optional[str] = None, @@ -1649,6 +1698,7 @@ class Safekeeper: class SafekeeperTimelineStatus: acceptor_epoch: int flush_lsn: str + remote_consistent_lsn: str @dataclass @@ -1672,7 +1722,8 @@ class SafekeeperHttpClient(requests.Session): res.raise_for_status() resj = res.json() return SafekeeperTimelineStatus(acceptor_epoch=resj['acceptor_state']['epoch'], - flush_lsn=resj['flush_lsn']) + flush_lsn=resj['flush_lsn'], + remote_consistent_lsn=resj['remote_consistent_lsn']) def get_metrics(self) -> SafekeeperMetrics: request_result = self.get(f"http://localhost:{self.port}/metrics") @@ -1693,6 +1744,54 @@ class SafekeeperHttpClient(requests.Session): return metrics +@dataclass +class Etcd: + """ An object managing etcd instance """ + datadir: str + port: int + peer_port: int + handle: Optional[subprocess.Popen[Any]] = None # handle of running daemon + + def check_status(self): + s = requests.Session() + s.mount('http://', requests.adapters.HTTPAdapter(max_retries=1)) # do not retry + s.get(f"http://localhost:{self.port}/health").raise_for_status() + + def start(self): + pathlib.Path(self.datadir).mkdir(exist_ok=True) + etcd_full_path = etcd_path() + if etcd_full_path is None: + raise Exception('etcd not found') + + with open(os.path.join(self.datadir, "etcd.log"), "wb") as log_file: + args = [ + etcd_full_path, + f"--data-dir={self.datadir}", + f"--listen-client-urls=http://localhost:{self.port}", + f"--advertise-client-urls=http://localhost:{self.port}", + f"--listen-peer-urls=http://localhost:{self.peer_port}" + ] + self.handle = subprocess.Popen(args, stdout=log_file, stderr=log_file) + + # wait for start + started_at = time.time() + while True: + try: + self.check_status() + except Exception as e: + elapsed = time.time() - started_at + if elapsed > 5: + raise RuntimeError(f"timed out waiting {elapsed:.0f}s for etcd start: {e}") + time.sleep(0.5) + else: + break # success + + def stop(self): + if self.handle is not None: + self.handle.terminate() + self.handle.wait() + + def get_test_output_dir(request: Any) -> str: """ Compute the working directory for an individual test. """ test_name = request.node.name @@ -1846,3 +1945,63 @@ def check_restored_datadir_content(test_output_dir: str, env: ZenithEnv, pg: Pos subprocess.run([cmd], stdout=stdout_f, shell=True) assert (mismatch, error) == ([], []) + + +def wait_for(number_of_iterations: int, interval: int, func): + last_exception = None + for i in range(number_of_iterations): + try: + res = func() + except Exception as e: + log.info("waiting for %s iteration %s failed", func, i + 1) + last_exception = e + time.sleep(interval) + continue + return res + raise Exception("timed out while waiting for %s" % func) from last_exception + + +def assert_local(pageserver_http_client: ZenithPageserverHttpClient, + tenant: uuid.UUID, + timeline: uuid.UUID): + timeline_detail = pageserver_http_client.timeline_detail(tenant, timeline) + assert timeline_detail.get('local', {}).get("disk_consistent_lsn"), timeline_detail + return timeline_detail + + +def remote_consistent_lsn(pageserver_http_client: ZenithPageserverHttpClient, + tenant: uuid.UUID, + timeline: uuid.UUID) -> int: + detail = pageserver_http_client.timeline_detail(tenant, timeline) + + lsn_str = detail['remote']['remote_consistent_lsn'] + assert isinstance(lsn_str, str) + return lsn_from_hex(lsn_str) + + +def wait_for_upload(pageserver_http_client: ZenithPageserverHttpClient, + tenant: uuid.UUID, + timeline: uuid.UUID, + lsn: int): + """waits for local timeline upload up to specified lsn""" + + wait_for(10, 1, lambda: remote_consistent_lsn(pageserver_http_client, tenant, timeline) >= lsn) + + +def last_record_lsn(pageserver_http_client: ZenithPageserverHttpClient, + tenant: uuid.UUID, + timeline: uuid.UUID) -> int: + detail = pageserver_http_client.timeline_detail(tenant, timeline) + + lsn_str = detail['local']['last_record_lsn'] + assert isinstance(lsn_str, str) + return lsn_from_hex(lsn_str) + + +def wait_for_last_record_lsn(pageserver_http_client: ZenithPageserverHttpClient, + tenant: uuid.UUID, + timeline: uuid.UUID, + lsn: int): + """waits for pageserver to catch up to a certain lsn""" + + wait_for(10, 1, lambda: last_record_lsn(pageserver_http_client, tenant, timeline) >= lsn) diff --git a/test_runner/performance/test_random_writes.py b/test_runner/performance/test_random_writes.py index b41f2f72a8..ba9eabcd97 100644 --- a/test_runner/performance/test_random_writes.py +++ b/test_runner/performance/test_random_writes.py @@ -49,7 +49,15 @@ def test_random_writes(zenith_with_baseline: PgCompare): count integer default 0 ); """) - cur.execute(f"INSERT INTO Big (pk) values (generate_series(1,{n_rows}))") + + # Insert n_rows in batches to avoid query timeouts + rows_inserted = 0 + while rows_inserted < n_rows: + rows_to_insert = min(1000 * 1000, n_rows - rows_inserted) + low = rows_inserted + 1 + high = rows_inserted + rows_to_insert + cur.execute(f"INSERT INTO Big (pk) values (generate_series({low},{high}))") + rows_inserted += rows_to_insert # Get table size (can't be predicted because padding and alignment) cur.execute("SELECT pg_relation_size('Big');") diff --git a/test_runner/performance/test_seqscans.py b/test_runner/performance/test_seqscans.py index f4d28e3f93..85d0a24510 100644 --- a/test_runner/performance/test_seqscans.py +++ b/test_runner/performance/test_seqscans.py @@ -17,8 +17,8 @@ import pytest # into memory in the page server. pytest.param(100000, 100, 0), # Also test with a larger table, with and without parallelism - pytest.param(10000000, 1, 0, marks=pytest.mark.slow), - pytest.param(10000000, 1, 4, marks=pytest.mark.slow) + pytest.param(10000000, 1, 0), + pytest.param(10000000, 1, 4) ]) def test_seqscans(zenith_with_baseline: PgCompare, rows: int, iters: int, workers: int): env = zenith_with_baseline diff --git a/vendor/postgres b/vendor/postgres index 5e9bc37322..61afbf978b 160000 --- a/vendor/postgres +++ b/vendor/postgres @@ -1 +1 @@ -Subproject commit 5e9bc3732266c072151df20d6772b47ca51e233f +Subproject commit 61afbf978b17764134ab6f1650bbdcadac147e71 diff --git a/walkeeper/Cargo.toml b/walkeeper/Cargo.toml index 193fc4acf6..86aa56c9ae 100644 --- a/walkeeper/Cargo.toml +++ b/walkeeper/Cargo.toml @@ -14,24 +14,29 @@ serde_json = "1" tracing = "0.1.27" clap = "3.0" daemonize = "0.4.1" -rust-s3 = { version = "0.28", default-features = false, features = ["no-verify-ssl", "tokio-rustls-tls"] } -tokio = { version = "1.11", features = ["macros"] } +tokio = { version = "1.17", features = ["macros", "fs"] } postgres-protocol = { git = "https://github.com/zenithdb/rust-postgres.git", rev="2949d98df52587d562986aad155dd4e889e408b7" } postgres = { git = "https://github.com/zenithdb/rust-postgres.git", rev="2949d98df52587d562986aad155dd4e889e408b7" } anyhow = "1.0" crc32c = "0.6.0" humantime = "2.1.0" walkdir = "2" +url = "2.2.2" signal-hook = "0.3.10" serde = { version = "1.0", features = ["derive"] } +serde_with = {version = "1.12.0"} hex = "0.4.3" const_format = "0.2.21" tokio-postgres = { git = "https://github.com/zenithdb/rust-postgres.git", rev="2949d98df52587d562986aad155dd4e889e408b7" } +etcd-client = "0.8.3" +tokio-util = { version = "0.7", features = ["io"] } +rusoto_core = "0.47" +rusoto_s3 = "0.47" postgres_ffi = { path = "../postgres_ffi" } -workspace_hack = { path = "../workspace_hack" } zenith_metrics = { path = "../zenith_metrics" } zenith_utils = { path = "../zenith_utils" } +workspace_hack = { version = "0.1", path = "../workspace_hack" } [dev-dependencies] tempfile = "3.2" diff --git a/walkeeper/src/bin/safekeeper.rs b/walkeeper/src/bin/safekeeper.rs index 6c45115e5f..b3087a1004 100644 --- a/walkeeper/src/bin/safekeeper.rs +++ b/walkeeper/src/bin/safekeeper.rs @@ -11,18 +11,19 @@ use std::io::{ErrorKind, Write}; use std::path::{Path, PathBuf}; use std::thread; use tracing::*; +use url::{ParseError, Url}; use walkeeper::control_file::{self}; use zenith_utils::http::endpoint; use zenith_utils::zid::ZNodeId; use zenith_utils::{logging, tcp_listener, GIT_VERSION}; use tokio::sync::mpsc; -use walkeeper::callmemaybe; use walkeeper::defaults::{DEFAULT_HTTP_LISTEN_ADDR, DEFAULT_PG_LISTEN_ADDR}; use walkeeper::http; use walkeeper::s3_offload; use walkeeper::wal_service; use walkeeper::SafeKeeperConf; +use walkeeper::{broker, callmemaybe}; use zenith_utils::shutdown::exit_now; use zenith_utils::signals; @@ -104,6 +105,11 @@ fn main() -> Result<()> { ) .arg( Arg::new("id").long("id").takes_value(true).help("safekeeper node id: integer") + ).arg( + Arg::new("broker-endpoints") + .long("broker-endpoints") + .takes_value(true) + .help("a comma separated broker (etcd) endpoints for storage nodes coordination, e.g. 'http://127.0.0.1:2379'"), ) .get_matches(); @@ -154,6 +160,11 @@ fn main() -> Result<()> { )); } + if let Some(addr) = arg_matches.value_of("broker-endpoints") { + let collected_ep: Result, ParseError> = addr.split(',').map(Url::parse).collect(); + conf.broker_endpoints = Some(collected_ep?); + } + start_safekeeper(conf, given_id, arg_matches.is_present("init")) } @@ -259,11 +270,12 @@ fn start_safekeeper(mut conf: SafeKeeperConf, given_id: Option, init: b threads.push(wal_acceptor_thread); + let conf_cloned = conf.clone(); let callmemaybe_thread = thread::Builder::new() .name("callmemaybe thread".into()) .spawn(|| { // thread code - let thread_result = callmemaybe::thread_main(conf, rx); + let thread_result = callmemaybe::thread_main(conf_cloned, rx); if let Err(e) = thread_result { error!("callmemaybe thread terminated: {}", e); } @@ -271,6 +283,17 @@ fn start_safekeeper(mut conf: SafeKeeperConf, given_id: Option, init: b .unwrap(); threads.push(callmemaybe_thread); + if conf.broker_endpoints.is_some() { + let conf_ = conf.clone(); + threads.push( + thread::Builder::new() + .name("broker thread".into()) + .spawn(|| { + broker::thread_main(conf_); + })?, + ); + } + // TODO: put more thoughts into handling of failed threads // We probably should restart them. diff --git a/walkeeper/src/broker.rs b/walkeeper/src/broker.rs new file mode 100644 index 0000000000..147497d673 --- /dev/null +++ b/walkeeper/src/broker.rs @@ -0,0 +1,211 @@ +//! Communication with etcd, providing safekeeper peers and pageserver coordination. + +use anyhow::bail; +use anyhow::Context; +use anyhow::Error; +use anyhow::Result; +use etcd_client::Client; +use etcd_client::EventType; +use etcd_client::PutOptions; +use etcd_client::WatchOptions; +use lazy_static::lazy_static; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; +use std::str::FromStr; +use std::time::Duration; +use tokio::task::JoinHandle; +use tokio::{runtime, time::sleep}; +use tracing::*; +use zenith_utils::zid::ZTenantId; +use zenith_utils::zid::ZTimelineId; +use zenith_utils::{ + lsn::Lsn, + zid::{ZNodeId, ZTenantTimelineId}, +}; + +use crate::{safekeeper::Term, timeline::GlobalTimelines, SafeKeeperConf}; + +const RETRY_INTERVAL_MSEC: u64 = 1000; +const PUSH_INTERVAL_MSEC: u64 = 1000; +const LEASE_TTL_SEC: i64 = 5; +// TODO: add global zenith installation ID. +const ZENITH_PREFIX: &str = "zenith"; + +/// Published data about safekeeper. Fields made optional for easy migrations. +#[serde_as] +#[derive(Deserialize, Serialize)] +pub struct SafekeeperInfo { + /// Term of the last entry. + pub last_log_term: Option, + /// LSN of the last record. + #[serde_as(as = "Option")] + pub flush_lsn: Option, + /// Up to which LSN safekeeper regards its WAL as committed. + #[serde_as(as = "Option")] + pub commit_lsn: Option, + /// LSN up to which safekeeper offloaded WAL to s3. + #[serde_as(as = "Option")] + pub s3_wal_lsn: Option, + /// LSN of last checkpoint uploaded by pageserver. + #[serde_as(as = "Option")] + pub remote_consistent_lsn: Option, + #[serde_as(as = "Option")] + pub peer_horizon_lsn: Option, +} + +pub fn thread_main(conf: SafeKeeperConf) { + let runtime = runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + let _enter = info_span!("broker").entered(); + info!("started, broker endpoints {:?}", conf.broker_endpoints); + + runtime.block_on(async { + main_loop(conf).await; + }); +} + +/// Prefix to timeline related data. +fn timeline_path(zttid: &ZTenantTimelineId) -> String { + format!( + "{}/{}/{}", + ZENITH_PREFIX, zttid.tenant_id, zttid.timeline_id + ) +} + +/// Key to per timeline per safekeeper data. +fn timeline_safekeeper_path(zttid: &ZTenantTimelineId, sk_id: ZNodeId) -> String { + format!("{}/safekeeper/{}", timeline_path(zttid), sk_id) +} + +/// Push once in a while data about all active timelines to the broker. +async fn push_loop(conf: SafeKeeperConf) -> Result<()> { + let mut client = Client::connect(conf.broker_endpoints.as_ref().unwrap(), None).await?; + + // Get and maintain lease to automatically delete obsolete data + let lease = client.lease_grant(LEASE_TTL_SEC, None).await?; + let (mut keeper, mut ka_stream) = client.lease_keep_alive(lease.id()).await?; + + let push_interval = Duration::from_millis(PUSH_INTERVAL_MSEC); + loop { + // Note: we lock runtime here and in timeline methods as GlobalTimelines + // is under plain mutex. That's ok, all this code is not performance + // sensitive and there is no risk of deadlock as we don't await while + // lock is held. + let active_tlis = GlobalTimelines::get_active_timelines(); + for zttid in &active_tlis { + if let Ok(tli) = GlobalTimelines::get(&conf, *zttid, false) { + let sk_info = tli.get_public_info(); + let put_opts = PutOptions::new().with_lease(lease.id()); + client + .put( + timeline_safekeeper_path(zttid, conf.my_id), + serde_json::to_string(&sk_info)?, + Some(put_opts), + ) + .await + .context("failed to push safekeeper info")?; + } + } + // revive the lease + keeper + .keep_alive() + .await + .context("failed to send LeaseKeepAliveRequest")?; + ka_stream + .message() + .await + .context("failed to receive LeaseKeepAliveResponse")?; + sleep(push_interval).await; + } +} + +/// Subscribe and fetch all the interesting data from the broker. +async fn pull_loop(conf: SafeKeeperConf) -> Result<()> { + lazy_static! { + static ref TIMELINE_SAFEKEEPER_RE: Regex = + Regex::new(r"^zenith/([[:xdigit:]]+)/([[:xdigit:]]+)/safekeeper/([[:digit:]])$") + .unwrap(); + } + let mut client = Client::connect(conf.broker_endpoints.as_ref().unwrap(), None).await?; + loop { + let wo = WatchOptions::new().with_prefix(); + // TODO: subscribe only to my timelines + let (_, mut stream) = client.watch(ZENITH_PREFIX, Some(wo)).await?; + while let Some(resp) = stream.message().await? { + if resp.canceled() { + bail!("watch canceled"); + } + + for event in resp.events() { + if EventType::Put == event.event_type() { + if let Some(kv) = event.kv() { + if let Some(caps) = TIMELINE_SAFEKEEPER_RE.captures(kv.key_str()?) { + let tenant_id = ZTenantId::from_str(caps.get(1).unwrap().as_str())?; + let timeline_id = ZTimelineId::from_str(caps.get(2).unwrap().as_str())?; + let zttid = ZTenantTimelineId::new(tenant_id, timeline_id); + let safekeeper_id = ZNodeId(caps.get(3).unwrap().as_str().parse()?); + let value_str = kv.value_str()?; + match serde_json::from_str::(value_str) { + Ok(safekeeper_info) => { + if let Ok(tli) = GlobalTimelines::get(&conf, zttid, false) { + tli.record_safekeeper_info(&safekeeper_info, safekeeper_id)? + } + } + Err(err) => warn!( + "failed to deserialize safekeeper info {}: {}", + value_str, err + ), + } + } + } + } + } + } + } +} + +async fn main_loop(conf: SafeKeeperConf) { + let mut ticker = tokio::time::interval(Duration::from_millis(RETRY_INTERVAL_MSEC)); + let mut push_handle: Option>> = None; + let mut pull_handle: Option>> = None; + // Selecting on JoinHandles requires some squats; is there a better way to + // reap tasks individually? + + // Handling failures in task itself won't catch panic and in Tokio, task's + // panic doesn't kill the whole executor, so it is better to do reaping + // here. + loop { + tokio::select! { + res = async { push_handle.as_mut().unwrap().await }, if push_handle.is_some() => { + // was it panic or normal error? + let err = match res { + Ok(res_internal) => res_internal.unwrap_err(), + Err(err_outer) => err_outer.into(), + }; + warn!("push task failed: {:?}", err); + push_handle = None; + }, + res = async { pull_handle.as_mut().unwrap().await }, if pull_handle.is_some() => { + // was it panic or normal error? + let err = match res { + Ok(res_internal) => res_internal.unwrap_err(), + Err(err_outer) => err_outer.into(), + }; + warn!("pull task failed: {:?}", err); + pull_handle = None; + }, + _ = ticker.tick() => { + if push_handle.is_none() { + push_handle = Some(tokio::spawn(push_loop(conf.clone()))); + } + if pull_handle.is_none() { + pull_handle = Some(tokio::spawn(pull_loop(conf.clone()))); + } + } + } + } +} diff --git a/walkeeper/src/handler.rs b/walkeeper/src/handler.rs index ead6fab9fb..00d177da56 100644 --- a/walkeeper/src/handler.rs +++ b/walkeeper/src/handler.rs @@ -168,7 +168,14 @@ impl SafekeeperPostgresHandler { fn handle_identify_system(&mut self, pgb: &mut PostgresBackend) -> Result<()> { let start_pos = self.timeline.get().get_end_of_wal(); let lsn = start_pos.to_string(); - let sysid = self.timeline.get().get_info().server.system_id.to_string(); + let sysid = self + .timeline + .get() + .get_state() + .1 + .server + .system_id + .to_string(); let lsn_bytes = lsn.as_bytes(); let tli = PG_TLI.to_string(); let tli_bytes = tli.as_bytes(); diff --git a/walkeeper/src/http/routes.rs b/walkeeper/src/http/routes.rs index 74f7f4a735..26b23cddcc 100644 --- a/walkeeper/src/http/routes.rs +++ b/walkeeper/src/http/routes.rs @@ -31,7 +31,7 @@ struct SafekeeperStatus { async fn status_handler(request: Request) -> Result, ApiError> { let conf = get_conf(&request); let status = SafekeeperStatus { id: conf.my_id }; - Ok(json_response(StatusCode::OK, status)?) + json_response(StatusCode::OK, status) } fn get_conf(request: &Request) -> &SafeKeeperConf { @@ -86,26 +86,27 @@ async fn timeline_status_handler(request: Request) -> Result) -> Result, ApiError> { @@ -118,7 +119,7 @@ async fn timeline_create_handler(mut request: Request) -> Result Result<()> { fn send_proposer_elected(spg: &mut SafekeeperPostgresHandler, term: Term, lsn: Lsn) -> Result<()> { // add new term to existing history - let history = spg.timeline.get().get_info().acceptor_state.term_history; + let history = spg.timeline.get().get_state().1.acceptor_state.term_history; let history = history.up_to(lsn.checked_sub(1u64).unwrap()); let mut history_entries = history.0; history_entries.push(TermSwitchEntry { term, lsn }); @@ -142,7 +142,7 @@ fn append_logical_message( msg: &AppendLogicalMessage, ) -> Result { let wal_data = encode_logical_message(&msg.lm_prefix, &msg.lm_message); - let sk_state = spg.timeline.get().get_info(); + let sk_state = spg.timeline.get().get_state().1; let begin_lsn = msg.begin_lsn; let end_lsn = begin_lsn + wal_data.len() as u64; diff --git a/walkeeper/src/lib.rs b/walkeeper/src/lib.rs index dfd71e4de2..69423d42d8 100644 --- a/walkeeper/src/lib.rs +++ b/walkeeper/src/lib.rs @@ -1,9 +1,11 @@ // use std::path::PathBuf; use std::time::Duration; +use url::Url; use zenith_utils::zid::{ZNodeId, ZTenantTimelineId}; +pub mod broker; pub mod callmemaybe; pub mod control_file; pub mod control_file_upgrade; @@ -47,6 +49,7 @@ pub struct SafeKeeperConf { pub ttl: Option, pub recall_period: Duration, pub my_id: ZNodeId, + pub broker_endpoints: Option>, } impl SafeKeeperConf { @@ -71,6 +74,7 @@ impl Default for SafeKeeperConf { ttl: None, recall_period: defaults::DEFAULT_RECALL_PERIOD, my_id: ZNodeId(0), + broker_endpoints: None, } } } diff --git a/walkeeper/src/s3_offload.rs b/walkeeper/src/s3_offload.rs index 2b3285e6c6..c796f53615 100644 --- a/walkeeper/src/s3_offload.rs +++ b/walkeeper/src/s3_offload.rs @@ -2,19 +2,19 @@ // Offload old WAL segments to S3 and remove them locally // -use anyhow::Result; +use anyhow::Context; use postgres_ffi::xlog_utils::*; -use s3::bucket::Bucket; -use s3::creds::Credentials; -use s3::region::Region; +use rusoto_core::credential::StaticProvider; +use rusoto_core::{HttpClient, Region}; +use rusoto_s3::{ListObjectsV2Request, PutObjectRequest, S3Client, StreamingBody, S3}; use std::collections::HashSet; use std::env; -use std::fs::{self, File}; -use std::io::prelude::*; use std::path::Path; use std::time::SystemTime; +use tokio::fs::{self, File}; use tokio::runtime; use tokio::time::sleep; +use tokio_util::io::ReaderStream; use tracing::*; use walkdir::WalkDir; @@ -39,11 +39,12 @@ pub fn thread_main(conf: SafeKeeperConf) { } async fn offload_files( - bucket: &Bucket, + client: &S3Client, + bucket_name: &str, listing: &HashSet, dir_path: &Path, conf: &SafeKeeperConf, -) -> Result { +) -> anyhow::Result { let horizon = SystemTime::now() - conf.ttl.unwrap(); let mut n: u64 = 0; for entry in WalkDir::new(dir_path) { @@ -57,12 +58,17 @@ async fn offload_files( let relpath = path.strip_prefix(&conf.workdir).unwrap(); let s3path = String::from("walarchive/") + relpath.to_str().unwrap(); if !listing.contains(&s3path) { - let mut file = File::open(&path)?; - let mut content = Vec::new(); - file.read_to_end(&mut content)?; - bucket.put_object(s3path, &content).await?; + let file = File::open(&path).await?; + client + .put_object(PutObjectRequest { + body: Some(StreamingBody::new(ReaderStream::new(file))), + bucket: bucket_name.to_string(), + key: s3path, + ..PutObjectRequest::default() + }) + .await?; - fs::remove_file(&path)?; + fs::remove_file(&path).await?; n += 1; } } @@ -70,35 +76,59 @@ async fn offload_files( Ok(n) } -async fn main_loop(conf: &SafeKeeperConf) -> Result<()> { +async fn main_loop(conf: &SafeKeeperConf) -> anyhow::Result<()> { let region = Region::Custom { - region: env::var("S3_REGION").unwrap(), - endpoint: env::var("S3_ENDPOINT").unwrap(), + name: env::var("S3_REGION").context("S3_REGION env var is not set")?, + endpoint: env::var("S3_ENDPOINT").context("S3_ENDPOINT env var is not set")?, }; - let credentials = Credentials::new( - Some(&env::var("S3_ACCESSKEY").unwrap()), - Some(&env::var("S3_SECRET").unwrap()), - None, - None, - None, - ) - .unwrap(); - // Create Bucket in REGION for BUCKET - let bucket = Bucket::new_with_path_style("zenith-testbucket", region, credentials)?; + let client = S3Client::new_with( + HttpClient::new().context("Failed to create S3 http client")?, + StaticProvider::new_minimal( + env::var("S3_ACCESSKEY").context("S3_ACCESSKEY env var is not set")?, + env::var("S3_SECRET").context("S3_SECRET env var is not set")?, + ), + region, + ); + + let bucket_name = "zenith-testbucket"; loop { - // List out contents of directory - let results = bucket - .list("walarchive/".to_string(), Some("".to_string())) - .await?; - let listing = results - .iter() - .flat_map(|b| b.contents.iter().map(|o| o.key.clone())) - .collect(); - - let n = offload_files(&bucket, &listing, &conf.workdir, conf).await?; + let listing = gather_wal_entries(&client, bucket_name).await?; + let n = offload_files(&client, bucket_name, &listing, &conf.workdir, conf).await?; info!("Offload {} files to S3", n); sleep(conf.ttl.unwrap()).await; } } + +async fn gather_wal_entries( + client: &S3Client, + bucket_name: &str, +) -> anyhow::Result> { + let mut document_keys = HashSet::new(); + + let mut continuation_token = None::; + loop { + let response = client + .list_objects_v2(ListObjectsV2Request { + bucket: bucket_name.to_string(), + prefix: Some("walarchive/".to_string()), + continuation_token, + ..ListObjectsV2Request::default() + }) + .await?; + document_keys.extend( + response + .contents + .unwrap_or_default() + .into_iter() + .filter_map(|o| o.key), + ); + + continuation_token = response.continuation_token; + if continuation_token.is_none() { + break; + } + } + Ok(document_keys) +} diff --git a/walkeeper/src/safekeeper.rs b/walkeeper/src/safekeeper.rs index 53fd6f5588..1e23d87b34 100644 --- a/walkeeper/src/safekeeper.rs +++ b/walkeeper/src/safekeeper.rs @@ -193,7 +193,7 @@ pub struct SafeKeeperState { pub peer_horizon_lsn: Lsn, /// LSN of the oldest known checkpoint made by pageserver and successfully /// pushed to s3. We don't remove WAL beyond it. Persisted only for - /// informational purposes, we receive it from pageserver. + /// informational purposes, we receive it from pageserver (or broker). pub remote_consistent_lsn: Lsn, // Peers and their state as we remember it. Knowing peers themselves is // fundamental; but state is saved here only for informational purposes and @@ -202,6 +202,16 @@ pub struct SafeKeeperState { pub peers: Peers, } +#[derive(Debug, Clone)] +// In memory safekeeper state. Fields mirror ones in `SafeKeeperState`; values +// are not flushed yet. +pub struct SafekeeperMemState { + pub commit_lsn: Lsn, + pub s3_wal_lsn: Lsn, // TODO: keep only persistent version + pub peer_horizon_lsn: Lsn, + pub remote_consistent_lsn: Lsn, +} + impl SafeKeeperState { pub fn new(zttid: &ZTenantTimelineId, peers: Vec) -> SafeKeeperState { SafeKeeperState { @@ -470,14 +480,12 @@ struct SafeKeeperMetrics { } impl SafeKeeperMetrics { - fn new(tenant_id: ZTenantId, timeline_id: ZTimelineId, commit_lsn: Lsn) -> Self { + fn new(tenant_id: ZTenantId, timeline_id: ZTimelineId) -> Self { let tenant_id = tenant_id.to_string(); let timeline_id = timeline_id.to_string(); - let m = Self { + Self { commit_lsn: COMMIT_LSN_GAUGE.with_label_values(&[&tenant_id, &timeline_id]), - }; - m.commit_lsn.set(u64::from(commit_lsn) as f64); - m + } } } @@ -487,10 +495,14 @@ pub struct SafeKeeper { // Cached metrics so we don't have to recompute labels on each update. metrics: SafeKeeperMetrics, - /// not-yet-flushed pairs of same named fields in s.* - pub commit_lsn: Lsn, - pub peer_horizon_lsn: Lsn, - pub s: SafeKeeperState, // persistent part + /// Maximum commit_lsn between all nodes, can be ahead of local flush_lsn. + pub global_commit_lsn: Lsn, + /// LSN since the proposer safekeeper currently talking to appends WAL; + /// determines epoch switch point. + epoch_start_lsn: Lsn, + + pub inmem: SafekeeperMemState, // in memory part + pub s: SafeKeeperState, // persistent part pub control_store: CTRL, pub wal_store: WAL, @@ -505,21 +517,29 @@ where pub fn new( ztli: ZTimelineId, control_store: CTRL, - wal_store: WAL, + mut wal_store: WAL, state: SafeKeeperState, - ) -> SafeKeeper { + ) -> Result> { if state.timeline_id != ZTimelineId::from([0u8; 16]) && ztli != state.timeline_id { - panic!("Calling SafeKeeper::new with inconsistent ztli ({}) and SafeKeeperState.server.timeline_id ({})", ztli, state.timeline_id); + bail!("Calling SafeKeeper::new with inconsistent ztli ({}) and SafeKeeperState.server.timeline_id ({})", ztli, state.timeline_id); } - SafeKeeper { - metrics: SafeKeeperMetrics::new(state.tenant_id, ztli, state.commit_lsn), - commit_lsn: state.commit_lsn, - peer_horizon_lsn: state.peer_horizon_lsn, + wal_store.init_storage(&state)?; + + Ok(SafeKeeper { + metrics: SafeKeeperMetrics::new(state.tenant_id, ztli), + global_commit_lsn: state.commit_lsn, + epoch_start_lsn: Lsn(0), + inmem: SafekeeperMemState { + commit_lsn: state.commit_lsn, + s3_wal_lsn: state.s3_wal_lsn, + peer_horizon_lsn: state.peer_horizon_lsn, + remote_consistent_lsn: state.remote_consistent_lsn, + }, s: state, control_store, wal_store, - } + }) } /// Get history of term switches for the available WAL @@ -530,8 +550,7 @@ where .up_to(self.wal_store.flush_lsn()) } - #[cfg(test)] - fn get_epoch(&self) -> Term { + pub fn get_epoch(&self) -> Term { self.s.acceptor_state.get_epoch(self.wal_store.flush_lsn()) } @@ -602,9 +621,6 @@ where // pass wal_seg_size to read WAL and find flush_lsn self.wal_store.init_storage(&self.s)?; - // update tenant_id/timeline_id in metrics - self.metrics = SafeKeeperMetrics::new(msg.tenant_id, msg.ztli, self.commit_lsn); - info!( "processed greeting from proposer {:?}, sending term {:?}", msg.proposer_id, self.s.acceptor_state.term @@ -684,12 +700,49 @@ where Ok(None) } + /// Advance commit_lsn taking into account what we have locally + pub fn update_commit_lsn(&mut self) -> Result<()> { + let commit_lsn = min(self.global_commit_lsn, self.wal_store.flush_lsn()); + assert!(commit_lsn >= self.inmem.commit_lsn); + + self.inmem.commit_lsn = commit_lsn; + self.metrics.commit_lsn.set(self.inmem.commit_lsn.0 as f64); + + // If new commit_lsn reached epoch switch, force sync of control + // file: walproposer in sync mode is very interested when this + // happens. Note: this is for sync-safekeepers mode only, as + // otherwise commit_lsn might jump over epoch_start_lsn. + // Also note that commit_lsn can reach epoch_start_lsn earlier + // that we receive new epoch_start_lsn, and we still need to sync + // control file in this case. + if commit_lsn == self.epoch_start_lsn && self.s.commit_lsn != commit_lsn { + self.persist_control_file()?; + } + + // We got our first commit_lsn, which means we should sync + // everything to disk, to initialize the state. + if self.s.commit_lsn == Lsn(0) && commit_lsn > Lsn(0) { + self.wal_store.flush_wal()?; + self.persist_control_file()?; + } + + Ok(()) + } + + /// Persist in-memory state to the disk. + fn persist_control_file(&mut self) -> Result<()> { + self.s.commit_lsn = self.inmem.commit_lsn; + self.s.peer_horizon_lsn = self.inmem.peer_horizon_lsn; + + self.control_store.persist(&self.s) + } + /// Handle request to append WAL. #[allow(clippy::comparison_chain)] fn handle_append_request( &mut self, msg: &AppendRequest, - mut require_flush: bool, + require_flush: bool, ) -> Result> { if self.s.acceptor_state.term < msg.h.term { bail!("got AppendRequest before ProposerElected"); @@ -701,25 +754,22 @@ where return Ok(Some(AcceptorProposerMessage::AppendResponse(resp))); } - // After ProposerElected, which performs truncation, we should get only - // indeed append requests (but flush_lsn is advanced only on record - // boundary, so might be less). - assert!(self.wal_store.flush_lsn() <= msg.h.begin_lsn); + // Now we know that we are in the same term as the proposer, + // processing the message. + self.epoch_start_lsn = msg.h.epoch_start_lsn; + // TODO: don't update state without persisting to disk self.s.proposer_uuid = msg.h.proposer_uuid; - let mut sync_control_file = false; // do the job if !msg.wal_data.is_empty() { self.wal_store.write_wal(msg.h.begin_lsn, &msg.wal_data)?; - // If this was the first record we ever receieved, initialize + // If this was the first record we ever received, initialize // commit_lsn to help find_end_of_wal skip the hole in the // beginning. - if self.s.commit_lsn == Lsn(0) { - self.s.commit_lsn = msg.h.begin_lsn; - sync_control_file = true; - require_flush = true; + if self.global_commit_lsn == Lsn(0) { + self.global_commit_lsn = msg.h.begin_lsn; } } @@ -728,35 +778,22 @@ where self.wal_store.flush_wal()?; } - // Advance commit_lsn taking into account what we have locally. - // commit_lsn can be 0, being unknown to new walproposer while he hasn't - // collected majority of its epoch acks yet, ignore it in this case. + // Update global_commit_lsn, verifying that it cannot decrease. if msg.h.commit_lsn != Lsn(0) { - let commit_lsn = min(msg.h.commit_lsn, self.wal_store.flush_lsn()); - // If new commit_lsn reached epoch switch, force sync of control - // file: walproposer in sync mode is very interested when this - // happens. Note: this is for sync-safekeepers mode only, as - // otherwise commit_lsn might jump over epoch_start_lsn. - sync_control_file |= commit_lsn == msg.h.epoch_start_lsn; - self.commit_lsn = commit_lsn; - self.metrics - .commit_lsn - .set(u64::from(self.commit_lsn) as f64); + assert!(msg.h.commit_lsn >= self.global_commit_lsn); + self.global_commit_lsn = msg.h.commit_lsn; } - self.peer_horizon_lsn = msg.h.truncate_lsn; + self.inmem.peer_horizon_lsn = msg.h.truncate_lsn; + self.update_commit_lsn()?; + // Update truncate and commit LSN in control file. // To avoid negative impact on performance of extra fsync, do it only // when truncate_lsn delta exceeds WAL segment size. - sync_control_file |= - self.s.peer_horizon_lsn + (self.s.server.wal_seg_size as u64) < self.peer_horizon_lsn; - if sync_control_file { - self.s.commit_lsn = self.commit_lsn; - self.s.peer_horizon_lsn = self.peer_horizon_lsn; - } - - if sync_control_file { - self.control_store.persist(&self.s)?; + if self.s.peer_horizon_lsn + (self.s.server.wal_seg_size as u64) + < self.inmem.peer_horizon_lsn + { + self.persist_control_file()?; } trace!( @@ -780,6 +817,10 @@ where /// Flush WAL to disk. Return AppendResponse with latest LSNs. fn handle_flush(&mut self) -> Result> { self.wal_store.flush_wal()?; + + // commit_lsn can be updated because we have new flushed data locally. + self.update_commit_lsn()?; + Ok(Some(AcceptorProposerMessage::AppendResponse( self.append_response(), ))) @@ -838,7 +879,7 @@ mod tests { }; let wal_store = DummyWalStore { lsn: Lsn(0) }; let ztli = ZTimelineId::from([0u8; 16]); - let mut sk = SafeKeeper::new(ztli, storage, wal_store, SafeKeeperState::empty()); + let mut sk = SafeKeeper::new(ztli, storage, wal_store, SafeKeeperState::empty()).unwrap(); // check voting for 1 is ok let vote_request = ProposerAcceptorMessage::VoteRequest(VoteRequest { term: 1 }); @@ -853,7 +894,7 @@ mod tests { let storage = InMemoryState { persisted_state: state.clone(), }; - sk = SafeKeeper::new(ztli, storage, sk.wal_store, state); + sk = SafeKeeper::new(ztli, storage, sk.wal_store, state).unwrap(); // and ensure voting second time for 1 is not ok vote_resp = sk.process_msg(&vote_request); @@ -870,7 +911,7 @@ mod tests { }; let wal_store = DummyWalStore { lsn: Lsn(0) }; let ztli = ZTimelineId::from([0u8; 16]); - let mut sk = SafeKeeper::new(ztli, storage, wal_store, SafeKeeperState::empty()); + let mut sk = SafeKeeper::new(ztli, storage, wal_store, SafeKeeperState::empty()).unwrap(); let mut ar_hdr = AppendRequestHeader { term: 1, diff --git a/walkeeper/src/send_wal.rs b/walkeeper/src/send_wal.rs index 1febd71842..f12fb5cb4a 100644 --- a/walkeeper/src/send_wal.rs +++ b/walkeeper/src/send_wal.rs @@ -230,7 +230,7 @@ impl ReplicationConn { let mut wal_seg_size: usize; loop { - wal_seg_size = spg.timeline.get().get_info().server.wal_seg_size as usize; + wal_seg_size = spg.timeline.get().get_state().1.server.wal_seg_size as usize; if wal_seg_size == 0 { error!("Cannot start replication before connecting to wal_proposer"); sleep(Duration::from_secs(1)); diff --git a/walkeeper/src/timeline.rs b/walkeeper/src/timeline.rs index ea8308b95e..a76ef77615 100644 --- a/walkeeper/src/timeline.rs +++ b/walkeeper/src/timeline.rs @@ -17,12 +17,14 @@ use tracing::*; use zenith_utils::lsn::Lsn; use zenith_utils::zid::{ZNodeId, ZTenantTimelineId}; +use crate::broker::SafekeeperInfo; use crate::callmemaybe::{CallmeEvent, SubscriptionStateKey}; use crate::control_file; use crate::control_file::Storage as cf_storage; use crate::safekeeper::{ AcceptorProposerMessage, ProposerAcceptorMessage, SafeKeeper, SafeKeeperState, + SafekeeperMemState, }; use crate::send_wal::HotStandbyFeedback; use crate::wal_storage; @@ -98,7 +100,7 @@ impl SharedState { let state = SafeKeeperState::new(zttid, peer_ids); let control_store = control_file::FileStorage::new(zttid, conf); let wal_store = wal_storage::PhysicalStorage::new(zttid, conf); - let mut sk = SafeKeeper::new(zttid.timeline_id, control_store, wal_store, state); + let mut sk = SafeKeeper::new(zttid.timeline_id, control_store, wal_store, state)?; sk.control_store.persist(&sk.s)?; Ok(Self { @@ -125,7 +127,7 @@ impl SharedState { Ok(Self { notified_commit_lsn: Lsn(0), - sk: SafeKeeper::new(zttid.timeline_id, control_store, wal_store, state), + sk: SafeKeeper::new(zttid.timeline_id, control_store, wal_store, state)?, replicas: Vec::new(), active: false, num_computes: 0, @@ -340,7 +342,7 @@ impl Timeline { let replica_state = shared_state.replicas[replica_id].unwrap(); let deactivate = shared_state.notified_commit_lsn == Lsn(0) || // no data at all yet (replica_state.last_received_lsn != Lsn::MAX && // Lsn::MAX means that we don't know the latest LSN yet. - replica_state.last_received_lsn >= shared_state.sk.commit_lsn); + replica_state.last_received_lsn >= shared_state.sk.inmem.commit_lsn); if deactivate { shared_state.deactivate(&self.zttid, callmemaybe_tx)?; return Ok(true); @@ -349,6 +351,11 @@ impl Timeline { Ok(false) } + fn is_active(&self) -> bool { + let shared_state = self.mutex.lock().unwrap(); + shared_state.active + } + /// Timed wait for an LSN to be committed. /// /// Returns the last committed LSN, which will be at least @@ -394,7 +401,7 @@ impl Timeline { rmsg = shared_state.sk.process_msg(msg)?; // locally available commit lsn. flush_lsn can be smaller than // commit_lsn if we are catching up safekeeper. - commit_lsn = shared_state.sk.commit_lsn; + commit_lsn = shared_state.sk.inmem.commit_lsn; // if this is AppendResponse, fill in proper hot standby feedback and disk consistent lsn if let Some(AcceptorProposerMessage::AppendResponse(ref mut resp)) = rmsg { @@ -410,8 +417,61 @@ impl Timeline { Ok(rmsg) } - pub fn get_info(&self) -> SafeKeeperState { - self.mutex.lock().unwrap().sk.s.clone() + pub fn get_state(&self) -> (SafekeeperMemState, SafeKeeperState) { + let shared_state = self.mutex.lock().unwrap(); + (shared_state.sk.inmem.clone(), shared_state.sk.s.clone()) + } + + /// Prepare public safekeeper info for reporting. + pub fn get_public_info(&self) -> SafekeeperInfo { + let shared_state = self.mutex.lock().unwrap(); + SafekeeperInfo { + last_log_term: Some(shared_state.sk.get_epoch()), + flush_lsn: Some(shared_state.sk.wal_store.flush_lsn()), + // note: this value is not flushed to control file yet and can be lost + commit_lsn: Some(shared_state.sk.inmem.commit_lsn), + s3_wal_lsn: Some(shared_state.sk.inmem.s3_wal_lsn), + // TODO: rework feedbacks to avoid max here + remote_consistent_lsn: Some(max( + shared_state.get_replicas_state().remote_consistent_lsn, + shared_state.sk.inmem.remote_consistent_lsn, + )), + peer_horizon_lsn: Some(shared_state.sk.inmem.peer_horizon_lsn), + } + } + + /// Update timeline state with peer safekeeper data. + pub fn record_safekeeper_info(&self, sk_info: &SafekeeperInfo, _sk_id: ZNodeId) -> Result<()> { + let mut shared_state = self.mutex.lock().unwrap(); + // Note: the check is too restrictive, generally we can update local + // commit_lsn if our history matches (is part of) history of advanced + // commit_lsn provider. + if let (Some(commit_lsn), Some(last_log_term)) = (sk_info.commit_lsn, sk_info.last_log_term) + { + if last_log_term == shared_state.sk.get_epoch() { + shared_state.sk.global_commit_lsn = + max(commit_lsn, shared_state.sk.global_commit_lsn); + shared_state.sk.update_commit_lsn()?; + let local_commit_lsn = min(commit_lsn, shared_state.sk.wal_store.flush_lsn()); + shared_state.sk.inmem.commit_lsn = + max(local_commit_lsn, shared_state.sk.inmem.commit_lsn); + } + } + if let Some(s3_wal_lsn) = sk_info.s3_wal_lsn { + shared_state.sk.inmem.s3_wal_lsn = max(s3_wal_lsn, shared_state.sk.inmem.s3_wal_lsn); + } + if let Some(remote_consistent_lsn) = sk_info.remote_consistent_lsn { + shared_state.sk.inmem.remote_consistent_lsn = max( + remote_consistent_lsn, + shared_state.sk.inmem.remote_consistent_lsn, + ); + } + if let Some(peer_horizon_lsn) = sk_info.peer_horizon_lsn { + shared_state.sk.inmem.peer_horizon_lsn = + max(peer_horizon_lsn, shared_state.sk.inmem.peer_horizon_lsn); + } + // TODO: sync control file + Ok(()) } pub fn add_replica(&self, state: ReplicaState) -> usize { @@ -495,7 +555,7 @@ impl GlobalTimelines { } /// Get a timeline with control file loaded from the global TIMELINES map. - /// If control file doesn't exist, bails out. + /// If control file doesn't exist and create=false, bails out. pub fn get( conf: &SafeKeeperConf, zttid: ZTenantTimelineId, @@ -537,4 +597,14 @@ impl GlobalTimelines { } } } + + /// Get ZTenantTimelineIDs of all active timelines. + pub fn get_active_timelines() -> Vec { + let timelines = TIMELINES.lock().unwrap(); + timelines + .iter() + .filter(|&(_, tli)| tli.is_active()) + .map(|(zttid, _)| *zttid) + .collect() + } } diff --git a/workspace_hack/Cargo.toml b/workspace_hack/Cargo.toml index 48d81bbc07..6e6a0e09d7 100644 --- a/workspace_hack/Cargo.toml +++ b/workspace_hack/Cargo.toml @@ -1,22 +1,50 @@ +# This file is generated by `cargo hakari`. +# To regenerate, run: +# cargo hakari generate + [package] name = "workspace_hack" version = "0.1.0" -edition = "2021" +description = "workspace-hack package, managed by hakari" +# You can choose to publish this crate: see https://docs.rs/cargo-hakari/latest/cargo_hakari/publishing. +publish = false -[target.'cfg(all())'.dependencies] -libc = { version = "0.2", features = ["default", "extra_traits", "std"] } -memchr = { version = "2", features = ["default", "std", "use_std"] } +# The parts of the file between the BEGIN HAKARI SECTION and END HAKARI SECTION comments +# are managed by hakari. + +### BEGIN HAKARI SECTION +[dependencies] +anyhow = { version = "1", features = ["backtrace", "std"] } +bytes = { version = "1", features = ["serde", "std"] } +clap = { version = "2", features = ["ansi_term", "atty", "color", "strsim", "suggestions", "vec_map"] } +either = { version = "1", features = ["use_std"] } +hashbrown = { version = "0.11", features = ["ahash", "inline-more", "raw"] } +libc = { version = "0.2", features = ["extra_traits", "std"] } +log = { version = "0.4", default-features = false, features = ["serde", "std"] } +memchr = { version = "2", features = ["std", "use_std"] } num-integer = { version = "0.1", default-features = false, features = ["std"] } -num-traits = { version = "0.2", default-features = false, features = ["std"] } -regex = { version = "1", features = ["aho-corasick", "default", "memchr", "perf", "perf-cache", "perf-dfa", "perf-inline", "perf-literal", "std", "unicode", "unicode-age", "unicode-bool", "unicode-case", "unicode-gencat", "unicode-perl", "unicode-script", "unicode-segment"] } -regex-syntax = { version = "0.6", features = ["default", "unicode", "unicode-age", "unicode-bool", "unicode-case", "unicode-gencat", "unicode-perl", "unicode-script", "unicode-segment"] } -serde = { version = "1", features = ["default", "derive", "serde_derive", "std"] } +num-traits = { version = "0.2", features = ["std"] } +regex = { version = "1", features = ["aho-corasick", "memchr", "perf", "perf-cache", "perf-dfa", "perf-inline", "perf-literal", "std", "unicode", "unicode-age", "unicode-bool", "unicode-case", "unicode-gencat", "unicode-perl", "unicode-script", "unicode-segment"] } +regex-syntax = { version = "0.6", features = ["unicode", "unicode-age", "unicode-bool", "unicode-case", "unicode-gencat", "unicode-perl", "unicode-script", "unicode-segment"] } +reqwest = { version = "0.11", default-features = false, features = ["__rustls", "__tls", "blocking", "hyper-rustls", "json", "rustls", "rustls-pemfile", "rustls-tls", "rustls-tls-webpki-roots", "serde_json", "stream", "tokio-rustls", "tokio-util", "webpki-roots"] } +scopeguard = { version = "1", features = ["use_std"] } +serde = { version = "1", features = ["alloc", "derive", "serde_derive", "std"] } +tokio = { version = "1", features = ["bytes", "fs", "io-util", "libc", "macros", "memchr", "mio", "net", "num_cpus", "once_cell", "process", "rt", "rt-multi-thread", "signal-hook-registry", "sync", "time", "tokio-macros"] } +tracing = { version = "0.1", features = ["attributes", "std", "tracing-attributes"] } +tracing-core = { version = "0.1", features = ["lazy_static", "std"] } -[target.'cfg(all())'.build-dependencies] -libc = { version = "0.2", features = ["default", "extra_traits", "std"] } -memchr = { version = "2", features = ["default", "std", "use_std"] } -proc-macro2 = { version = "1", features = ["default", "proc-macro"] } -quote = { version = "1", features = ["default", "proc-macro"] } -regex = { version = "1", features = ["aho-corasick", "default", "memchr", "perf", "perf-cache", "perf-dfa", "perf-inline", "perf-literal", "std", "unicode", "unicode-age", "unicode-bool", "unicode-case", "unicode-gencat", "unicode-perl", "unicode-script", "unicode-segment"] } -regex-syntax = { version = "0.6", features = ["default", "unicode", "unicode-age", "unicode-bool", "unicode-case", "unicode-gencat", "unicode-perl", "unicode-script", "unicode-segment"] } -syn = { version = "1", features = ["clone-impls", "default", "derive", "full", "parsing", "printing", "proc-macro", "quote", "visit", "visit-mut"] } +[build-dependencies] +cc = { version = "1", default-features = false, features = ["jobserver", "parallel"] } +clap = { version = "2", features = ["ansi_term", "atty", "color", "strsim", "suggestions", "vec_map"] } +either = { version = "1", features = ["use_std"] } +libc = { version = "0.2", features = ["extra_traits", "std"] } +log = { version = "0.4", default-features = false, features = ["serde", "std"] } +memchr = { version = "2", features = ["std", "use_std"] } +proc-macro2 = { version = "1", features = ["proc-macro"] } +quote = { version = "1", features = ["proc-macro"] } +regex = { version = "1", features = ["aho-corasick", "memchr", "perf", "perf-cache", "perf-dfa", "perf-inline", "perf-literal", "std", "unicode", "unicode-age", "unicode-bool", "unicode-case", "unicode-gencat", "unicode-perl", "unicode-script", "unicode-segment"] } +regex-syntax = { version = "0.6", features = ["unicode", "unicode-age", "unicode-bool", "unicode-case", "unicode-gencat", "unicode-perl", "unicode-script", "unicode-segment"] } +serde = { version = "1", features = ["alloc", "derive", "serde_derive", "std"] } +syn = { version = "1", features = ["clone-impls", "derive", "extra-traits", "full", "parsing", "printing", "proc-macro", "quote", "visit", "visit-mut"] } + +### END HAKARI SECTION diff --git a/workspace_hack/src/lib.rs b/workspace_hack/src/lib.rs index ceba3d145d..22489f632b 100644 --- a/workspace_hack/src/lib.rs +++ b/workspace_hack/src/lib.rs @@ -1,23 +1 @@ -//! This crate contains no code. -//! -//! The workspace_hack crate exists only to pin down some dependencies, -//! so that those dependencies always build with the same features, -//! under a few different cases that can be problematic: -//! - Running `cargo check` or `cargo build` from a crate sub-directory -//! instead of the workspace root. -//! - Running `cargo install`, which can only be done per-crate -//! -//! The dependency lists in Cargo.toml were automatically generated by -//! a tool called -//! [Hakari](https://github.com/facebookincubator/cargo-guppy/tree/main/tools/hakari). -//! -//! Hakari doesn't have a CLI yet; in the meantime the example code in -//! their `README` file is enough to regenerate the dependencies. -//! Hakari's output was pasted into Cargo.toml, except for the -//! following manual edits: -//! - `winapi` dependency was removed. This is probably just due to the -//! fact that Hakari's target analysis is incomplete. -//! -//! There isn't any penalty to this data falling out of date; it just -//! means that under the conditions above Cargo will rebuild more -//! packages than strictly necessary. +// This is a stub lib.rs. diff --git a/zenith/Cargo.toml b/zenith/Cargo.toml index 8adbda0723..74aeffb51c 100644 --- a/zenith/Cargo.toml +++ b/zenith/Cargo.toml @@ -15,4 +15,4 @@ control_plane = { path = "../control_plane" } walkeeper = { path = "../walkeeper" } postgres_ffi = { path = "../postgres_ffi" } zenith_utils = { path = "../zenith_utils" } -workspace_hack = { path = "../workspace_hack" } +workspace_hack = { version = "0.1", path = "../workspace_hack" } diff --git a/zenith/src/main.rs b/zenith/src/main.rs index dd35427d5d..f5d4184e63 100644 --- a/zenith/src/main.rs +++ b/zenith/src/main.rs @@ -299,42 +299,40 @@ fn print_timelines_tree( .iter() .map(|t| { ( - t.timeline_id(), + t.timeline_id, TimelineTreeEl { info: t.clone(), children: BTreeSet::new(), name: timeline_name_mappings - .remove(&ZTenantTimelineId::new(t.tenant_id(), t.timeline_id())), + .remove(&ZTenantTimelineId::new(t.tenant_id, t.timeline_id)), }, ) }) .collect::>(); // Memorize all direct children of each timeline. - for timeline in &timelines { - if let TimelineInfo::Local { - ancestor_timeline_id: Some(tid), - .. - } = timeline + for timeline in timelines.iter() { + if let Some(ancestor_timeline_id) = + timeline.local.as_ref().and_then(|l| l.ancestor_timeline_id) { timelines_hash - .get_mut(tid) + .get_mut(&ancestor_timeline_id) .context("missing timeline info in the HashMap")? .children - .insert(timeline.timeline_id()); + .insert(timeline.timeline_id); } } for timeline in timelines_hash.values() { // Start with root local timelines (no ancestors) first. - if let TimelineInfo::Local { - ancestor_timeline_id, - .. - } = &timeline.info + if timeline + .info + .local + .as_ref() + .and_then(|l| l.ancestor_timeline_id) + .is_none() { - if ancestor_timeline_id.is_none() { - print_timeline(0, &Vec::from([true]), timeline, &timelines_hash)?; - } + print_timeline(0, &Vec::from([true]), timeline, &timelines_hash)?; } } @@ -350,20 +348,21 @@ fn print_timeline( timeline: &TimelineTreeEl, timelines: &HashMap, ) -> Result<()> { - let local_or_remote = match timeline.info { - TimelineInfo::Local { .. } => "(L)", - TimelineInfo::Remote { .. } => "(R)", + let local_remote = match (timeline.info.local.as_ref(), timeline.info.remote.as_ref()) { + (None, None) => unreachable!("in this case no info for a timeline is found"), + (None, Some(_)) => "(R)", + (Some(_), None) => "(L)", + (Some(_), Some(_)) => "(L+R)", }; // Draw main padding - print!("{} ", local_or_remote); + print!("{} ", local_remote); if nesting_level > 0 { - let lsn_string = match &timeline.info { - TimelineInfo::Local { ancestor_lsn, .. } => ancestor_lsn - .map(|lsn| lsn.to_string()) - .unwrap_or_else(|| "Unknown local Lsn".to_string()), - TimelineInfo::Remote { .. } => "unknown Lsn (remote)".to_string(), + let ancestor_lsn = match timeline.info.local.as_ref().and_then(|i| i.ancestor_lsn) { + Some(lsn) => lsn.to_string(), + None => "Unknown Lsn".to_string(), }; + let mut br_sym = "┣━"; // Draw each nesting padding with proper style @@ -383,14 +382,14 @@ fn print_timeline( br_sym = "┗━"; } - print!("{} @{}: ", br_sym, lsn_string); + print!("{} @{}: ", br_sym, ancestor_lsn); } // Finally print a timeline id and name with new line println!( "{} [{}]", timeline.name.as_deref().unwrap_or("_no_name_"), - timeline.info.timeline_id() + timeline.info.timeline_id ); let len = timeline.children.len(); @@ -430,7 +429,7 @@ fn get_timeline_infos( Ok(PageServerNode::from_env(env) .timeline_list(tenant_id)? .into_iter() - .map(|timeline_info| (timeline_info.timeline_id(), timeline_info)) + .map(|timeline_info| (timeline_info.timeline_id, timeline_info)) .collect()) } @@ -438,8 +437,8 @@ fn get_timeline_infos( fn get_tenant_id(sub_match: &ArgMatches, env: &local_env::LocalEnv) -> anyhow::Result { if let Some(tenant_id_from_arguments) = parse_tenant_id(sub_match).transpose() { tenant_id_from_arguments - } else if let Some(tenantid_conf) = env.default_tenant_id { - Ok(ZTenantId::from(tenantid_conf)) + } else if let Some(default_id) = env.default_tenant_id { + Ok(default_id) } else { bail!("No tenant id. Use --tenant-id, or set 'default_tenant_id' in the config file"); } @@ -480,7 +479,7 @@ fn handle_init(init_match: &ArgMatches) -> Result { .context("Failed to initialize zenith repository")?; // default_tenantid was generated by the `env.init()` call above - let initial_tenant_id = ZTenantId::from(env.default_tenant_id.unwrap()); + let initial_tenant_id = env.default_tenant_id.unwrap(); // Call 'pageserver init'. let pageserver = PageServerNode::from_env(&env); @@ -555,26 +554,17 @@ fn handle_timeline(timeline_match: &ArgMatches, env: &mut local_env::LocalEnv) - let timeline = pageserver .timeline_create(tenant_id, None, None, None)? .ok_or_else(|| anyhow!("Failed to create new timeline for tenant {}", tenant_id))?; - let new_timeline_id = timeline.timeline_id(); + let new_timeline_id = timeline.timeline_id; - let last_record_lsn = match timeline { - TimelineInfo::Local { - last_record_lsn, .. - } => last_record_lsn, - TimelineInfo::Remote { .. } => { - bail!( - "Timeline {} was created as remote, not local", - new_timeline_id - ) - } - }; + let last_record_lsn = timeline + .local + .expect("no local timeline info") + .last_record_lsn; env.register_branch_mapping(new_branch_name.to_string(), tenant_id, new_timeline_id)?; println!( "Created timeline '{}' at Lsn {} for tenant: {}", - timeline.timeline_id(), - last_record_lsn, - tenant_id, + timeline.timeline_id, last_record_lsn, tenant_id, ); } Some(("branch", branch_match)) => { @@ -602,26 +592,18 @@ fn handle_timeline(timeline_match: &ArgMatches, env: &mut local_env::LocalEnv) - let timeline = pageserver .timeline_create(tenant_id, None, start_lsn, Some(ancestor_timeline_id))? .ok_or_else(|| anyhow!("Failed to create new timeline for tenant {}", tenant_id))?; - let new_timeline_id = timeline.timeline_id(); + let new_timeline_id = timeline.timeline_id; - let last_record_lsn = match timeline { - TimelineInfo::Local { - last_record_lsn, .. - } => last_record_lsn, - TimelineInfo::Remote { .. } => bail!( - "Timeline {} was created as remote, not local", - new_timeline_id - ), - }; + let last_record_lsn = timeline + .local + .expect("no local timeline info") + .last_record_lsn; env.register_branch_mapping(new_branch_name.to_string(), tenant_id, new_timeline_id)?; println!( "Created timeline '{}' at Lsn {} for tenant: {}. Ancestor timeline: '{}'", - timeline.timeline_id(), - last_record_lsn, - tenant_id, - ancestor_branch_name, + timeline.timeline_id, last_record_lsn, tenant_id, ancestor_branch_name, ); } Some((sub_name, _)) => bail!("Unexpected tenant subcommand '{}'", sub_name), @@ -662,13 +644,8 @@ fn handle_pg(pg_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<()> { // older point in time, or following but lagging behind the primary. let lsn_str = timeline_infos .get(&node.timeline_id) - .map(|bi| match bi { - TimelineInfo::Local { - last_record_lsn, .. - } => last_record_lsn.to_string(), - TimelineInfo::Remote { .. } => "? (remote)".to_string(), - }) - .unwrap_or_else(|| '?'.to_string()); + .and_then(|bi| bi.local.as_ref().map(|l| l.last_record_lsn.to_string())) + .unwrap_or_else(|| "?".to_string()); let branch_name = timeline_name_mappings .get(&ZTenantTimelineId::new(tenant_id, node.timeline_id)) diff --git a/zenith_metrics/Cargo.toml b/zenith_metrics/Cargo.toml index 0c921ede0b..906c5a1d64 100644 --- a/zenith_metrics/Cargo.toml +++ b/zenith_metrics/Cargo.toml @@ -8,3 +8,4 @@ prometheus = {version = "0.13", default_features=false} # removes protobuf depen libc = "0.2" lazy_static = "1.4" once_cell = "1.8.0" +workspace_hack = { version = "0.1", path = "../workspace_hack" } diff --git a/zenith_utils/Cargo.toml b/zenith_utils/Cargo.toml index daaf345f8f..cf864b3a54 100644 --- a/zenith_utils/Cargo.toml +++ b/zenith_utils/Cargo.toml @@ -16,7 +16,7 @@ routerify = "3" serde = { version = "1.0", features = ["derive"] } serde_json = "1" thiserror = "1.0" -tokio = { version = "1.11", features = ["macros"]} +tokio = { version = "1.17", features = ["macros"]} tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } nix = "0.23.0" @@ -27,9 +27,10 @@ hex = { version = "0.4.3", features = ["serde"] } rustls = "0.19.1" rustls-split = "0.2.1" git-version = "0.3.5" +serde_with = "1.12.0" zenith_metrics = { path = "../zenith_metrics" } -workspace_hack = { path = "../workspace_hack" } +workspace_hack = { version = "0.1", path = "../workspace_hack" } [dev-dependencies] byteorder = "1.4.3" diff --git a/zenith_utils/scripts/restore_from_wal.sh b/zenith_utils/scripts/restore_from_wal.sh new file mode 100755 index 0000000000..ef2171312b --- /dev/null +++ b/zenith_utils/scripts/restore_from_wal.sh @@ -0,0 +1,20 @@ +PG_BIN=$1 +WAL_PATH=$2 +DATA_DIR=$3 +PORT=$4 +SYSID=`od -A n -j 24 -N 8 -t d8 $WAL_PATH/000000010000000000000002* | cut -c 3-` +rm -fr $DATA_DIR +env -i LD_LIBRARY_PATH=$PG_BIN/../lib $PG_BIN/initdb -E utf8 -D $DATA_DIR --sysid=$SYSID +echo port=$PORT >> $DATA_DIR/postgresql.conf +REDO_POS=0x`$PG_BIN/pg_controldata -D $DATA_DIR | fgrep "REDO location"| cut -c 42-` +declare -i WAL_SIZE=$REDO_POS+114 +$PG_BIN/pg_ctl -D $DATA_DIR -l logfile start +$PG_BIN/pg_ctl -D $DATA_DIR -l logfile stop -m immediate +cp $DATA_DIR/pg_wal/000000010000000000000001 . +cp $WAL_PATH/* $DATA_DIR/pg_wal/ +if [ -f $DATA_DIR/pg_wal/*.partial ] +then + (cd $DATA_DIR/pg_wal ; for partial in \*.partial ; do mv $partial `basename $partial .partial` ; done) +fi +dd if=000000010000000000000001 of=$DATA_DIR/pg_wal/000000010000000000000001 bs=$WAL_SIZE count=1 conv=notrunc +rm -f 000000010000000000000001 diff --git a/zenith_utils/scripts/restore_from_wal_archive.sh b/zenith_utils/scripts/restore_from_wal_archive.sh new file mode 100755 index 0000000000..07f4fe1e4f --- /dev/null +++ b/zenith_utils/scripts/restore_from_wal_archive.sh @@ -0,0 +1,20 @@ +PG_BIN=$1 +WAL_PATH=$2 +DATA_DIR=$3 +PORT=$4 +SYSID=`od -A n -j 24 -N 8 -t d8 $WAL_PATH/000000010000000000000002* | cut -c 3-` +rm -fr $DATA_DIR /tmp/pg_wals +mkdir /tmp/pg_wals +env -i LD_LIBRARY_PATH=$PG_BIN/../lib $PG_BIN/initdb -E utf8 -U zenith_admin -D $DATA_DIR --sysid=$SYSID +echo port=$PORT >> $DATA_DIR/postgresql.conf +REDO_POS=0x`$PG_BIN/pg_controldata -D $DATA_DIR | fgrep "REDO location"| cut -c 42-` +declare -i WAL_SIZE=$REDO_POS+114 +cp $WAL_PATH/* /tmp/pg_wals +if [ -f $DATA_DIR/pg_wal/*.partial ] +then + (cd /tmp/pg_wals ; for partial in \*.partial ; do mv $partial `basename $partial .partial` ; done) +fi +dd if=$DATA_DIR/pg_wal/000000010000000000000001 of=/tmp/pg_wals/000000010000000000000001 bs=$WAL_SIZE count=1 conv=notrunc +echo > $DATA_DIR/recovery.signal +rm -f $DATA_DIR/pg_wal/* +echo "restore_command = 'cp /tmp/pg_wals/%f %p'" >> $DATA_DIR/postgresql.conf diff --git a/zenith_utils/src/auth.rs b/zenith_utils/src/auth.rs index cbc4fcee61..8271121c63 100644 --- a/zenith_utils/src/auth.rs +++ b/zenith_utils/src/auth.rs @@ -14,8 +14,9 @@ use jsonwebtoken::{ decode, encode, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation, }; use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; -use crate::zid::{HexZTenantId, ZTenantId}; +use crate::zid::ZTenantId; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -26,18 +27,18 @@ pub enum Scope { PageServerApi, } +#[serde_as] #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Claims { - pub tenant_id: Option, + #[serde(default)] + #[serde_as(as = "Option")] + pub tenant_id: Option, pub scope: Scope, } impl Claims { pub fn new(tenant_id: Option, scope: Scope) -> Self { - Self { - tenant_id: tenant_id.map(HexZTenantId::from), - scope, - } + Self { tenant_id, scope } } } @@ -47,7 +48,7 @@ pub fn check_permission(claims: &Claims, tenantid: Option) -> Result< bail!("Attempt to access management api with tenant scope. Permission denied") } (Scope::Tenant, Some(tenantid)) => { - if ZTenantId::from(claims.tenant_id.unwrap()) != tenantid { + if claims.tenant_id.unwrap() != tenantid { bail!("Tenant id mismatch. Permission denied") } Ok(()) diff --git a/zenith_utils/src/http/endpoint.rs b/zenith_utils/src/http/endpoint.rs index 0be08f45e1..7669f18cd2 100644 --- a/zenith_utils/src/http/endpoint.rs +++ b/zenith_utils/src/http/endpoint.rs @@ -160,7 +160,7 @@ pub fn serve_thread_main( where S: Future + Send + Sync, { - info!("Starting a http endpoint at {}", listener.local_addr()?); + info!("Starting an HTTP endpoint at {}", listener.local_addr()?); // Create a Service from the router above to handle incoming requests. let service = RouterService::new(router_builder.build().map_err(|err| anyhow!(err))?).unwrap(); diff --git a/zenith_utils/src/http/error.rs b/zenith_utils/src/http/error.rs index 3262c33a51..b3bbec0f1c 100644 --- a/zenith_utils/src/http/error.rs +++ b/zenith_utils/src/http/error.rs @@ -14,6 +14,12 @@ pub enum ApiError { #[error("Unauthorized: {0}")] Unauthorized(String), + #[error("NotFound: {0}")] + NotFound(String), + + #[error("Conflict: {0}")] + Conflict(String), + #[error(transparent)] InternalServerError(#[from] anyhow::Error), } @@ -36,6 +42,12 @@ impl ApiError { self.to_string(), StatusCode::UNAUTHORIZED, ), + ApiError::NotFound(_) => { + HttpErrorBody::response_from_msg_and_status(self.to_string(), StatusCode::NOT_FOUND) + } + ApiError::Conflict(_) => { + HttpErrorBody::response_from_msg_and_status(self.to_string(), StatusCode::CONFLICT) + } ApiError::InternalServerError(err) => HttpErrorBody::response_from_msg_and_status( err.to_string(), StatusCode::INTERNAL_SERVER_ERROR, diff --git a/zenith_utils/src/http/json.rs b/zenith_utils/src/http/json.rs index f57e81649c..08f2ac4205 100644 --- a/zenith_utils/src/http/json.rs +++ b/zenith_utils/src/http/json.rs @@ -10,8 +10,8 @@ pub async fn json_request Deserialize<'de>>( let whole_body = hyper::body::aggregate(request.body_mut()) .await .map_err(ApiError::from_err)?; - Ok(serde_json::from_reader(whole_body.reader()) - .map_err(|err| ApiError::BadRequest(format!("Failed to parse json request {}", err)))?) + serde_json::from_reader(whole_body.reader()) + .map_err(|err| ApiError::BadRequest(format!("Failed to parse json request {}", err))) } pub fn json_response( diff --git a/zenith_utils/src/zid.rs b/zenith_utils/src/zid.rs index e047e38da7..fce5ed97c1 100644 --- a/zenith_utils/src/zid.rs +++ b/zenith_utils/src/zid.rs @@ -2,100 +2,19 @@ use std::{fmt, str::FromStr}; use hex::FromHex; use rand::Rng; -use serde::{ - de::{self, Visitor}, - Deserialize, Serialize, -}; - -macro_rules! mutual_from { - ($id1:ident, $id2:ident) => { - impl From<$id1> for $id2 { - fn from(id1: $id1) -> Self { - Self(id1.0.into()) - } - } - - impl From<$id2> for $id1 { - fn from(id2: $id2) -> Self { - Self(id2.0.into()) - } - } - }; -} +use serde::{Deserialize, Serialize}; /// Zenith ID is a 128-bit random ID. /// Used to represent various identifiers. Provides handy utility methods and impls. /// /// NOTE: It (de)serializes as an array of hex bytes, so the string representation would look /// like `[173,80,132,115,129,226,72,254,170,201,135,108,199,26,228,24]`. -/// Use [`HexZId`] to serialize it as hex string instead: `ad50847381e248feaac9876cc71ae418`. +/// +/// Use `#[serde_as(as = "DisplayFromStr")]` to (de)serialize it as hex string instead: `ad50847381e248feaac9876cc71ae418`. +/// Check the `serde_with::serde_as` documentation for options for more complex types. #[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] struct ZId([u8; 16]); -/// [`ZId`] version that serializes and deserializes as a hex string. -/// Useful for various json serializations, where hex byte array from original id is not convenient. -/// -/// Plain `ZId` could be (de)serialized into hex string with `#[serde(with = "hex")]` attribute. -/// This however won't work on nested types like `Option` or `Vec`, see https://github.com/serde-rs/serde/issues/723 for the details. -/// Every separate type currently needs a new (de)serializing method for every type separately. -/// -/// To provide a generic way to serialize the ZId as a hex string where `#[serde(with = "hex")]` is not enough, this wrapper is created. -/// The default wrapper serialization is left unchanged due to -/// * byte array (de)serialization being faster and simpler -/// * byte deserialization being used in Safekeeper already, with those bytes coming from compute (see `ProposerGreeting` in safekeeper) -/// * current `HexZId`'s deserialization impl breaks on compute byte array deserialization, having it by default is dangerous -#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -struct HexZId([u8; 16]); - -impl Serialize for HexZId { - fn serialize(&self, ser: S) -> Result - where - S: serde::Serializer, - { - hex::encode(self.0).serialize(ser) - } -} - -impl<'de> Deserialize<'de> for HexZId { - fn deserialize(de: D) -> Result - where - D: serde::Deserializer<'de>, - { - de.deserialize_bytes(HexVisitor) - } -} - -struct HexVisitor; - -impl<'de> Visitor<'de> for HexVisitor { - type Value = HexZId; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "A hexadecimal representation of a 128-bit random Zenith ID" - ) - } - - fn visit_bytes(self, hex_bytes: &[u8]) -> Result - where - E: de::Error, - { - ZId::from_hex(hex_bytes) - .map(HexZId::from) - .map_err(de::Error::custom) - } - - fn visit_str(self, hex_bytes_str: &str) -> Result - where - E: de::Error, - { - Self::visit_bytes(self, hex_bytes_str.as_bytes()) - } -} - -mutual_from!(ZId, HexZId); - impl ZId { pub fn get_from_buf(buf: &mut dyn bytes::Buf) -> ZId { let mut arr = [0u8; 16]; @@ -256,76 +175,22 @@ macro_rules! zid_newtype { /// /// NOTE: It (de)serializes as an array of hex bytes, so the string representation would look /// like `[173,80,132,115,129,226,72,254,170,201,135,108,199,26,228,24]`. -/// Use [`HexZTimelineId`] to serialize it as hex string instead: `ad50847381e248feaac9876cc71ae418`. +/// See [`ZId`] for alternative ways to serialize it. #[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)] pub struct ZTimelineId(ZId); -/// A [`ZTimelineId`] version that gets (de)serialized as a hex string. -/// Use in complex types, where `#[serde(with = "hex")]` does not work. -/// See [`HexZId`] for more details. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)] -pub struct HexZTimelineId(HexZId); - -impl std::fmt::Debug for HexZTimelineId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - ZTimelineId::from(*self).fmt(f) - } -} - -impl std::fmt::Display for HexZTimelineId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - ZTimelineId::from(*self).fmt(f) - } -} - -impl FromStr for HexZTimelineId { - type Err = ::Err; - - fn from_str(s: &str) -> Result { - Ok(HexZTimelineId::from(ZTimelineId::from_str(s)?)) - } -} - zid_newtype!(ZTimelineId); -mutual_from!(ZTimelineId, HexZTimelineId); /// Zenith Tenant Id represents identifiar of a particular tenant. /// Is used for distinguishing requests and data belonging to different users. /// /// NOTE: It (de)serializes as an array of hex bytes, so the string representation would look /// like `[173,80,132,115,129,226,72,254,170,201,135,108,199,26,228,24]`. -/// Use [`HexZTenantId`] to serialize it as hex string instead: `ad50847381e248feaac9876cc71ae418`. +/// See [`ZId`] for alternative ways to serialize it. #[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub struct ZTenantId(ZId); -/// A [`ZTenantId`] version that gets (de)serialized as a hex string. -/// Use in complex types, where `#[serde(with = "hex")]` does not work. -/// See [`HexZId`] for more details. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)] -pub struct HexZTenantId(HexZId); - -impl std::fmt::Debug for HexZTenantId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - ZTenantId::from(*self).fmt(f) - } -} - -impl std::fmt::Display for HexZTenantId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - ZTenantId::from(*self).fmt(f) - } -} - -impl FromStr for HexZTenantId { - type Err = ::Err; - - fn from_str(s: &str) -> Result { - Ok(HexZTenantId::from(ZTenantId::from_str(s)?)) - } -} - zid_newtype!(ZTenantId); -mutual_from!(ZTenantId, HexZTenantId); // A pair uniquely identifying Zenith instance. #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] @@ -368,55 +233,3 @@ impl fmt::Display for ZNodeId { write!(f, "{}", self.0) } } - -#[cfg(test)] -mod tests { - use std::fmt::Display; - - use super::*; - use hex::FromHexError; - use hex_literal::hex; - - #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] - struct TestStruct + Display> { - field: Option, - } - - #[test] - fn test_hex_serializations_tenant_id() { - let original_struct = TestStruct { - field: Some(HexZTenantId::from(ZTenantId::from_array(hex!( - "11223344556677881122334455667788" - )))), - }; - - let serialized_string = serde_json::to_string(&original_struct).unwrap(); - assert_eq!( - serialized_string, - r#"{"field":"11223344556677881122334455667788"}"# - ); - - let deserialized_struct: TestStruct = - serde_json::from_str(&serialized_string).unwrap(); - assert_eq!(original_struct, deserialized_struct); - } - - #[test] - fn test_hex_serializations_timeline_id() { - let original_struct = TestStruct { - field: Some(HexZTimelineId::from(ZTimelineId::from_array(hex!( - "AA223344556677881122334455667788" - )))), - }; - - let serialized_string = serde_json::to_string(&original_struct).unwrap(); - assert_eq!( - serialized_string, - r#"{"field":"aa223344556677881122334455667788"}"# - ); - - let deserialized_struct: TestStruct = - serde_json::from_str(&serialized_string).unwrap(); - assert_eq!(original_struct, deserialized_struct); - } -}