diff --git a/.github/actions/release-cn-artifacts/action.yaml b/.github/actions/release-cn-artifacts/action.yaml index 2825d3f5d0..fe78d5a760 100644 --- a/.github/actions/release-cn-artifacts/action.yaml +++ b/.github/actions/release-cn-artifacts/action.yaml @@ -37,17 +37,14 @@ inputs: description: Whether to push the latest tag of the image required: false default: 'true' - aws-cn-s3-bucket: - description: S3 bucket to store released artifacts in CN region + proxy-url: + description: The url of the S3 proxy server required: true - aws-cn-access-key-id: - description: AWS access key id in CN region + proxy-username: + description: The username of the S3 proxy required: true - aws-cn-secret-access-key: - description: AWS secret access key in CN region - required: true - aws-cn-region: - description: AWS region in CN + proxy-password: + description: The password of the S3 proxy required: true upload-to-s3: description: Upload to S3 @@ -77,21 +74,13 @@ runs: with: path: ${{ inputs.artifacts-dir }} - - name: Install s5cmd - shell: bash - run: | - wget https://github.com/peak/s5cmd/releases/download/v2.3.0/s5cmd_2.3.0_Linux-64bit.tar.gz - tar -xzf s5cmd_2.3.0_Linux-64bit.tar.gz - sudo mv s5cmd /usr/local/bin/ - sudo chmod +x /usr/local/bin/s5cmd - - name: Release artifacts to cn region uses: nick-invision/retry@v2 if: ${{ inputs.upload-to-s3 == 'true' }} env: - AWS_ACCESS_KEY_ID: ${{ inputs.aws-cn-access-key-id }} - AWS_SECRET_ACCESS_KEY: ${{ inputs.aws-cn-secret-access-key }} - AWS_REGION: ${{ inputs.aws-cn-region }} + PROXY_URL: ${{ inputs.proxy-url }} + PROXY_USERNAME: ${{ inputs.proxy-username }} + PROXY_PASSWORD: ${{ inputs.proxy-password }} UPDATE_VERSION_INFO: ${{ inputs.update-version-info }} with: max_attempts: ${{ inputs.upload-max-retry-times }} @@ -99,8 +88,7 @@ runs: command: | ./.github/scripts/upload-artifacts-to-s3.sh \ ${{ inputs.artifacts-dir }} \ - ${{ inputs.version }} \ - ${{ inputs.aws-cn-s3-bucket }} + ${{ inputs.version }} - name: Push greptimedb image from Dockerhub to ACR shell: bash diff --git a/.github/actions/setup-chaos/action.yml b/.github/actions/setup-chaos/action.yml index 76eb48cf4a..4b3fb86744 100644 --- a/.github/actions/setup-chaos/action.yml +++ b/.github/actions/setup-chaos/action.yml @@ -1,15 +1,16 @@ -name: Setup Kind -description: Deploy Kind +name: Setup Chaos Mesh +description: Install and wait for Chaos Mesh runs: using: composite steps: - uses: actions/checkout@v4 - - name: Create kind cluster + - name: Install Chaos Mesh shell: bash run: | helm repo add chaos-mesh https://charts.chaos-mesh.org + helm repo update chaos-mesh kubectl create ns chaos-mesh - helm install chaos-mesh chaos-mesh/chaos-mesh -n=chaos-mesh --version 2.6.3 + helm install chaos-mesh chaos-mesh/chaos-mesh -n=chaos-mesh --set chaosDaemon.runtime=containerd --set chaosDaemon.socketPath=/run/containerd/containerd.sock --version 2.8.0 - name: Print Chaos-mesh if: always() shell: bash diff --git a/.github/actions/setup-greptimedb-cluster/with-minio-repartition-gc.yaml b/.github/actions/setup-greptimedb-cluster/with-minio-repartition-gc.yaml index b836606ad6..5b7f43d81b 100644 --- a/.github/actions/setup-greptimedb-cluster/with-minio-repartition-gc.yaml +++ b/.github/actions/setup-greptimedb-cluster/with-minio-repartition-gc.yaml @@ -1,3 +1,8 @@ +logging: + level: "info" + format: "json" + filters: + - mito2::sst::file=debug meta: configData: |- [runtime] diff --git a/.github/cargo-blacklist.txt b/.github/cargo-blacklist.txt index d2f071130e..32e7878a86 100644 --- a/.github/cargo-blacklist.txt +++ b/.github/cargo-blacklist.txt @@ -1,3 +1,2 @@ native-tls openssl -aws-lc-sys diff --git a/.github/scripts/check-version.sh b/.github/scripts/check-version.sh index 28c2812ded..1efa3bb4db 100755 --- a/.github/scripts/check-version.sh +++ b/.github/scripts/check-version.sh @@ -30,13 +30,72 @@ CLEAN_LATEST=$(echo "$LATEST_VERSION" | sed 's/^v//' | sed 's/-nightly-.*//') echo "Current version: $CLEAN_CURRENT" echo "Latest release version: $CLEAN_LATEST" -# Use sort -V to compare versions -HIGHER_VERSION=$(printf "%s\n%s" "$CLEAN_CURRENT" "$CLEAN_LATEST" | sort -V | tail -n1) +# Function to extract base version (without pre-release suffix) +get_base_version() { + echo "$1" | sed -E 's/-(alpha|beta|rc|pre).*//' +} -if [ "$HIGHER_VERSION" = "$CLEAN_CURRENT" ]; then +# Function to check if a version is pre-release +is_prerelease() { + [[ "$1" =~ -(alpha|beta|rc|pre) ]] +} + +# Compare versions properly considering pre-release +compare_versions() { + local current=$1 + local latest=$2 + + # Extract base versions + local current_base=$(get_base_version "$current") + local latest_base=$(get_base_version "$latest") + + # Compare base versions first + HIGHER_BASE=$(printf "%s\n%s" "$current_base" "$latest_base" | sort -V | tail -n1) + + if [ "$HIGHER_BASE" = "$latest_base" ] && [ "$current_base" != "$latest_base" ]; then + # Latest has higher base version + echo "current_older" + return + elif [ "$HIGHER_BASE" = "$current_base" ] && [ "$current_base" != "$latest_base" ]; then + # Current has higher base version + echo "current_newer" + return + fi + + # Base versions are equal, compare pre-release status + if [ "$current_base" = "$latest_base" ]; then + # If current is pre-release and latest is not, current is older + if is_prerelease "$current" && ! is_prerelease "$latest"; then + echo "current_older" + return + fi + + # If latest is pre-release and current is not, current is newer + if ! is_prerelease "$current" && is_prerelease "$latest"; then + echo "current_newer" + return + fi + fi + + # Both are same type or different base versions already handled, use sort -V + HIGHER_VERSION=$(printf "%s\n%s" "$current" "$latest" | sort -V | tail -n1) + if [ "$HIGHER_VERSION" = "$current" ]; then + echo "current_newer_or_equal" + else + echo "current_older" + fi +} + +RESULT=$(compare_versions "$CLEAN_CURRENT" "$CLEAN_LATEST") + +if [ "$RESULT" = "current_newer" ] || [ "$RESULT" = "current_newer_or_equal" ]; then echo "Current version ($CLEAN_CURRENT) is NEWER than or EQUAL to latest ($CLEAN_LATEST)" - echo "is-current-version-latest=true" >> $GITHUB_OUTPUT + if [ -n "$GITHUB_OUTPUT" ]; then + echo "is-current-version-latest=true" >> $GITHUB_OUTPUT + fi else echo "Current version ($CLEAN_CURRENT) is OLDER than latest ($CLEAN_LATEST)" - echo "is-current-version-latest=false" >> $GITHUB_OUTPUT + if [ -n "$GITHUB_OUTPUT" ]; then + echo "is-current-version-latest=false" >> $GITHUB_OUTPUT + fi fi diff --git a/.github/scripts/update-dev-builder-version.sh b/.github/scripts/update-dev-builder-version.sh index 38466760a4..a593f385fd 100755 --- a/.github/scripts/update-dev-builder-version.sh +++ b/.github/scripts/update-dev-builder-version.sh @@ -30,8 +30,11 @@ update_dev_builder_version() { --body "This PR updates the dev-builder image tag" \ --base main \ --head $BRANCH_NAME \ - --reviewer zyy17 \ - --reviewer daviderli614 + --reviewer sunng87 \ + --reviewer daviderli614 \ + --reviewer killme2008 \ + --reviewer evenyag \ + --reviewer fengjiachun } update_dev_builder_version diff --git a/.github/scripts/upload-artifacts-to-s3.sh b/.github/scripts/upload-artifacts-to-s3.sh index 75c8f8d932..9c92859e3b 100755 --- a/.github/scripts/upload-artifacts-to-s3.sh +++ b/.github/scripts/upload-artifacts-to-s3.sh @@ -5,16 +5,15 @@ set -o pipefail ARTIFACTS_DIR=$1 VERSION=$2 -AWS_S3_BUCKET=$3 RELEASE_DIRS="releases/greptimedb" GREPTIMEDB_REPO="GreptimeTeam/greptimedb" # Check if necessary variables are set. function check_vars() { - for var in AWS_S3_BUCKET VERSION ARTIFACTS_DIR; do + for var in VERSION ARTIFACTS_DIR; do if [ -z "${!var}" ]; then echo "$var is not set or empty." - echo "Usage: $0 " + echo "Usage: $0 " exit 1 fi done @@ -33,8 +32,18 @@ function upload_artifacts() { # ├── greptime-darwin-amd64-v0.2.0.sha256sum # └── greptime-darwin-amd64-v0.2.0.tar.gz find "$ARTIFACTS_DIR" -type f \( -name "*.tar.gz" -o -name "*.sha256sum" \) | while IFS= read -r file; do - s5cmd cp \ - "$file" "s3://$AWS_S3_BUCKET/$RELEASE_DIRS/$VERSION/$(basename "$file")" + filename=$(basename "$file") + TARGET_URL="$PROXY_URL/$RELEASE_DIRS/$VERSION" + + curl -X PUT \ + -u "$PROXY_USERNAME:$PROXY_PASSWORD" \ + -F "file=@$file" \ + --max-time 3600 \ + --connect-timeout 20 \ + --retry 5 \ + --retry-delay 10 \ + --retry-max-time 3000 \ + "$TARGET_URL" done } @@ -45,16 +54,34 @@ function update_version_info() { if [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Updating latest-version.txt" echo "$VERSION" > latest-version.txt - s5cmd cp \ - latest-version.txt "s3://$AWS_S3_BUCKET/$RELEASE_DIRS/latest-version.txt" + TARGET_URL="$PROXY_URL/$RELEASE_DIRS" + + curl -X PUT \ + -u "$PROXY_USERNAME:$PROXY_PASSWORD" \ + -F "file=@latest-version.txt" \ + --max-time 3600 \ + --connect-timeout 20 \ + --retry 5 \ + --retry-delay 10 \ + --retry-max-time 3000 \ + "$TARGET_URL" fi # If it's the nightly release, update latest-nightly-version.txt. if [[ "$VERSION" == *"nightly"* ]]; then echo "Updating latest-nightly-version.txt" echo "$VERSION" > latest-nightly-version.txt - s5cmd cp \ - latest-nightly-version.txt "s3://$AWS_S3_BUCKET/$RELEASE_DIRS/latest-nightly-version.txt" + + TARGET_URL="$PROXY_URL/$RELEASE_DIRS" + curl -X PUT \ + -u "$PROXY_USERNAME:$PROXY_PASSWORD" \ + -F "file=@latest-nightly-version.txt" \ + --max-time 3600 \ + --connect-timeout 20 \ + --retry 5 \ + --retry-delay 10 \ + --retry-max-time 3000 \ + "$TARGET_URL" fi fi } @@ -93,10 +120,10 @@ function main() { } # Usage example: -# AWS_ACCESS_KEY_ID= \ -# AWS_SECRET_ACCESS_KEY= \ -# AWS_DEFAULT_REGION= \ +# PROXY_URL= \ +# PROXY_USERNAME= \ +# PROXY_PASSWORD= \ # UPDATE_VERSION_INFO=true \ # DOWNLOAD_ARTIFACTS_FROM_GITHUB=false \ -# ./upload-artifacts-to-s3.sh +# ./upload-artifacts-to-s3.sh main diff --git a/.github/workflows/bump-helm-charts-version.yml b/.github/workflows/bump-helm-charts-version.yml new file mode 100644 index 0000000000..5921ec8a8c --- /dev/null +++ b/.github/workflows/bump-helm-charts-version.yml @@ -0,0 +1,29 @@ +name: Bump helm charts version + +on: + workflow_dispatch: + inputs: + version: + description: The version to bump (e.g. v1.0.0) + required: true + type: string + +jobs: + bump-helm-charts-version: + name: Bump helm charts version + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Bump helm charts version + env: + GITHUB_TOKEN: ${{ secrets.HELM_CHARTS_REPO_TOKEN }} + VERSION: ${{ inputs.version }} + run: | + ./.github/scripts/update-helm-charts-version.sh diff --git a/.github/workflows/bump-homebrew-greptime-version.yml b/.github/workflows/bump-homebrew-greptime-version.yml new file mode 100644 index 0000000000..af8ca8fc99 --- /dev/null +++ b/.github/workflows/bump-homebrew-greptime-version.yml @@ -0,0 +1,29 @@ +name: Bump homebrew greptime version + +on: + workflow_dispatch: + inputs: + version: + description: The version to bump (e.g. v1.0.0) + required: true + type: string + +jobs: + bump-homebrew-greptime-version: + name: Bump homebrew greptime version + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Bump homebrew greptime version + env: + GITHUB_TOKEN: ${{ secrets.HOMEBREW_GREPTIME_REPO_TOKEN }} + VERSION: ${{ inputs.version }} + run: | + ./.github/scripts/update-homebrew-greptme-version.sh diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 021867e4ed..d03fbeff14 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -285,10 +285,9 @@ jobs: dst-image-registry: ${{ vars.ACR_IMAGE_REGISTRY }} dst-image-namespace: ${{ vars.IMAGE_NAMESPACE }} version: ${{ needs.allocate-runners.outputs.version }} - aws-cn-s3-bucket: ${{ vars.AWS_RELEASE_BUCKET }} - aws-cn-access-key-id: ${{ secrets.AWS_CN_ACCESS_KEY_ID }} - aws-cn-secret-access-key: ${{ secrets.AWS_CN_SECRET_ACCESS_KEY }} - aws-cn-region: ${{ vars.AWS_RELEASE_BUCKET_REGION }} + proxy-url: ${{ secrets.PROXY_URL }} + proxy-username: ${{ secrets.PROXY_USERNAME }} + proxy-password: ${{ secrets.PROXY_PASSWORD }} upload-to-s3: ${{ inputs.upload_artifacts_to_s3 }} dev-mode: true # Only build the standard images(exclude centos images). push-latest-tag: false # Don't push the latest tag to registry. diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 0238e92c8d..d0d2804c6a 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -319,7 +319,13 @@ jobs: include: - target: "fuzz_repartition_table" mode: - name: "Local WAL Repartition GC" + name: "Local WAL mito table repartition" + minio: true + kafka: false + values: "with-minio-repartition-gc.yaml" + - target: "fuzz_repartition_metric_table" + mode: + name: "Local WAL metric table repartition" minio: true kafka: false values: "with-minio-repartition-gc.yaml" @@ -455,6 +461,14 @@ jobs: path: /tmp/fuzz-monitor-dumps if-no-files-found: warn retention-days: 3 + - name: Upload CSV dumps + if: failure() + uses: actions/upload-artifact@v4 + with: + name: fuzz-tests-csv-dumps-${{ matrix.mode.name }}-${{ matrix.target }} + path: /tmp/greptime-fuzz-dumps + if-no-files-found: warn + retention-days: 3 - name: Delete cluster if: success() shell: bash @@ -492,6 +506,12 @@ jobs: minio: true kafka: false values: "with-minio.yaml" + - target: "fuzz_repartition_table_chaos" + mode: + name: "Local WAL repartition chaos" + minio: true + kafka: false + values: "with-minio-repartition-gc.yaml" steps: - name: Remove unused software run: | diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 9eaa38c789..14ebb6e715 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -236,10 +236,9 @@ jobs: dst-image-registry: ${{ vars.ACR_IMAGE_REGISTRY }} dst-image-namespace: ${{ vars.IMAGE_NAMESPACE }} version: ${{ needs.allocate-runners.outputs.version }} - aws-cn-s3-bucket: ${{ vars.AWS_RELEASE_BUCKET }} - aws-cn-access-key-id: ${{ secrets.AWS_CN_ACCESS_KEY_ID }} - aws-cn-secret-access-key: ${{ secrets.AWS_CN_SECRET_ACCESS_KEY }} - aws-cn-region: ${{ vars.AWS_RELEASE_BUCKET_REGION }} + proxy-url: ${{ secrets.PROXY_URL }} + proxy-username: ${{ secrets.PROXY_USERNAME }} + proxy-password: ${{ secrets.PROXY_PASSWORD }} upload-to-s3: false dev-mode: false update-version-info: false # Don't update version info in S3. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b0eb2d68c..9f8f2d9703 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -358,10 +358,9 @@ jobs: dst-image-registry: ${{ vars.ACR_IMAGE_REGISTRY }} dst-image-namespace: ${{ vars.IMAGE_NAMESPACE }} version: ${{ needs.allocate-runners.outputs.version }} - aws-cn-s3-bucket: ${{ vars.AWS_RELEASE_BUCKET }} - aws-cn-access-key-id: ${{ secrets.AWS_CN_ACCESS_KEY_ID }} - aws-cn-secret-access-key: ${{ secrets.AWS_CN_SECRET_ACCESS_KEY }} - aws-cn-region: ${{ vars.AWS_RELEASE_BUCKET_REGION }} + proxy-url: ${{ secrets.PROXY_URL }} + proxy-username: ${{ secrets.PROXY_USERNAME }} + proxy-password: ${{ secrets.PROXY_PASSWORD }} dev-mode: false upload-to-s3: true update-version-info: true diff --git a/.github/workflows/run-multi-lang-tests.yml b/.github/workflows/run-multi-lang-tests.yml index 4fc405b5b2..52fdca2117 100644 --- a/.github/workflows/run-multi-lang-tests.yml +++ b/.github/workflows/run-multi-lang-tests.yml @@ -127,7 +127,7 @@ jobs: run: | ./bins/greptime standalone start \ --http-addr 0.0.0.0:${{ inputs.http-port }} \ - --rpc-addr 0.0.0.0:4001 \ + --grpc-bind-addr 0.0.0.0:4001 \ --mysql-addr 0.0.0.0:${{ inputs.mysql-port }} \ --postgres-addr 0.0.0.0:${{ inputs.postgres-port }} \ --user-provider=static_user_provider:cmd:${{ inputs.username }}=${{ inputs.password }} > /tmp/greptimedb.log 2>&1 & diff --git a/.gitignore b/.gitignore index 862eb8c5b4..0789f3ec10 100644 --- a/.gitignore +++ b/.gitignore @@ -65,8 +65,14 @@ greptimedb_data # github !/.github -# Claude code +# AI related CLAUDE.md - -# AGENTS.md AGENTS.md +.codex +.gemini +.opencode +.worktrees/ + +# local design docs +docs/specs/ +.vs/ diff --git a/Cargo.lock b/Cargo.lock index 85c2b1ed2d..acfa25df68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,9 +200,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "anymap2" @@ -212,7 +212,7 @@ checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" [[package]] name = "api" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "arrow-schema 57.3.0", "common-base", @@ -565,7 +565,7 @@ dependencies = [ "arrow-schema 57.3.0", "arrow-select 57.3.0", "flatbuffers", - "lz4_flex 0.12.0", + "lz4_flex 0.12.1", "zstd", ] @@ -933,7 +933,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auth" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "async-trait", @@ -982,6 +982,29 @@ dependencies = [ "cc", ] +[[package]] +name = "aws-lc-rs" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.6.20" @@ -1129,9 +1152,9 @@ dependencies = [ [[package]] name = "backon" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302eaff5357a264a2c42f127ecb8bac761cf99749fc3dc95677e2743991f99e7" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" dependencies = [ "fastrand", "gloo-timers", @@ -1252,7 +1275,7 @@ version = "0.72.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1287,11 +1310,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -1523,7 +1546,7 @@ dependencies = [ [[package]] name = "cache" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "catalog", "common-error", @@ -1559,7 +1582,7 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "catalog" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "arrow 57.3.0", @@ -1654,6 +1677,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -1894,7 +1923,7 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "cli" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "async-stream", "async-trait", @@ -1923,6 +1952,7 @@ dependencies = [ "common-wal", "datatypes", "etcd-client", + "fs2", "futures", "humantime", "meta-client", @@ -1933,7 +1963,7 @@ dependencies = [ "paste", "query", "rand 0.9.1", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "servers", @@ -1946,11 +1976,12 @@ dependencies = [ "tokio", "tracing-appender", "url", + "uuid", ] [[package]] name = "client" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "arc-swap", @@ -1982,7 +2013,7 @@ dependencies = [ "serde_json", "snafu 0.8.6", "store-api", - "substrait 1.0.0-rc.2", + "substrait 1.0.0", "tokio", "tokio-stream", "tonic 0.14.2", @@ -2022,7 +2053,7 @@ dependencies = [ [[package]] name = "cmd" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "async-trait", @@ -2083,8 +2114,7 @@ dependencies = [ "query", "rand 0.9.1", "regex", - "reqwest", - "rexpect", + "reqwest 0.13.2", "serde", "serde_json", "servers", @@ -2139,7 +2169,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", ] [[package]] @@ -2153,32 +2193,21 @@ dependencies = [ "unicode-width 0.2.1", ] -[[package]] -name = "comma" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335" - [[package]] name = "common-base" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "ahash 0.8.12", "anymap2", "async-trait", "bitvec", "bytes", - "common-error", - "common-macro", "common-test-util", "futures", "lazy_static", - "paste", "pin-project", - "rand 0.9.1", "regex", "serde", - "snafu 0.8.6", "tokio", "toml 0.8.23", "zeroize", @@ -2186,14 +2215,14 @@ dependencies = [ [[package]] name = "common-catalog" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "const_format", ] [[package]] name = "common-config" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "common-base", "common-error", @@ -2209,7 +2238,6 @@ dependencies = [ "object-store", "serde", "serde_json", - "serde_with", "snafu 0.8.6", "temp-env", "tempfile", @@ -2218,7 +2246,7 @@ dependencies = [ [[package]] name = "common-datasource" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "arrow 57.3.0", "arrow-schema 57.3.0", @@ -2239,7 +2267,6 @@ dependencies = [ "futures", "lazy_static", "object-store", - "object_store_opendal", "orc-rust", "parquet", "paste", @@ -2254,7 +2281,7 @@ dependencies = [ [[package]] name = "common-decimal" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "bigdecimal 0.4.8", "common-error", @@ -2267,7 +2294,7 @@ dependencies = [ [[package]] name = "common-error" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "common-macro", "http 1.3.1", @@ -2278,7 +2305,7 @@ dependencies = [ [[package]] name = "common-event-recorder" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "async-trait", @@ -2301,7 +2328,7 @@ dependencies = [ [[package]] name = "common-frontend" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "async-trait", @@ -2322,7 +2349,7 @@ dependencies = [ [[package]] name = "common-function" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "ahash 0.8.12", "api", @@ -2360,8 +2387,8 @@ dependencies = [ "geohash", "h3o", "hyperloglogplus", + "icu_properties", "jsonb", - "jsonpath-rust 0.7.5", "memchr", "mito-codec", "nalgebra", @@ -2385,7 +2412,7 @@ dependencies = [ [[package]] name = "common-greptimedb-telemetry" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "async-trait", "common-runtime", @@ -2393,7 +2420,7 @@ dependencies = [ "common-test-util", "common-version", "hyper 0.14.32", - "reqwest", + "reqwest 0.13.2", "serde", "tempfile", "tokio", @@ -2402,7 +2429,7 @@ dependencies = [ [[package]] name = "common-grpc" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "arrow-flight", @@ -2437,7 +2464,7 @@ dependencies = [ [[package]] name = "common-grpc-expr" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "common-base", @@ -2457,7 +2484,7 @@ dependencies = [ [[package]] name = "common-macro" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "greptime-proto", "once_cell", @@ -2468,7 +2495,7 @@ dependencies = [ [[package]] name = "common-mem-prof" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "anyhow", "common-error", @@ -2484,11 +2511,10 @@ dependencies = [ [[package]] name = "common-memory-manager" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "common-error", "common-macro", - "common-telemetry", "humantime", "serde", "snafu 0.8.6", @@ -2497,7 +2523,7 @@ dependencies = [ [[package]] name = "common-meta" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "anymap2", "api", @@ -2568,7 +2594,7 @@ dependencies = [ [[package]] name = "common-options" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "common-grpc", "humantime-serde", @@ -2578,11 +2604,11 @@ dependencies = [ [[package]] name = "common-plugins" -version = "1.0.0-rc.2" +version = "1.0.0" [[package]] name = "common-pprof" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "common-error", "common-macro", @@ -2593,7 +2619,7 @@ dependencies = [ [[package]] name = "common-procedure" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "async-stream", @@ -2622,7 +2648,7 @@ dependencies = [ [[package]] name = "common-procedure-test" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "async-trait", "common-procedure", @@ -2632,7 +2658,7 @@ dependencies = [ [[package]] name = "common-query" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "async-trait", @@ -2658,12 +2684,13 @@ dependencies = [ [[package]] name = "common-recordbatch" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "arc-swap", "common-base", "common-error", "common-macro", + "common-memory-manager", "common-telemetry", "common-time", "criterion 0.7.0", @@ -2682,7 +2709,7 @@ dependencies = [ [[package]] name = "common-runtime" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "async-trait", "clap", @@ -2711,7 +2738,7 @@ dependencies = [ [[package]] name = "common-session" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "serde", "strum 0.27.1", @@ -2719,7 +2746,7 @@ dependencies = [ [[package]] name = "common-sql" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "arrow-schema 57.3.0", "common-base", @@ -2739,7 +2766,7 @@ dependencies = [ [[package]] name = "common-stat" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "common-base", "common-runtime", @@ -2754,7 +2781,7 @@ dependencies = [ [[package]] name = "common-telemetry" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "backtrace", "common-base", @@ -2767,7 +2794,7 @@ dependencies = [ "once_cell", "opentelemetry 0.30.0", "opentelemetry-otlp", - "opentelemetry-semantic-conventions", + "opentelemetry-semantic-conventions 0.30.0", "opentelemetry_sdk 0.30.0", "parking_lot 0.12.4", "prometheus 0.14.0", @@ -2783,7 +2810,7 @@ dependencies = [ [[package]] name = "common-test-util" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "client", "common-grpc", @@ -2796,7 +2823,7 @@ dependencies = [ [[package]] name = "common-time" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "arrow 57.3.0", "chrono", @@ -2814,7 +2841,7 @@ dependencies = [ [[package]] name = "common-version" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "cargo-manifest", "const_format", @@ -2824,7 +2851,7 @@ dependencies = [ [[package]] name = "common-wal" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "common-base", "common-error", @@ -2847,7 +2874,7 @@ dependencies = [ [[package]] name = "common-workload" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "common-telemetry", "serde", @@ -3330,6 +3357,22 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "ctr" version = "0.9.2" @@ -3567,7 +3610,7 @@ dependencies = [ "itertools 0.14.0", "liblzma", "log", - "object_store", + "object_store 0.12.5", "parking_lot 0.12.4", "parquet", "rand 0.9.1", @@ -3599,7 +3642,7 @@ dependencies = [ "futures", "itertools 0.14.0", "log", - "object_store", + "object_store 0.12.5", "parking_lot 0.12.4", "tokio", ] @@ -3623,7 +3666,7 @@ dependencies = [ "futures", "itertools 0.14.0", "log", - "object_store", + "object_store 0.12.5", ] [[package]] @@ -3641,7 +3684,7 @@ dependencies = [ "itertools 0.14.0", "libc", "log", - "object_store", + "object_store 0.12.5", "parquet", "paste", "recursive", @@ -3686,7 +3729,7 @@ dependencies = [ "itertools 0.14.0", "liblzma", "log", - "object_store", + "object_store 0.12.5", "rand 0.9.1", "tokio", "tokio-util", @@ -3713,7 +3756,7 @@ dependencies = [ "datafusion-session", "futures", "itertools 0.14.0", - "object_store", + "object_store 0.12.5", "tokio", ] @@ -3734,7 +3777,7 @@ dependencies = [ "datafusion-physical-plan", "datafusion-session", "futures", - "object_store", + "object_store 0.12.5", "regex", "tokio", ] @@ -3756,7 +3799,7 @@ dependencies = [ "datafusion-physical-plan", "datafusion-session", "futures", - "object_store", + "object_store 0.12.5", "serde_json", "tokio", "tokio-stream", @@ -3785,7 +3828,7 @@ dependencies = [ "futures", "itertools 0.14.0", "log", - "object_store", + "object_store 0.12.5", "parking_lot 0.12.4", "parquet", "tokio", @@ -3811,7 +3854,7 @@ dependencies = [ "datafusion-physical-expr-common", "futures", "log", - "object_store", + "object_store 0.12.5", "parking_lot 0.12.4", "rand 0.9.1", "tempfile", @@ -4020,7 +4063,7 @@ dependencies = [ "datafusion", "futures", "futures-util", - "object_store", + "object_store 0.12.5", "orc-rust", "tokio", ] @@ -4199,7 +4242,7 @@ dependencies = [ "datafusion", "half", "itertools 0.14.0", - "object_store", + "object_store 0.12.5", "pbjson-types", "prost 0.14.1", "substrait 0.62.2", @@ -4209,7 +4252,7 @@ dependencies = [ [[package]] name = "datanode" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "arrow-flight", @@ -4260,7 +4303,7 @@ dependencies = [ "prometheus 0.14.0", "prost 0.14.1", "query", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "servers", @@ -4277,7 +4320,7 @@ dependencies = [ [[package]] name = "datatypes" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "arrow 57.3.0", "arrow-array 57.3.0", @@ -4672,6 +4715,27 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf" +[[package]] +name = "dtor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "duration-str" version = "0.11.3" @@ -4955,7 +5019,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "file-engine" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "async-trait", @@ -4975,7 +5039,6 @@ dependencies = [ "datatypes", "futures", "object-store", - "object_store_opendal", "serde", "serde_json", "snafu 0.8.6", @@ -5026,7 +5089,7 @@ version = "25.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "rustc_version", ] @@ -5087,7 +5150,7 @@ checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" [[package]] name = "flow" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "arrow 57.3.0", @@ -5156,7 +5219,7 @@ dependencies = [ "sql", "store-api", "strum 0.27.1", - "substrait 1.0.0-rc.2", + "substrait 1.0.0", "table", "tokio", "tonic 0.14.2", @@ -5217,7 +5280,7 @@ checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" [[package]] name = "frontend" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "arc-swap", @@ -5261,12 +5324,14 @@ dependencies = [ "humantime", "humantime-serde", "hyper-util", + "itertools 0.14.0", "lazy_static", "log-query", "meta-client", "meta-srv", "num_cpus", "opentelemetry-proto 0.31.0", + "opentelemetry-semantic-conventions 0.31.0", "operator", "otel-arrow-rust", "partition", @@ -5276,7 +5341,7 @@ dependencies = [ "prost 0.14.1", "query", "rand 0.9.1", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "servers", @@ -5372,6 +5437,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -5664,7 +5735,7 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "libc", "libgit2-sys", "log", @@ -5692,7 +5763,7 @@ dependencies = [ [[package]] name = "greptime-proto" version = "0.1.0" -source = "git+https://github.com/GreptimeTeam/greptime-proto.git?rev=092ba1d01e2da676dca66cca7eebb55009da8ef8#092ba1d01e2da676dca66cca7eebb55009da8ef8" +source = "git+https://github.com/GreptimeTeam/greptime-proto.git?rev=0de5437582920c8b30d6c34212f161db71d95c50#0de5437582920c8b30d6c34212f161db71d95c50" dependencies = [ "prost 0.14.1", "prost-types 0.14.1", @@ -5702,7 +5773,6 @@ dependencies = [ "strum_macros 0.25.3", "tonic 0.14.2", "tonic-prost", - "tonic-prost-build", ] [[package]] @@ -6464,7 +6534,7 @@ dependencies = [ [[package]] name = "index" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "async-trait", "asynchronous-codec", @@ -6618,7 +6688,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "inotify-sys", "libc", ] @@ -6797,6 +6867,93 @@ dependencies = [ "regex", ] +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "js-sys", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "wasm-bindgen", + "windows-sys 0.61.2", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.33" @@ -6809,10 +6966,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -6897,16 +7056,18 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "9.3.1" +version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ + "aws-lc-rs", "base64 0.22.1", + "getrandom 0.2.16", "js-sys", "pem", - "ring", "serde", "serde_json", + "signature", "simple_asn1", ] @@ -7301,7 +7462,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -7336,7 +7497,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "libc", "redox_syscall 0.5.13", ] @@ -7432,7 +7593,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "log-query" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "chrono", "common-error", @@ -7444,7 +7605,7 @@ dependencies = [ [[package]] name = "log-store" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "async-stream", "async-trait", @@ -7598,18 +7759,18 @@ dependencies = [ [[package]] name = "lz4_flex" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" +checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" dependencies = [ "twox-hash", ] [[package]] name = "lz4_flex" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6473172471198271ff72e9379150e9dfd70d8e533e0752a27e515b48dd375e" +checksum = "98c23545df7ecf1b16c303910a69b079e8e251d60f7dd2cc9b4177f2afaf1746" dependencies = [ "twox-hash", ] @@ -7690,6 +7851,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" +[[package]] +name = "mea" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6747f54621d156e1b47eb6b25f39a941b9fc347f98f67d25d8881ff99e8ed832" +dependencies = [ + "slab", +] + [[package]] name = "measure_time" version = "0.9.0" @@ -7724,15 +7894,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.7.1" @@ -7744,7 +7905,7 @@ dependencies = [ [[package]] name = "meta-client" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "async-trait", @@ -7755,6 +7916,7 @@ dependencies = [ "common-meta", "common-options", "common-telemetry", + "common-time", "datatypes", "futures", "futures-util", @@ -7775,7 +7937,7 @@ dependencies = [ [[package]] name = "meta-srv" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "async-trait", @@ -7846,7 +8008,7 @@ dependencies = [ "toml 0.8.23", "tonic 0.14.2", "tower 0.5.2", - "tower-http 0.6.6", + "tower-http 0.6.8", "tracing", "tracing-subscriber", "typetag", @@ -7875,7 +8037,7 @@ dependencies = [ [[package]] name = "metric-engine" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "aquamarine", @@ -7887,6 +8049,7 @@ dependencies = [ "common-base", "common-error", "common-function", + "common-grpc", "common-macro", "common-meta", "common-query", @@ -7975,7 +8138,7 @@ dependencies = [ [[package]] name = "mito-codec" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "bytes", @@ -8000,13 +8163,15 @@ dependencies = [ [[package]] name = "mito2" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "aquamarine", + "arrow-schema 57.3.0", "async-channel 1.9.0", "async-stream", "async-trait", + "base64 0.22.1", "bytemuck", "bytes", "chrono", @@ -8268,7 +8433,7 @@ dependencies = [ "base64 0.21.7", "bigdecimal 0.4.8", "bindgen", - "bitflags 2.9.1", + "bitflags 2.11.1", "bitvec", "btoi", "byteorder", @@ -8306,7 +8471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34a9141e735d5bb02414a7ac03add09522466d4db65bdd827069f76ae0850e58" dependencies = [ "base64 0.22.1", - "bitflags 2.9.1", + "bitflags 2.11.1", "btoi", "byteorder", "bytes", @@ -8399,20 +8564,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.6.5", - "pin-utils", -] - [[package]] name = "nix" version = "0.26.4" @@ -8422,7 +8573,7 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset 0.7.1", + "memoffset", "pin-utils", ] @@ -8432,7 +8583,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -8444,7 +8595,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -8481,7 +8632,7 @@ version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "filetime", "fsevent-sys", "inotify", @@ -8738,10 +8889,12 @@ dependencies = [ [[package]] name = "object-store" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "anyhow", + "async-trait", "bytes", + "chrono", "common-base", "common-error", "common-macro", @@ -8751,12 +8904,16 @@ dependencies = [ "futures", "humantime-serde", "lazy_static", - "moka", + "object_store 0.12.5", + "object_store 0.13.2", + "object_store_opendal", "opendal", "prometheus 0.14.0", - "reqwest", + "rand 0.9.1", + "reqwest 0.13.2", "serde", "snafu 0.8.6", + "tempfile", "tokio", "uuid", ] @@ -8786,15 +8943,43 @@ dependencies = [ ] [[package]] -name = "object_store_opendal" -version = "0.54.0" +name = "object_store" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce697ee723fdc3eaf6c457abf4059034be15167022b18b619993802cd1443d5" +checksum = "622acbc9100d3c10e2ee15804b0caa40e55c933d5aa53814cd520805b7958a49" dependencies = [ "async-trait", "bytes", + "chrono", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "humantime", + "itertools 0.14.0", + "parking_lot 0.12.4", + "percent-encoding", + "thiserror 2.0.17", + "tokio", + "tracing", + "url", + "walkdir", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "object_store_opendal" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08298874eee5935c95bcaa393148834f9c53d904461ca15584a041d8a1c907c2" +dependencies = [ + "async-trait", + "bytes", + "chrono", "futures", - "object_store", + "mea", + "object_store 0.13.2", "opendal", "pin-project", "tokio", @@ -8844,7 +9029,7 @@ version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "libc", "once_cell", "onig_sys", @@ -8874,33 +9059,245 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "opendal" -version = "0.54.1" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42afda58fa2cf50914402d132cc1caacff116a85d10c72ab2082bb7c50021754" +checksum = "97b31d3d8e99a85d83b73ec26647f5607b80578ed9375810b6e44ffa3590a236" +dependencies = [ + "ctor", + "opendal-core", + "opendal-layer-concurrent-limit", + "opendal-layer-logging", + "opendal-layer-prometheus", + "opendal-layer-retry", + "opendal-layer-timeout", + "opendal-layer-tracing", + "opendal-service-azblob", + "opendal-service-fs", + "opendal-service-gcs", + "opendal-service-http", + "opendal-service-oss", + "opendal-service-s3", +] + +[[package]] +name = "opendal-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1849dd2687e173e776d3af5fce1ba3ae47b9dd37a09d1c4deba850ef45fe00ca" dependencies = [ "anyhow", - "backon", "base64 0.22.1", "bytes", - "chrono", - "crc32c", "futures", - "getrandom 0.2.16", "http 1.3.1", "http-body 1.0.1", + "jiff", "log", "md-5", + "mea", "percent-encoding", - "prometheus 0.14.0", "quick-xml 0.38.4", - "reqsign", - "reqwest", + "reqsign-core", + "reqwest 0.13.2", "serde", "serde_json", - "sha2", "tokio", - "tracing", + "url", "uuid", + "web-time", +] + +[[package]] +name = "opendal-layer-concurrent-limit" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048b1b29c503263bdd80a9afe46a68cd02ea9bd361185b1feab4b151078998e9" +dependencies = [ + "futures", + "http 1.3.1", + "mea", + "opendal-core", +] + +[[package]] +name = "opendal-layer-logging" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2645adc988b12eda106e2679ae529facfbbaa868ceb706f6f8125c6af15c47b" +dependencies = [ + "log", + "opendal-core", +] + +[[package]] +name = "opendal-layer-observe-metrics-common" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9130f0ac11569edc0f70b0e64078b9a12e37a128849d27ea62b0ca7568e8eb97" +dependencies = [ + "futures", + "http 1.3.1", + "opendal-core", +] + +[[package]] +name = "opendal-layer-prometheus" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef98056f8198b5053005e1fbe7d163562f56d9f3be1b73a3792667e7cbaf7be" +dependencies = [ + "opendal-core", + "opendal-layer-observe-metrics-common", + "prometheus 0.14.0", +] + +[[package]] +name = "opendal-layer-retry" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eac134ffa4ddda6131a640a84a5315996424b9416c85052f8c64c1a33b70ad4" +dependencies = [ + "backon", + "log", + "opendal-core", +] + +[[package]] +name = "opendal-layer-timeout" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619586ab7480c2e3009f6d18eabab18957bc094778fd130bcc38924970a90f4c" +dependencies = [ + "opendal-core", + "tokio", +] + +[[package]] +name = "opendal-layer-tracing" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f569b1cfae56851662e0db0e2d8df2f417d40aa281536c13f0c5211e7b56cd6f" +dependencies = [ + "futures", + "http 1.3.1", + "opendal-core", + "tracing", +] + +[[package]] +name = "opendal-service-azblob" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7452bf3ec61cfd81ac9ad9ada17825931e9e371d44a045c6bfab9596c0a2ac3b" +dependencies = [ + "base64 0.22.1", + "bytes", + "http 1.3.1", + "log", + "opendal-core", + "opendal-service-azure-common", + "quick-xml 0.38.4", + "reqsign-azure-storage", + "reqsign-core", + "reqsign-file-read-tokio", + "serde", + "sha2", + "uuid", +] + +[[package]] +name = "opendal-service-azure-common" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb0e45d6c8dcf66ce2da20e241bcb80e6e540e109a4ff20f318f6c9b4c54e0c" +dependencies = [ + "http 1.3.1", + "opendal-core", +] + +[[package]] +name = "opendal-service-fs" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf0be0417abeeb0053376d816b90fceb9ca98f20dfb54ebf1f2a282729f83663" +dependencies = [ + "bytes", + "log", + "opendal-core", + "serde", + "tokio", + "xattr", +] + +[[package]] +name = "opendal-service-gcs" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a49477a10163431896d106136117f5670717f9c9e49cf6f710528800c6633a" +dependencies = [ + "async-trait", + "bytes", + "http 1.3.1", + "log", + "opendal-core", + "percent-encoding", + "quick-xml 0.38.4", + "reqsign-core", + "reqsign-file-read-tokio", + "reqsign-google", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "opendal-service-http" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe73e6978feec293acfb92bfa94bdb9cf1b5be3f7c3f93a4333a25455826005" +dependencies = [ + "http 1.3.1", + "log", + "opendal-core", + "serde", +] + +[[package]] +name = "opendal-service-oss" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c8a917829ad06d21b639558532cb0101fe49b040d946d673a73018683fac05" +dependencies = [ + "bytes", + "http 1.3.1", + "log", + "opendal-core", + "quick-xml 0.38.4", + "reqsign-aliyun-oss", + "reqsign-core", + "reqsign-file-read-tokio", + "serde", +] + +[[package]] +name = "opendal-service-s3" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dadddeb9bb50b0d30927dd914c298c4ddca47e4c1cfa7674d311f0cf9b051c8" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc32c", + "http 1.3.1", + "log", + "md-5", + "opendal-core", + "quick-xml 0.38.4", + "reqsign-aws-v4", + "reqsign-core", + "reqsign-file-read-tokio", + "serde", + "url", ] [[package]] @@ -8917,7 +9314,7 @@ dependencies = [ [[package]] name = "opensrv-mysql" version = "0.8.0" -source = "git+https://github.com/datafuselabs/opensrv?tag=v0.10.0#074bd8fb81da3c9e6d6a098a482f3380478b9c0b" +source = "git+https://github.com/GreptimeTeam/opensrv?rev=6c5a451544194b7bb60a8318d155d4f892b49f2c#6c5a451544194b7bb60a8318d155d4f892b49f2c" dependencies = [ "async-trait", "byteorder", @@ -8974,7 +9371,7 @@ dependencies = [ "bytes", "http 1.3.1", "opentelemetry 0.30.0", - "reqwest", + "reqwest 0.12.28", ] [[package]] @@ -8989,7 +9386,7 @@ dependencies = [ "opentelemetry-proto 0.30.0", "opentelemetry_sdk 0.30.0", "prost 0.13.5", - "reqwest", + "reqwest 0.12.28", "thiserror 2.0.17", "tokio", "tonic 0.13.1", @@ -9031,6 +9428,12 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83d059a296a47436748557a353c5e6c5705b9470ef6c95cfc52c21a8814ddac2" +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e62e29dfe041afb8ed2a6c9737ab57db4907285d999ef8ad3a59092a36bdc846" + [[package]] name = "opentelemetry_sdk" version = "0.30.0" @@ -9066,7 +9469,7 @@ dependencies = [ [[package]] name = "operator" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "ahash 0.8.12", "api", @@ -9112,7 +9515,6 @@ dependencies = [ "meter-macros", "moka", "object-store", - "object_store_opendal", "partition", "path-slash", "prometheus 0.14.0", @@ -9126,7 +9528,7 @@ dependencies = [ "sql", "sqlparser", "store-api", - "substrait 1.0.0-rc.2", + "substrait 1.0.0", "table", "tokio", "tokio-util", @@ -9150,7 +9552,7 @@ dependencies = [ "flate2", "futures", "futures-util", - "lz4_flex 0.11.5", + "lz4_flex 0.11.6", "lzokay-native", "num", "prost 0.13.5", @@ -9373,11 +9775,11 @@ dependencies = [ "futures", "half", "hashbrown 0.16.1", - "lz4_flex 0.12.0", + "lz4_flex 0.12.1", "num-bigint", "num-integer", "num-traits", - "object_store", + "object_store 0.12.5", "paste", "seq-macro", "simdutf8", @@ -9402,7 +9804,7 @@ checksum = "e3c406c9e2aa74554e662d2c2ee11cd3e73756988800be7e6f5eddb16fed4699" [[package]] name = "partition" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "async-trait", @@ -9411,6 +9813,7 @@ dependencies = [ "common-macro", "common-meta", "common-query", + "common-telemetry", "criterion 0.7.0", "datafusion-common", "datafusion-expr", @@ -9619,11 +10022,12 @@ dependencies = [ [[package]] name = "pgwire" -version = "0.38.0" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d5e5a60d3f6e40c91f6a2a7f8d09665e636272bd5611977253559b6651aabb" +checksum = "24bd4e6b1bfddc5c6420dee6602ec80946700b4c31ddcb64ee190ad6d979c210" dependencies = [ "async-trait", + "aws-lc-rs", "base64 0.22.1", "bytes", "chrono", @@ -9635,7 +10039,6 @@ dependencies = [ "pg_interval_2", "postgres-types", "rand 0.10.0", - "ring", "rust_decimal", "rustls-pki-types", "ryu", @@ -9757,7 +10160,7 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pipeline" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "ahash 0.8.12", "api", @@ -9914,7 +10317,7 @@ dependencies = [ [[package]] name = "plugins" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "auth", "catalog", @@ -9952,6 +10355,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postgres-protocol" version = "0.6.8" @@ -10170,7 +10582,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "hex", "procfs-core", "rustix 0.38.44", @@ -10182,7 +10594,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "hex", ] @@ -10232,7 +10644,7 @@ dependencies = [ [[package]] name = "promql" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "ahash 0.8.12", "async-trait", @@ -10241,6 +10653,7 @@ dependencies = [ "common-macro", "common-recordbatch", "common-telemetry", + "criterion 0.7.0", "datafusion", "datafusion-common", "datafusion-expr", @@ -10276,7 +10689,7 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "num-traits", "rand 0.9.1", "rand_chacha 0.9.0", @@ -10332,7 +10745,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck 0.5.0", + "heck 0.4.1", "itertools 0.12.1", "log", "multimap", @@ -10352,7 +10765,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "itertools 0.14.0", "log", "multimap", @@ -10583,14 +10996,14 @@ dependencies = [ [[package]] name = "puffin" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "async-compression", "async-trait", "async-walkdir", "auto_impl", "base64 0.22.1", - "bitflags 2.9.1", + "bitflags 2.11.1", "bytes", "common-base", "common-error", @@ -10600,7 +11013,7 @@ dependencies = [ "common-test-util", "derive_builder 0.20.2", "futures", - "lz4_flex 0.11.5", + "lz4_flex 0.11.6", "moka", "pin-project", "prometheus 0.14.0", @@ -10619,7 +11032,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "memchr", "unicase", ] @@ -10645,7 +11058,7 @@ dependencies = [ [[package]] name = "query" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "ahash 0.8.12", "api", @@ -10675,11 +11088,13 @@ dependencies = [ "datafusion", "datafusion-common", "datafusion-expr", + "datafusion-expr-common", "datafusion-functions", "datafusion-optimizer", "datafusion-physical-expr", "datafusion-sql", "datatypes", + "either", "fastrand", "futures", "futures-util", @@ -10712,7 +11127,7 @@ dependencies = [ "sql", "sqlparser", "store-api", - "substrait 1.0.0-rc.2", + "substrait 1.0.0", "table", "tokio", "tokio-stream", @@ -10736,7 +11151,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ "memchr", - "serde", ] [[package]] @@ -10749,6 +11163,16 @@ dependencies = [ "serde", ] +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quinn" version = "0.11.8" @@ -10771,10 +11195,11 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.3", "lru-slab", @@ -11039,7 +11464,7 @@ version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", ] [[package]] @@ -11143,42 +11568,123 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" [[package]] -name = "reqsign" -version = "0.16.5" +name = "reqsign-aliyun-oss" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43451dbf3590a7590684c25fb8d12ecdcc90ed3ac123433e500447c7d77ed701" +checksum = "57ac2757f3140aa2e213b554148ae0b52733e624fc6723f0cc6bb3d440176c95" dependencies = [ "anyhow", - "async-trait", - "base64 0.22.1", - "chrono", "form_urlencoded", - "getrandom 0.2.16", - "hex", - "hmac", - "home", "http 1.3.1", - "jsonwebtoken", "log", - "once_cell", "percent-encoding", - "quick-xml 0.37.5", - "rand 0.8.5", - "reqwest", - "rsa", + "reqsign-core", "rust-ini 0.21.1", "serde", "serde_json", +] + +[[package]] +name = "reqsign-aws-v4" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44eaca382e94505a49f1a4849658d153aebf79d9c1a58e5dd3b10361511e9f43" +dependencies = [ + "anyhow", + "bytes", + "form_urlencoded", + "http 1.3.1", + "log", + "percent-encoding", + "quick-xml 0.39.2", + "reqsign-core", + "rust-ini 0.21.1", + "serde", + "serde_json", + "serde_urlencoded", "sha1", +] + +[[package]] +name = "reqsign-azure-storage" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a321980405d596bd34aaf95c4722a3de4128a67fd19e74a81a83aa3fdf082e6" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "http 1.3.1", + "jsonwebtoken", + "log", + "pem", + "percent-encoding", + "reqsign-core", + "rsa", + "serde", + "serde_json", + "sha1", +] + +[[package]] +name = "reqsign-core" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10302cf0a7d7e7352ba211fc92c3c5bebf1286153e49cc5aa87348078a8e102" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "futures", + "hex", + "hmac", + "http 1.3.1", + "jiff", + "log", + "percent-encoding", + "sha1", + "sha2", + "windows-sys 0.61.2", +] + +[[package]] +name = "reqsign-file-read-tokio" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d89295b3d17abea31851cc8de55d843d89c52132c864963c38d41920613dc5" +dependencies = [ + "anyhow", + "reqsign-core", + "tokio", +] + +[[package]] +name = "reqsign-google" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35cc609b49c69e76ecaceb775a03f792d1ed3e7755ab3548d4534fd801e3242e" +dependencies = [ + "form_urlencoded", + "http 1.3.1", + "jsonwebtoken", + "log", + "percent-encoding", + "reqsign-aws-v4", + "reqsign-core", + "rsa", + "serde", + "serde_json", "sha2", "tokio", ] [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", @@ -11193,13 +11699,54 @@ dependencies = [ "hyper-util", "js-sys", "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-rustls", + "tokio-util", + "tower 0.5.2", + "tower-http 0.6.8", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", + "webpki-roots 1.0.1", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", "mime_guess", "percent-encoding", "pin-project-lite", "quinn", "rustls", - "rustls-native-certs 0.8.1", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "serde_urlencoded", @@ -11208,27 +11755,13 @@ dependencies = [ "tokio-rustls", "tokio-util", "tower 0.5.2", - "tower-http 0.6.6", + "tower-http 0.6.8", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", - "webpki-roots 1.0.1", -] - -[[package]] -name = "rexpect" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ff60778f96fb5a48adbe421d21bf6578ed58c0872d712e7e08593c195adff8" -dependencies = [ - "comma", - "nix 0.25.1", - "regex", - "tempfile", - "thiserror 1.0.69", ] [[package]] @@ -11250,7 +11783,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -11345,9 +11878,9 @@ dependencies = [ [[package]] name = "rsasl" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8b534a23662bb559c5c73213be63ecd6524e774d291f3618c2b04b723d184eb" +checksum = "9f1bcb95b531681a622f3d6972eaab523e17e2aad6d6209f0276628eb1cb5038" dependencies = [ "base64 0.22.1", "core2", @@ -11359,7 +11892,7 @@ dependencies = [ "serde_json", "sha2", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.17", ] [[package]] @@ -11553,7 +12086,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -11566,7 +12099,7 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.9.4", @@ -11579,6 +12112,7 @@ version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", @@ -11610,7 +12144,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", + "security-framework 3.7.0", ] [[package]] @@ -11633,14 +12167,42 @@ dependencies = [ ] [[package]] -name = "rustls-webpki" -version = "0.103.3" +name = "rustls-platform-verifier" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs 0.8.1", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.7.0", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -11822,7 +12384,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -11831,11 +12393,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.2.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -11844,9 +12406,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -12029,7 +12591,7 @@ dependencies = [ [[package]] name = "servers" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "ahash 0.8.12", "api", @@ -12079,6 +12641,7 @@ dependencies = [ "datafusion-pg-catalog", "datatypes", "derive_builder 0.20.2", + "either", "futures", "futures-util", "headers", @@ -12096,6 +12659,7 @@ dependencies = [ "local-ip-address", "log-query", "loki-proto", + "metric-engine", "mime_guess", "mysql_async", "notify", @@ -12107,6 +12671,7 @@ dependencies = [ "operator", "otel-arrow-rust", "parking_lot 0.12.4", + "partition", "permutation", "pg_interval_2", "pgwire", @@ -12121,8 +12686,9 @@ dependencies = [ "quoted-string", "rand 0.9.1", "regex", - "reqwest", + "reqwest 0.13.2", "rust-embed", + "rust_decimal", "rustls", "rustls-pemfile", "rustls-pki-types", @@ -12131,6 +12697,7 @@ dependencies = [ "session", "simd-json", "simdutf8", + "smallvec", "snafu 0.8.6", "snap", "socket2 0.5.10", @@ -12148,8 +12715,9 @@ dependencies = [ "tokio-util", "tonic 0.14.2", "tonic-reflection", + "tonic-web", "tower 0.5.2", - "tower-http 0.6.6", + "tower-http 0.6.8", "tracing", "tracing-opentelemetry", "urlencoding", @@ -12160,7 +12728,7 @@ dependencies = [ [[package]] name = "session" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "ahash 0.8.12", "api", @@ -12346,9 +12914,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slotmap" @@ -12415,7 +12983,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1961e2ef424c1424204d3a5d6975f934f56b6d50ff5732382d84ebf460e147f7" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.117", @@ -12492,7 +13060,7 @@ dependencies = [ [[package]] name = "sql" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "arrow-buffer 57.3.0", @@ -12553,7 +13121,7 @@ dependencies = [ [[package]] name = "sqlness-runner" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "async-trait", "clap", @@ -12568,7 +13136,7 @@ dependencies = [ "local-ip-address", "mysql", "num_cpus", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "sha2", @@ -12712,7 +13280,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.1", + "bitflags 2.11.1", "byteorder", "bytes", "chrono", @@ -12756,7 +13324,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.1", + "bitflags 2.11.1", "byteorder", "chrono", "crc", @@ -12833,7 +13401,7 @@ dependencies = [ [[package]] name = "standalone" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "async-trait", "catalog", @@ -12877,7 +13445,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "store-api" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "aquamarine", @@ -13069,7 +13637,7 @@ dependencies = [ [[package]] name = "substrait" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "async-trait", "bytes", @@ -13191,7 +13759,7 @@ dependencies = [ [[package]] name = "table" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "arc-swap", @@ -13262,7 +13830,7 @@ dependencies = [ "levenshtein_automata", "log", "lru", - "lz4_flex 0.11.5", + "lz4_flex 0.11.6", "measure_time", "memmap2", "once_cell", @@ -13403,9 +13971,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -13461,7 +14029,7 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "tests-fuzz" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "arbitrary", "async-trait", @@ -13488,7 +14056,8 @@ dependencies = [ "paste", "rand 0.9.1", "rand_chacha 0.9.0", - "reqwest", + "reqwest 0.13.2", + "rustls", "schemars", "serde", "serde_json", @@ -13505,7 +14074,7 @@ dependencies = [ [[package]] name = "tests-integration" -version = "1.0.0-rc.2" +version = "1.0.0" dependencies = [ "api", "arrow-flight", @@ -13513,6 +14082,7 @@ dependencies = [ "async-trait", "auth", "axum 0.8.4", + "base64 0.22.1", "cache", "catalog", "chrono", @@ -13528,6 +14098,7 @@ dependencies = [ "common-grpc", "common-memory-manager", "common-meta", + "common-options", "common-procedure", "common-procedure-test", "common-query", @@ -13543,6 +14114,7 @@ dependencies = [ "datanode", "datatypes", "dotenv", + "either", "flate2", "flow", "frontend", @@ -13558,7 +14130,6 @@ dependencies = [ "meta-client", "meta-srv", "mito2", - "moka", "mysql_async", "object-store", "opentelemetry-proto 0.31.0", @@ -13567,6 +14138,7 @@ dependencies = [ "partition", "paste", "pipeline", + "plugins", "prost 0.14.1", "query", "rand 0.9.1", @@ -13582,7 +14154,7 @@ dependencies = [ "sqlx", "standalone", "store-api", - "substrait 1.0.0-rc.2", + "substrait 1.0.0", "table", "tempfile", "time", @@ -14132,6 +14704,24 @@ dependencies = [ "tonic-prost", ] +[[package]] +name = "tonic-web" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29453d84de05f4f1b573db22e6f9f6c95c189a6089a440c9a098aa9dea009299" +dependencies = [ + "base64 0.22.1", + "bytes", + "http 1.3.1", + "http-body 1.0.1", + "pin-project", + "tokio-stream", + "tonic 0.14.2", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.4.13" @@ -14179,7 +14769,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "base64 0.21.7", - "bitflags 2.9.1", + "bitflags 2.11.1", "bytes", "http 1.3.1", "http-body 1.0.1", @@ -14193,13 +14783,13 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", "base64 0.22.1", - "bitflags 2.9.1", + "bitflags 2.11.1", "bytes", "futures-core", "futures-util", @@ -14553,6 +15143,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -14752,7 +15348,7 @@ dependencies = [ "itertools 0.14.0", "lalrpop", "lalrpop-util", - "lz4_flex 0.11.5", + "lz4_flex 0.11.6", "md-5", "nom 7.1.3", "ofb", @@ -14873,48 +15469,32 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -14922,22 +15502,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.117", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -14977,13 +15557,26 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "hashbrown 0.15.4", "indexmap 2.13.0", "semver", @@ -14991,9 +15584,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -15016,7 +15609,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ "ring", - "untrusted", + "untrusted 0.9.0", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", ] [[package]] @@ -15080,7 +15682,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -15250,6 +15852,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -15286,6 +15897,21 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -15326,6 +15952,12 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -15338,6 +15970,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -15350,6 +15988,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -15368,6 +16012,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -15380,6 +16030,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -15392,6 +16048,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -15404,6 +16066,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -15469,7 +16137,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", ] [[package]] @@ -15510,7 +16178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.9.1", + "bitflags 2.11.1", "indexmap 2.13.0", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index 5041f167c3..c9ddba912a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,7 @@ members = [ resolver = "2" [workspace.package] -version = "1.0.0-rc.2" +version = "1.0.0" edition = "2024" license = "Apache-2.0" @@ -110,6 +110,7 @@ arrow-schema = { version = "57.3", features = ["serde"] } async-stream = "0.3" async-trait = "0.1" # Remember to update axum-extra, axum-macros when updating axum +arrow_object_store = { package = "object_store", version = "0.13.2" } axum = "0.8" axum-extra = "0.10" axum-macros = "0.5" @@ -131,6 +132,7 @@ datafusion = "=52.1" datafusion-common = "=52.1" datafusion-datasource = "=52.1" datafusion-expr = "=52.1" +datafusion-expr-common = "=52.1" datafusion-functions = "=52.1" datafusion-functions-aggregate-common = "=52.1" datafusion-functions-window-common = "=52.1" @@ -141,6 +143,7 @@ datafusion-physical-expr = "=52.1" datafusion-physical-plan = "=52.1" datafusion-sql = "=52.1" datafusion-substrait = "=52.1" +datafusion_object_store = { package = "object_store", version = "0.12.5" } deadpool = "0.12" deadpool-postgres = "0.14" derive_builder = "0.20" @@ -151,16 +154,18 @@ etcd-client = { version = "0.17", features = [ "tls", "tls-roots", ] } +fs2 = "0.4" fst = "0.4.7" futures = "0.3" futures-util = "0.3" -greptime-proto = { git = "https://github.com/GreptimeTeam/greptime-proto.git", rev = "092ba1d01e2da676dca66cca7eebb55009da8ef8" } +greptime-proto = { git = "https://github.com/GreptimeTeam/greptime-proto.git", rev = "0de5437582920c8b30d6c34212f161db71d95c50" } hex = "0.4" http = "1" humantime = "2.1" humantime-serde = "1.1" hyper = "1.1" hyper-util = "0.1" +icu_properties = "2.0.1" itertools = "0.14" jsonb = { version = "0.4.4", default-features = false } lazy_static = "1.4" @@ -173,7 +178,7 @@ nalgebra = "0.33" nix = { version = "0.30.1", default-features = false, features = ["event", "fs", "process"] } notify = "8.0" num_cpus = "1.16" -object_store_opendal = "0.54" +object_store_opendal = "0.56" once_cell = "1.18" opentelemetry-proto = { version = "0.31", features = [ "gen-tonic", @@ -200,14 +205,17 @@ rand = "0.9" ratelimit = "0.10" regex = "1.12" regex-automata = "0.4" -reqwest = { version = "0.12", default-features = false, features = [ +reqwest = { version = "0.13", default-features = false, features = [ + "form", "json", - "rustls-tls-native-roots", + "query", + "rustls", "stream", "multipart", ] } url = "2.3" # Branch: feat/request-timeout +hostname = "0.4.0" rskafka = { git = "https://github.com/GreptimeTeam/rskafka.git", rev = "f5688f83e7da591cda3f2674c2408b4c0ed4ed50", features = [ "transport-tls", ] } @@ -215,8 +223,6 @@ rstest = "0.25" rstest_reuse = "0.7" rust_decimal = "1.33" rustc-hash = "2.0" -# It is worth noting that we should try to avoid using aws-lc-rs until it can be compiled on various platforms. -hostname = "0.4.0" rustls = { version = "0.23.25", default-features = false } sea-query = "0.32" serde = { version = "1.0", features = ["derive"] } @@ -231,7 +237,8 @@ sqlx = { version = "0.8", default-features = false, features = [ "any", "macros", "json", - "runtime-tokio-rustls", + "runtime-tokio", + "tls-rustls-aws-lc-rs", "rust_decimal", ] } strum = { version = "0.27", features = ["derive"] } @@ -243,7 +250,7 @@ tokio-rustls = { version = "0.26.2", default-features = false } tokio-stream = "0.1" tokio-util = { version = "0.7", features = ["io-util", "compat"] } toml = "0.8.8" -tonic = { version = "0.14", features = ["tls-ring", "gzip", "zstd"] } +tonic = { version = "0.14", features = ["tls-aws-lc", "gzip", "zstd"] } tower = "0.5" tower-http = "0.6" tracing = "0.1" @@ -334,6 +341,7 @@ rev = "5618e779cf2bb4755b499c630fba4c35e91898cb" datafusion = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "02b82535e0160c4545667f36a03e1ff9d1d2e51f" } datafusion-common = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "02b82535e0160c4545667f36a03e1ff9d1d2e51f" } datafusion-expr = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "02b82535e0160c4545667f36a03e1ff9d1d2e51f" } +datafusion-expr-common = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "02b82535e0160c4545667f36a03e1ff9d1d2e51f" } datafusion-functions = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "02b82535e0160c4545667f36a03e1ff9d1d2e51f" } datafusion-functions-aggregate-common = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "02b82535e0160c4545667f36a03e1ff9d1d2e51f" } datafusion-functions-window-common = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "02b82535e0160c4545667f36a03e1ff9d1d2e51f" } diff --git a/Makefile b/Makefile index 60ea01a3ce..3fd09ad4ea 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ CARGO_BUILD_OPTS := --locked IMAGE_REGISTRY ?= docker.io IMAGE_NAMESPACE ?= greptime IMAGE_TAG ?= latest -DEV_BUILDER_IMAGE_TAG ?= 2025-10-01-8fe17d43-20251011080129 +DEV_BUILDER_IMAGE_TAG ?= 2026-03-21-9c9d9e9e-20260331090344 BUILDX_MULTI_PLATFORM_BUILD ?= false BUILDX_BUILDER_NAME ?= gtbuilder BASE_IMAGE ?= ubuntu diff --git a/README.md b/README.md index 290fe0263d..4ed99fa306 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ docker run -p 127.0.0.1:4000-4003:4000-4003 \ --name greptime --rm \ greptime/greptimedb:latest standalone start \ --http-addr 0.0.0.0:4000 \ - --rpc-bind-addr 0.0.0.0:4001 \ + --grpc-bind-addr 0.0.0.0:4001 \ --mysql-addr 0.0.0.0:4002 \ --postgres-addr 0.0.0.0:4003 ``` @@ -175,17 +175,16 @@ cargo run -- standalone start ## Project Status -> **Status:** RC — marching toward v1.0 GA! -> **GA (v1.0):** March 2026 +> **Status:** [v1.0 GA](https://github.com/GreptimeTeam/greptimedb/releases/tag/v1.0.0) — generally available and production-ready! 🎉 - Deployed in production handling billions of data points daily - Stable APIs, actively maintained, with regular releases ([version info](https://docs.greptime.com/nightly/reference/about-greptimedb-version)) -GreptimeDB v1.0 represents a major milestone toward maturity — marking stable APIs, production readiness, and proven performance. +GreptimeDB v1.0 marks a major milestone — stable APIs, production readiness, and proven performance at scale. -**Roadmap:** [v1.0 highlights and release plan](https://greptime.com/blogs/2025-11-05-greptimedb-v1-highlights) and [2026 roadmap](https://greptime.com/blogs/2026-02-11-greptimedb-roadmap-2026). +**Learn more:** [v1.0 highlights](https://greptime.com/blogs/2025-11-05-greptimedb-v1-highlights) and [2026 roadmap](https://greptime.com/blogs/2026-02-11-greptimedb-roadmap-2026). -For production use, we recommend using the latest stable release. +For production use, we recommend v1.0 or later. If you find this project useful, a ⭐ would mean a lot to us! diff --git a/cliff.toml b/cliff.toml index 4245203e92..2b35ddab5c 100644 --- a/cliff.toml +++ b/cliff.toml @@ -12,7 +12,9 @@ footer = "" body = """ # {{ version }} +{% if timestamp -%} Release date: {{ timestamp | date(format="%B %d, %Y") }} +{% endif -%} {%- set breakings = commits | filter(attribute="breaking", value=true) -%} {%- if breakings | length > 0 %} @@ -118,7 +120,10 @@ filter_commits = false # regex for skipping tags # skip_tags = "" # regex for ignoring tags -ignore_tags = ".*-nightly-.*" +# Ignore nightly tags and build-suffixed release tags such as +# v1.0.0-rc.2-13cdfa9b5-20260325-1774407105 so their commits are merged into +# the next visible release section instead of creating extra headings. +ignore_tags = ".*-nightly-.*|^v[0-9]+\\.[0-9]+\\.[0-9]+(-(alpha|beta|rc)\\.[0-9]+)?-[0-9a-f]{7,}-[0-9]{8}-[0-9]+$" # sort the tags topologically topo_order = false # sort the commits inside sections by oldest/newest order diff --git a/config/config.md b/config/config.md index 2ac11dd6e6..dfb4db1cf1 100644 --- a/config/config.md +++ b/config/config.md @@ -14,11 +14,12 @@ | --- | -----| ------- | ----------- | | `default_timezone` | String | Unset | The default timezone of the server. | | `default_column_prefix` | String | Unset | The default column prefix for auto-created time index and value columns. | +| `user_provider` | String | Unset | The user provider for authentication.
Examples: "static_user_provider:file:/path/to/users", "static_user_provider:cmd:greptime_user=greptime_pwd" | | `max_in_flight_write_bytes` | String | Unset | Maximum total memory for all concurrent write request bodies and messages (HTTP, gRPC, Flight).
Set to 0 to disable the limit. Default: "0" (unlimited) | | `write_bytes_exhausted_policy` | String | Unset | Policy when write bytes quota is exhausted.
Options: "wait" (default, 10s timeout), "wait()" (e.g., "wait(30s)"), "fail" | | `init_regions_in_background` | Bool | `false` | Initialize all regions in the background during the startup.
By default, it provides services after all regions have been initialized. | | `init_regions_parallelism` | Integer | `16` | Parallelism of initializing regions. | -| `max_concurrent_queries` | Integer | `0` | The maximum current queries allowed to be executed. Zero means unlimited.
NOTE: This setting affects scan_memory_limit's privileged tier allocation.
When set, 70% of queries get privileged memory access (full scan_memory_limit).
The remaining 30% get standard tier access (70% of scan_memory_limit). | +| `max_concurrent_queries` | Integer | `0` | The maximum concurrent queries allowed to be executed. Zero means unlimited. | | `enable_telemetry` | Bool | `true` | Enable telemetry to collect anonymous usage data. Enabled by default. | | `runtime` | -- | -- | The runtime options. | | `runtime.global_rt_size` | Integer | `8` | The number of threads to execute the runtime for global read operations. | @@ -69,6 +70,11 @@ | `prom_store` | -- | -- | Prometheus remote storage options | | `prom_store.enable` | Bool | `true` | Whether to enable Prometheus remote write and read in HTTP API. | | `prom_store.with_metric_engine` | Bool | `true` | Whether to store the data from Prometheus remote write in metric engine. | +| `prom_store.pending_rows_flush_interval` | String | `0s` | Interval to flush pending rows batcher.
Set to "0s" to disable batching mode in Prometheus Remote Write endpoint | +| `prom_store.max_batch_rows` | Integer | `100000` | Max rows per pending batch before triggering a flush. | +| `prom_store.max_concurrent_flushes` | Integer | `256` | Max number of concurrent batch flushes. | +| `prom_store.worker_channel_capacity` | Integer | `65526` | Capacity of the pending batch worker channel. | +| `prom_store.max_inflight_requests` | Integer | `3000` | Max inflight write requests before backpressure. | | `wal` | -- | -- | The WAL options. | | `wal.provider` | String | `raft_engine` | The provider of the WAL.
- `raft_engine`: the wal is stored in the local file system by raft-engine.
- `kafka`: it's remote wal that data is stored in Kafka. | | `wal.dir` | String | Unset | The directory to store the WAL files.
**It's only used when the provider is `raft_engine`**. | @@ -139,7 +145,7 @@ | `region_engine.mito.max_background_flushes` | Integer | Auto | Max number of running background flush jobs (default: 1/2 of cpu cores). | | `region_engine.mito.max_background_compactions` | Integer | Auto | Max number of running background compaction jobs (default: 1/4 of cpu cores). | | `region_engine.mito.max_background_purges` | Integer | Auto | Max number of running background purge jobs (default: number of cpu cores). | -| `region_engine.mito.experimental_compaction_memory_limit` | String | 0 | Memory budget for compaction tasks. Setting it to 0 or "unlimited" disables the limit. | +| `region_engine.mito.experimental_compaction_memory_limit` | String | 0 | Memory budget for compaction tasks.
Supports absolute size (e.g., "2GiB", "512MB") or percentage of system memory (e.g., "50%").
Setting it to 0 or "unlimited" disables the limit. | | `region_engine.mito.experimental_compaction_on_exhausted` | String | wait | Behavior when compaction cannot acquire memory from the budget.
Options: "wait" (default, 10s), "wait()", "fail" | | `region_engine.mito.auto_flush_interval` | String | `1h` | Interval to auto flush a region if it has not flushed yet. | | `region_engine.mito.global_write_buffer_size` | String | Auto | Global write buffer size for all regions. If not set, it's default to 1/8 of OS memory with a max limitation of 1GB. | @@ -157,12 +163,12 @@ | `region_engine.mito.enable_refill_cache_on_read` | Bool | `true` | Enable refilling cache on read operations (default: true).
When disabled, cache refilling on read won't happen. | | `region_engine.mito.manifest_cache_size` | String | `256MB` | Capacity for manifest cache (default: 256MB). | | `region_engine.mito.sst_write_buffer_size` | String | `8MB` | Buffer size for SST writing. | -| `region_engine.mito.parallel_scan_channel_size` | Integer | `32` | Capacity of the channel to send data from parallel scan tasks to the main task. | | `region_engine.mito.max_concurrent_scan_files` | Integer | `384` | Maximum number of SST files to scan concurrently. | | `region_engine.mito.allow_stale_entries` | Bool | `false` | Whether to allow stale WAL entries read during replay. | -| `region_engine.mito.scan_memory_limit` | String | `50%` | Memory limit for table scans across all queries.
Supports absolute size (e.g., "2GB") or percentage of system memory (e.g., "20%").
Setting it to 0 disables the limit.
NOTE: Works with max_concurrent_queries for tiered memory allocation.
- If max_concurrent_queries is set: 70% of queries get full access, 30% get 70% access.
- If max_concurrent_queries is 0 (unlimited): first 20 queries get full access, rest get 70% access. | +| `region_engine.mito.scan_memory_limit` | String | `50%` | Memory limit for table scans across all queries.
Supports absolute size (e.g., "2GB") or percentage of system memory (e.g., "20%").
Setting it to 0 disables the limit. | +| `region_engine.mito.scan_memory_on_exhausted` | String | `fail` | Controls what happens when a scan cannot get memory immediately.
"fail" (default) fails fast and is the recommended option for most users.
"wait" / "wait()" waits for memory to become available. This is mainly
for advanced tuning in bursty workloads where temporary contention is common and
higher latency is acceptable.
"wait" means "wait(10s)", not unlimited waiting. | | `region_engine.mito.min_compaction_interval` | String | `0m` | Minimum time interval between two compactions.
To align with the old behavior, the default value is 0 (no restrictions). | -| `region_engine.mito.default_experimental_flat_format` | Bool | `false` | Whether to enable experimental flat format as the default format. | +| `region_engine.mito.default_flat_format` | Bool | `true` | Whether to enable flat format as the default SST format. | | `region_engine.mito.index` | -- | -- | The options for index in Mito engine. | | `region_engine.mito.index.aux_path` | String | `""` | Auxiliary directory path for the index in filesystem, used to store intermediate files for
creating the index and staging files for searching the index, defaults to `{data_home}/index_intermediate`.
The default name for this directory is `index_intermediate` for backward compatibility.

This path contains two subdirectories:
- `__intm`: for storing intermediate files used during creating index.
- `staging`: for storing staging files used during searching index. | | `region_engine.mito.index.staging_size` | String | `2GB` | The max capacity of the staging directory. | @@ -226,14 +232,12 @@ | --- | -----| ------- | ----------- | | `default_timezone` | String | Unset | The default timezone of the server. | | `default_column_prefix` | String | Unset | The default column prefix for auto-created time index and value columns. | +| `user_provider` | String | Unset | The user provider for authentication.
Examples: "static_user_provider:file:/path/to/users", "static_user_provider:cmd:greptime_user=greptime_pwd" | | `max_in_flight_write_bytes` | String | Unset | Maximum total memory for all concurrent write request bodies and messages (HTTP, gRPC, Flight).
Set to 0 to disable the limit. Default: "0" (unlimited) | | `write_bytes_exhausted_policy` | String | Unset | Policy when write bytes quota is exhausted.
Options: "wait" (default, 10s timeout), "wait()" (e.g., "wait(30s)"), "fail" | | `runtime` | -- | -- | The runtime options. | | `runtime.global_rt_size` | Integer | `8` | The number of threads to execute the runtime for global read operations. | | `runtime.compact_rt_size` | Integer | `4` | The number of threads to execute the runtime for global write operations. | -| `heartbeat` | -- | -- | The heartbeat options. | -| `heartbeat.interval` | String | `18s` | Interval for sending heartbeat messages to the metasrv. | -| `heartbeat.retry_interval` | String | `3s` | Interval for retrying to send heartbeat messages to the metasrv. | | `http` | -- | -- | The HTTP server options. | | `http.addr` | String | `127.0.0.1:4000` | The address to bind the HTTP server. | | `http.timeout` | String | `0s` | HTTP request timeout. Set to 0 to disable timeout. | @@ -254,7 +258,7 @@ | `grpc.tls.watch` | Bool | `false` | Watch for Certificate and key file change and auto reload.
For now, gRPC tls config does not support auto reload. | | `internal_grpc` | -- | -- | The internal gRPC server options. Internal gRPC port for nodes inside cluster to access frontend. | | `internal_grpc.bind_addr` | String | `127.0.0.1:4010` | The address to bind the gRPC server. | -| `internal_grpc.server_addr` | String | `127.0.0.1:4010` | The address advertised to the metasrv, and used for connections from outside the host.
If left empty or unset, the server will automatically use the IP address of the first network interface
on the host, with the same port number as the one specified in `grpc.bind_addr`. | +| `internal_grpc.server_addr` | String | `127.0.0.1:4010` | The address advertised to the metasrv, and used for connections from outside the host.
If left empty or unset, the server will automatically use the IP address of the first network interface
on the host, with the same port number as the one specified in `internal_grpc.bind_addr`. | | `internal_grpc.runtime_size` | Integer | `8` | The number of server worker threads. | | `internal_grpc.flight_compression` | String | `arrow_ipc` | Compression mode for frontend side Arrow IPC service. Available options:
- `none`: disable all compression
- `transport`: only enable gRPC transport compression (zstd)
- `arrow_ipc`: only enable Arrow IPC compression (lz4)
- `all`: enable all compression.
Default to `none` | | `internal_grpc.tls` | -- | -- | internal gRPC server TLS options, see `mysql.tls` section. | @@ -292,6 +296,11 @@ | `prom_store` | -- | -- | Prometheus remote storage options | | `prom_store.enable` | Bool | `true` | Whether to enable Prometheus remote write and read in HTTP API. | | `prom_store.with_metric_engine` | Bool | `true` | Whether to store the data from Prometheus remote write in metric engine. | +| `prom_store.pending_rows_flush_interval` | String | `0s` | Interval to flush pending rows batcher.
Set to "0s" to disable batching mode in Prometheus Remote Write endpoint | +| `prom_store.max_batch_rows` | Integer | `100000` | Max rows per pending batch before triggering a flush. | +| `prom_store.max_concurrent_flushes` | Integer | `256` | Max number of concurrent batch flushes. | +| `prom_store.worker_channel_capacity` | Integer | `65526` | Capacity of the pending batch worker channel. | +| `prom_store.max_inflight_requests` | Integer | `3000` | Max inflight write requests before backpressure. | | `meta_client` | -- | -- | The metasrv client options. | | `meta_client.metasrv_addrs` | Array | -- | The addresses of the metasrv. | | `meta_client.timeout` | String | `3s` | Operation timeout. | @@ -352,7 +361,7 @@ | `region_failure_detector_initialization_delay` | String | `10m` | The delay before starting region failure detection.
This delay helps prevent Metasrv from triggering unnecessary region failovers before all Datanodes are fully started.
Especially useful when the cluster is not deployed with GreptimeDB Operator and maintenance mode is not enabled. | | `allow_region_failover_on_local_wal` | Bool | `false` | Whether to allow region failover on local WAL.
**This option is not recommended to be set to true, because it may lead to data loss during failover.** | | `node_max_idle_time` | String | `24hours` | Max allowed idle time before removing node info from metasrv memory. | -| `heartbeat_interval` | String | `3s` | Base heartbeat interval for calculating distributed time constants.
The frontend heartbeat interval is 6 times of the base heartbeat interval.
The flownode/datanode heartbeat interval is 1 times of the base heartbeat interval.
e.g., If the base heartbeat interval is 3s, the frontend heartbeat interval is 18s, the flownode/datanode heartbeat interval is 3s.
If you change this value, you need to change the heartbeat interval of the flownode/frontend/datanode accordingly. | +| `heartbeat_interval` | String | `3s` | Base heartbeat interval for calculating distributed time constants.
The frontend heartbeat interval is 6 times of the base heartbeat interval.
The flownode/datanode heartbeat interval is 1 times of the base heartbeat interval.
e.g., If the base heartbeat interval is 3s, the frontend heartbeat interval is 18s, the flownode/datanode heartbeat interval is 3s.
Heartbeat intervals are negotiated from metasrv during handshake; local node configs do not override this. | | `enable_telemetry` | Bool | `true` | Whether to enable greptimedb telemetry. Enabled by default. | | `runtime` | -- | -- | The runtime options. | | `runtime.global_rt_size` | Integer | `8` | The number of threads to execute the runtime for global read operations. | @@ -368,7 +377,7 @@ | `backend_client.connect_timeout` | String | `3s` | The connect timeout for backend client. | | `grpc` | -- | -- | The gRPC server options. | | `grpc.bind_addr` | String | `127.0.0.1:3002` | The address to bind the gRPC server. | -| `grpc.server_addr` | String | `127.0.0.1:3002` | The communication server address for the frontend and datanode to connect to metasrv.
If left empty or unset, the server will automatically use the IP address of the first network interface
on the host, with the same port number as the one specified in `bind_addr`. | +| `grpc.server_addr` | String | `127.0.0.1:3002` | The communication server address for the frontend and datanode to connect to metasrv.
If left empty or unset, the server will automatically use the IP address of the first network interface
on the host, with the same port number as the one specified in `grpc.bind_addr`. | | `grpc.runtime_size` | Integer | `8` | The number of server worker threads. | | `grpc.max_recv_message_size` | String | `512MB` | The maximum receive message size for gRPC server. | | `grpc.max_send_message_size` | String | `512MB` | The maximum send message size for gRPC server. | @@ -440,7 +449,7 @@ | `require_lease_before_startup` | Bool | `false` | Start services after regions have obtained leases.
It will block the datanode start if it can't receive leases in the heartbeat from metasrv. | | `init_regions_in_background` | Bool | `false` | Initialize all regions in the background during the startup.
By default, it provides services after all regions have been initialized. | | `init_regions_parallelism` | Integer | `16` | Parallelism of initializing regions. | -| `max_concurrent_queries` | Integer | `0` | The maximum current queries allowed to be executed. Zero means unlimited.
NOTE: This setting affects scan_memory_limit's privileged tier allocation.
When set, 70% of queries get privileged memory access (full scan_memory_limit).
The remaining 30% get standard tier access (70% of scan_memory_limit). | +| `max_concurrent_queries` | Integer | `0` | The maximum concurrent queries allowed to be executed. Zero means unlimited. | | `enable_telemetry` | Bool | `true` | Enable telemetry to collect anonymous usage data. Enabled by default. | | `http` | -- | -- | The HTTP server options. | | `http.addr` | String | `127.0.0.1:4000` | The address to bind the HTTP server. | @@ -461,9 +470,6 @@ | `runtime` | -- | -- | The runtime options. | | `runtime.global_rt_size` | Integer | `8` | The number of threads to execute the runtime for global read operations. | | `runtime.compact_rt_size` | Integer | `4` | The number of threads to execute the runtime for global write operations. | -| `heartbeat` | -- | -- | The heartbeat options. | -| `heartbeat.interval` | String | `3s` | Interval for sending heartbeat messages to the metasrv. | -| `heartbeat.retry_interval` | String | `3s` | Interval for retrying to send heartbeat messages to the metasrv. | | `meta_client` | -- | -- | The metasrv client options. | | `meta_client.metasrv_addrs` | Array | -- | The addresses of the metasrv. | | `meta_client.timeout` | String | `3s` | Operation timeout. | @@ -531,7 +537,7 @@ | `region_engine.mito.max_background_flushes` | Integer | Auto | Max number of running background flush jobs (default: 1/2 of cpu cores). | | `region_engine.mito.max_background_compactions` | Integer | Auto | Max number of running background compaction jobs (default: 1/4 of cpu cores). | | `region_engine.mito.max_background_purges` | Integer | Auto | Max number of running background purge jobs (default: number of cpu cores). | -| `region_engine.mito.experimental_compaction_memory_limit` | String | 0 | Memory budget for compaction tasks. Setting it to 0 or "unlimited" disables the limit. | +| `region_engine.mito.experimental_compaction_memory_limit` | String | 0 | Memory budget for compaction tasks.
Supports absolute size (e.g., "2GiB", "512MB") or percentage of system memory (e.g., "50%").
Setting it to 0 or "unlimited" disables the limit. | | `region_engine.mito.experimental_compaction_on_exhausted` | String | wait | Behavior when compaction cannot acquire memory from the budget.
Options: "wait" (default, 10s), "wait()", "fail" | | `region_engine.mito.auto_flush_interval` | String | `1h` | Interval to auto flush a region if it has not flushed yet. | | `region_engine.mito.global_write_buffer_size` | String | Auto | Global write buffer size for all regions. If not set, it's default to 1/8 of OS memory with a max limitation of 1GB. | @@ -549,12 +555,12 @@ | `region_engine.mito.enable_refill_cache_on_read` | Bool | `true` | Enable refilling cache on read operations (default: true).
When disabled, cache refilling on read won't happen. | | `region_engine.mito.manifest_cache_size` | String | `256MB` | Capacity for manifest cache (default: 256MB). | | `region_engine.mito.sst_write_buffer_size` | String | `8MB` | Buffer size for SST writing. | -| `region_engine.mito.parallel_scan_channel_size` | Integer | `32` | Capacity of the channel to send data from parallel scan tasks to the main task. | | `region_engine.mito.max_concurrent_scan_files` | Integer | `384` | Maximum number of SST files to scan concurrently. | | `region_engine.mito.allow_stale_entries` | Bool | `false` | Whether to allow stale WAL entries read during replay. | -| `region_engine.mito.scan_memory_limit` | String | `50%` | Memory limit for table scans across all queries.
Supports absolute size (e.g., "2GB") or percentage of system memory (e.g., "20%").
Setting it to 0 disables the limit.
NOTE: Works with max_concurrent_queries for tiered memory allocation.
- If max_concurrent_queries is set: 70% of queries get full access, 30% get 70% access.
- If max_concurrent_queries is 0 (unlimited): first 20 queries get full access, rest get 70% access. | +| `region_engine.mito.scan_memory_limit` | String | `50%` | Memory limit for table scans across all queries.
Supports absolute size (e.g., "2GB") or percentage of system memory (e.g., "20%").
Setting it to 0 disables the limit. | +| `region_engine.mito.scan_memory_on_exhausted` | String | `fail` | Controls what happens when a scan cannot get memory immediately.
"fail" (default) fails fast and is the recommended option for most users.
"wait" / "wait()" waits for memory to become available. This is mainly
for advanced tuning in bursty workloads where temporary contention is common and
higher latency is acceptable.
"wait" means "wait(10s)", not unlimited waiting. | | `region_engine.mito.min_compaction_interval` | String | `0m` | Minimum time interval between two compactions.
To align with the old behavior, the default value is 0 (no restrictions). | -| `region_engine.mito.default_experimental_flat_format` | Bool | `false` | Whether to enable experimental flat format as the default format. | +| `region_engine.mito.default_flat_format` | Bool | `true` | Whether to enable flat format as the default SST format. | | `region_engine.mito.index` | -- | -- | The options for index in Mito engine. | | `region_engine.mito.index.aux_path` | String | `""` | Auxiliary directory path for the index in filesystem, used to store intermediate files for
creating the index and staging files for searching the index, defaults to `{data_home}/index_intermediate`.
The default name for this directory is `index_intermediate` for backward compatibility.

This path contains two subdirectories:
- `__intm`: for storing intermediate files used during creating index.
- `staging`: for storing staging files used during searching index. | | `region_engine.mito.index.staging_size` | String | `2GB` | The max capacity of the staging directory. | @@ -614,6 +620,7 @@ | Key | Type | Default | Descriptions | | --- | -----| ------- | ----------- | | `node_id` | Integer | Unset | The flownode identifier and should be unique in the cluster. | +| `user_provider` | String | Unset | The user provider for authentication.
Examples: "static_user_provider:file:/path/to/users", "static_user_provider:cmd:greptime_user=greptime_pwd" | | `flow` | -- | -- | flow engine options. | | `flow.num_workers` | Integer | `0` | The number of flow worker in flownode.
Not setting(or set to 0) this value will use the number of CPU cores divided by 2. | | `flow.batching_mode` | -- | -- | -- | @@ -623,7 +630,6 @@ | `flow.batching_mode.grpc_conn_timeout` | String | `5s` | The gRPC connection timeout | | `flow.batching_mode.experimental_grpc_max_retries` | Integer | `3` | The gRPC max retry number | | `flow.batching_mode.experimental_frontend_scan_timeout` | String | `30s` | Flow wait for available frontend timeout,
if failed to find available frontend after frontend_scan_timeout elapsed, return error
which prevent flownode from starting | -| `flow.batching_mode.experimental_frontend_activity_timeout` | String | `60s` | Frontend activity timeout
if frontend is down(not sending heartbeat) for more than frontend_activity_timeout,
it will be removed from the list that flownode use to connect | | `flow.batching_mode.experimental_max_filter_num_per_query` | Integer | `20` | Maximum number of filters allowed in a single query | | `flow.batching_mode.experimental_time_window_merge_threshold` | Integer | `3` | Time window merge distance | | `flow.batching_mode.read_preference` | String | `Leader` | Read preference of the Frontend client. | @@ -651,9 +657,6 @@ | `meta_client.metadata_cache_max_capacity` | Integer | `100000` | The configuration about the cache of the metadata. | | `meta_client.metadata_cache_ttl` | String | `10m` | TTL of the metadata cache. | | `meta_client.metadata_cache_tti` | String | `5m` | -- | -| `heartbeat` | -- | -- | The heartbeat options. | -| `heartbeat.interval` | String | `3s` | Interval for sending heartbeat messages to the metasrv. | -| `heartbeat.retry_interval` | String | `3s` | Interval for retrying to send heartbeat messages to the metasrv. | | `logging` | -- | -- | The logging options. | | `logging.dir` | String | `./greptimedb_data/logs` | The directory to store the log files. If set to empty, logs will not be written to files. | | `logging.level` | String | Unset | The log level. Can be `info`/`debug`/`warn`/`error`. | diff --git a/config/datanode.example.toml b/config/datanode.example.toml index 2631a089e1..fbe205c17a 100644 --- a/config/datanode.example.toml +++ b/config/datanode.example.toml @@ -17,10 +17,7 @@ init_regions_in_background = false ## Parallelism of initializing regions. init_regions_parallelism = 16 -## The maximum current queries allowed to be executed. Zero means unlimited. -## NOTE: This setting affects scan_memory_limit's privileged tier allocation. -## When set, 70% of queries get privileged memory access (full scan_memory_limit). -## The remaining 30% get standard tier access (70% of scan_memory_limit). +## The maximum concurrent queries allowed to be executed. Zero means unlimited. max_concurrent_queries = 0 ## Enable telemetry to collect anonymous usage data. Enabled by default. @@ -83,14 +80,6 @@ watch = false ## The number of threads to execute the runtime for global write operations. #+ compact_rt_size = 4 -## The heartbeat options. -[heartbeat] -## Interval for sending heartbeat messages to the metasrv. -interval = "3s" - -## Interval for retrying to send heartbeat messages to the metasrv. -retry_interval = "3s" - ## The metasrv client options. [meta_client] ## The addresses of the metasrv. @@ -449,7 +438,9 @@ compress_manifest = false ## @toml2docs:none-default="Auto" #+ max_background_purges = 8 -## Memory budget for compaction tasks. Setting it to 0 or "unlimited" disables the limit. +## Memory budget for compaction tasks. +## Supports absolute size (e.g., "2GiB", "512MB") or percentage of system memory (e.g., "50%"). +## Setting it to 0 or "unlimited" disables the limit. ## @toml2docs:none-default="0" #+ experimental_compaction_memory_limit = "0" @@ -523,9 +514,6 @@ manifest_cache_size = "256MB" ## Buffer size for SST writing. sst_write_buffer_size = "8MB" -## Capacity of the channel to send data from parallel scan tasks to the main task. -parallel_scan_channel_size = 32 - ## Maximum number of SST files to scan concurrently. max_concurrent_scan_files = 384 @@ -535,17 +523,21 @@ allow_stale_entries = false ## Memory limit for table scans across all queries. ## Supports absolute size (e.g., "2GB") or percentage of system memory (e.g., "20%"). ## Setting it to 0 disables the limit. -## NOTE: Works with max_concurrent_queries for tiered memory allocation. -## - If max_concurrent_queries is set: 70% of queries get full access, 30% get 70% access. -## - If max_concurrent_queries is 0 (unlimited): first 20 queries get full access, rest get 70% access. scan_memory_limit = "50%" +## Controls what happens when a scan cannot get memory immediately. +## "fail" (default) fails fast and is the recommended option for most users. +## "wait" / "wait()" waits for memory to become available. This is mainly +## for advanced tuning in bursty workloads where temporary contention is common and +## higher latency is acceptable. +## "wait" means "wait(10s)", not unlimited waiting. +scan_memory_on_exhausted = "fail" ## Minimum time interval between two compactions. ## To align with the old behavior, the default value is 0 (no restrictions). min_compaction_interval = "0m" -## Whether to enable experimental flat format as the default format. -default_experimental_flat_format = false +## Whether to enable flat format as the default SST format. +default_flat_format = true ## The options for index in Mito engine. [region_engine.mito.index] diff --git a/config/flownode.example.toml b/config/flownode.example.toml index b13acfc447..2c053e6e8c 100644 --- a/config/flownode.example.toml +++ b/config/flownode.example.toml @@ -2,6 +2,11 @@ ## @toml2docs:none-default node_id = 14 +## The user provider for authentication. +## Examples: "static_user_provider:file:/path/to/users", "static_user_provider:cmd:greptime_user=greptime_pwd" +## @toml2docs:none-default +#+ user_provider = "static_user_provider:file:/path/to/users" + ## flow engine options. [flow] ## The number of flow worker in flownode. @@ -22,10 +27,6 @@ node_id = 14 ## if failed to find available frontend after frontend_scan_timeout elapsed, return error ## which prevent flownode from starting #+experimental_frontend_scan_timeout="30s" -## Frontend activity timeout -## if frontend is down(not sending heartbeat) for more than frontend_activity_timeout, -## it will be removed from the list that flownode use to connect -#+experimental_frontend_activity_timeout="60s" ## Maximum number of filters allowed in a single query #+experimental_max_filter_num_per_query=20 ## Time window merge distance @@ -96,14 +97,6 @@ metadata_cache_ttl = "10m" # TTI of the metadata cache. metadata_cache_tti = "5m" -## The heartbeat options. -[heartbeat] -## Interval for sending heartbeat messages to the metasrv. -interval = "3s" - -## Interval for retrying to send heartbeat messages to the metasrv. -retry_interval = "3s" - ## The logging options. [logging] ## The directory to store the log files. If set to empty, logs will not be written to files. diff --git a/config/frontend.example.toml b/config/frontend.example.toml index 435504b122..60115e93bb 100644 --- a/config/frontend.example.toml +++ b/config/frontend.example.toml @@ -6,6 +6,11 @@ default_timezone = "UTC" ## @toml2docs:none-default default_column_prefix = "greptime" +## The user provider for authentication. +## Examples: "static_user_provider:file:/path/to/users", "static_user_provider:cmd:greptime_user=greptime_pwd" +## @toml2docs:none-default +#+ user_provider = "static_user_provider:file:/path/to/users" + ## Maximum total memory for all concurrent write request bodies and messages (HTTP, gRPC, Flight). ## Set to 0 to disable the limit. Default: "0" (unlimited) ## @toml2docs:none-default @@ -23,14 +28,6 @@ default_column_prefix = "greptime" ## The number of threads to execute the runtime for global write operations. #+ compact_rt_size = 4 -## The heartbeat options. -[heartbeat] -## Interval for sending heartbeat messages to the metasrv. -interval = "18s" - -## Interval for retrying to send heartbeat messages to the metasrv. -retry_interval = "3s" - ## The HTTP server options. [http] ## The address to bind the HTTP server. @@ -100,7 +97,7 @@ watch = false bind_addr = "127.0.0.1:4010" ## The address advertised to the metasrv, and used for connections from outside the host. ## If left empty or unset, the server will automatically use the IP address of the first network interface -## on the host, with the same port number as the one specified in `grpc.bind_addr`. +## on the host, with the same port number as the one specified in `internal_grpc.bind_addr`. server_addr = "127.0.0.1:4010" ## The number of server worker threads. runtime_size = 8 @@ -214,6 +211,17 @@ enable = true enable = true ## Whether to store the data from Prometheus remote write in metric engine. with_metric_engine = true +## Interval to flush pending rows batcher. +## Set to "0s" to disable batching mode in Prometheus Remote Write endpoint +#+pending_rows_flush_interval = "0s" +## Max rows per pending batch before triggering a flush. +#+max_batch_rows = 100000 +## Max number of concurrent batch flushes. +#+max_concurrent_flushes = 256 +## Capacity of the pending batch worker channel. +#+worker_channel_capacity = 65526 +## Max inflight write requests before backpressure. +#+max_inflight_requests = 3000 ## The metasrv client options. [meta_client] diff --git a/config/metasrv.example.toml b/config/metasrv.example.toml index fed6e3b814..8e27e5be26 100644 --- a/config/metasrv.example.toml +++ b/config/metasrv.example.toml @@ -79,7 +79,7 @@ node_max_idle_time = "24hours" ## The frontend heartbeat interval is 6 times of the base heartbeat interval. ## The flownode/datanode heartbeat interval is 1 times of the base heartbeat interval. ## e.g., If the base heartbeat interval is 3s, the frontend heartbeat interval is 18s, the flownode/datanode heartbeat interval is 3s. -## If you change this value, you need to change the heartbeat interval of the flownode/frontend/datanode accordingly. +## Heartbeat intervals are negotiated from metasrv during handshake; local node configs do not override this. #+ heartbeat_interval = "3s" ## Whether to enable greptimedb telemetry. Enabled by default. @@ -136,7 +136,7 @@ ca_cert_path = "" bind_addr = "127.0.0.1:3002" ## The communication server address for the frontend and datanode to connect to metasrv. ## If left empty or unset, the server will automatically use the IP address of the first network interface -## on the host, with the same port number as the one specified in `bind_addr`. +## on the host, with the same port number as the one specified in `grpc.bind_addr`. server_addr = "127.0.0.1:3002" ## The number of server worker threads. runtime_size = 8 diff --git a/config/standalone.example.toml b/config/standalone.example.toml index ef96406316..aa745bb6ba 100644 --- a/config/standalone.example.toml +++ b/config/standalone.example.toml @@ -6,6 +6,11 @@ default_timezone = "UTC" ## @toml2docs:none-default default_column_prefix = "greptime" +## The user provider for authentication. +## Examples: "static_user_provider:file:/path/to/users", "static_user_provider:cmd:greptime_user=greptime_pwd" +## @toml2docs:none-default +#+ user_provider = "static_user_provider:file:/path/to/users" + ## Maximum total memory for all concurrent write request bodies and messages (HTTP, gRPC, Flight). ## Set to 0 to disable the limit. Default: "0" (unlimited) ## @toml2docs:none-default @@ -23,10 +28,7 @@ init_regions_in_background = false ## Parallelism of initializing regions. init_regions_parallelism = 16 -## The maximum current queries allowed to be executed. Zero means unlimited. -## NOTE: This setting affects scan_memory_limit's privileged tier allocation. -## When set, 70% of queries get privileged memory access (full scan_memory_limit). -## The remaining 30% get standard tier access (70% of scan_memory_limit). +## The maximum concurrent queries allowed to be executed. Zero means unlimited. max_concurrent_queries = 0 ## Enable telemetry to collect anonymous usage data. Enabled by default. @@ -176,6 +178,17 @@ enable = true enable = true ## Whether to store the data from Prometheus remote write in metric engine. with_metric_engine = true +## Interval to flush pending rows batcher. +## Set to "0s" to disable batching mode in Prometheus Remote Write endpoint +#+pending_rows_flush_interval = "0s" +## Max rows per pending batch before triggering a flush. +#+max_batch_rows = 100000 +## Max number of concurrent batch flushes. +#+max_concurrent_flushes = 256 +## Capacity of the pending batch worker channel. +#+worker_channel_capacity = 65526 +## Max inflight write requests before backpressure. +#+max_inflight_requests = 3000 ## The WAL options. [wal] @@ -541,7 +554,9 @@ compress_manifest = false ## @toml2docs:none-default="Auto" #+ max_background_purges = 8 -## Memory budget for compaction tasks. Setting it to 0 or "unlimited" disables the limit. +## Memory budget for compaction tasks. +## Supports absolute size (e.g., "2GiB", "512MB") or percentage of system memory (e.g., "50%"). +## Setting it to 0 or "unlimited" disables the limit. ## @toml2docs:none-default="0" #+ experimental_compaction_memory_limit = "0" @@ -615,9 +630,6 @@ manifest_cache_size = "256MB" ## Buffer size for SST writing. sst_write_buffer_size = "8MB" -## Capacity of the channel to send data from parallel scan tasks to the main task. -parallel_scan_channel_size = 32 - ## Maximum number of SST files to scan concurrently. max_concurrent_scan_files = 384 @@ -627,17 +639,21 @@ allow_stale_entries = false ## Memory limit for table scans across all queries. ## Supports absolute size (e.g., "2GB") or percentage of system memory (e.g., "20%"). ## Setting it to 0 disables the limit. -## NOTE: Works with max_concurrent_queries for tiered memory allocation. -## - If max_concurrent_queries is set: 70% of queries get full access, 30% get 70% access. -## - If max_concurrent_queries is 0 (unlimited): first 20 queries get full access, rest get 70% access. scan_memory_limit = "50%" +## Controls what happens when a scan cannot get memory immediately. +## "fail" (default) fails fast and is the recommended option for most users. +## "wait" / "wait()" waits for memory to become available. This is mainly +## for advanced tuning in bursty workloads where temporary contention is common and +## higher latency is acceptable. +## "wait" means "wait(10s)", not unlimited waiting. +scan_memory_on_exhausted = "fail" ## Minimum time interval between two compactions. ## To align with the old behavior, the default value is 0 (no restrictions). min_compaction_interval = "0m" -## Whether to enable experimental flat format as the default format. -default_experimental_flat_format = false +## Whether to enable flat format as the default SST format. +default_flat_format = true ## The options for index in Mito engine. [region_engine.mito.index] diff --git a/docker/dev-builder/centos/Dockerfile b/docker/dev-builder/centos/Dockerfile index 8b29bb3065..344bc1ec2a 100644 --- a/docker/dev-builder/centos/Dockerfile +++ b/docker/dev-builder/centos/Dockerfile @@ -7,7 +7,7 @@ RUN sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo RUN sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo # Install dependencies -RUN ulimit -n 1024000 && yum groupinstall -y 'Development Tools' +RUN yum groupinstall -y 'Development Tools' RUN yum install -y epel-release \ openssl \ openssl-devel \ diff --git a/docker/docker-compose/cluster-with-etcd.yaml b/docker/docker-compose/cluster-with-etcd.yaml index 34eee7f4b5..39f421e637 100644 --- a/docker/docker-compose/cluster-with-etcd.yaml +++ b/docker/docker-compose/cluster-with-etcd.yaml @@ -85,8 +85,8 @@ services: command: - metasrv - start - - --rpc-bind-addr=0.0.0.0:3002 - - --rpc-server-addr=metasrv:3002 + - --grpc-bind-addr=0.0.0.0:3002 + - --grpc-server-addr=metasrv:3002 - --store-addrs=etcd0:2379 - --http-addr=0.0.0.0:3000 healthcheck: @@ -111,8 +111,8 @@ services: - start - --node-id=0 - --data-home=/greptimedb_data - - --rpc-bind-addr=0.0.0.0:3001 - - --rpc-server-addr=datanode0:3001 + - --grpc-bind-addr=0.0.0.0:3001 + - --grpc-server-addr=datanode0:3001 - --metasrv-addrs=metasrv:3002 - --http-addr=0.0.0.0:5000 volumes: @@ -141,7 +141,7 @@ services: - start - --metasrv-addrs=metasrv:3002 - --http-addr=0.0.0.0:4000 - - --rpc-bind-addr=0.0.0.0:4001 + - --grpc-bind-addr=0.0.0.0:4001 - --mysql-addr=0.0.0.0:4002 - --postgres-addr=0.0.0.0:4003 healthcheck: @@ -166,8 +166,8 @@ services: - start - --node-id=0 - --metasrv-addrs=metasrv:3002 - - --rpc-bind-addr=0.0.0.0:4004 - - --rpc-server-addr=flownode0:4004 + - --grpc-bind-addr=0.0.0.0:4004 + - --grpc-server-addr=flownode0:4004 - --http-addr=0.0.0.0:4005 depends_on: frontend0: diff --git a/docs/rfcs/2025-12-30-export-import-v2.md b/docs/rfcs/2025-12-30-export-import-v2.md index 197eb7cc9d..6bc8428300 100644 --- a/docs/rfcs/2025-12-30-export-import-v2.md +++ b/docs/rfcs/2025-12-30-export-import-v2.md @@ -67,6 +67,7 @@ snapshot-20250101/ - Self-contained (all information needed for restore) - Immutable (content never changes after creation) - Verifiable (checksums at file, chunk, and snapshot levels) +- Schema-only snapshots contain only `manifest.json` and `schema/`; `data/` is absent, `chunks` is empty, and later data append is rejected (use `--force` to recreate) ### Chunk @@ -116,6 +117,8 @@ greptime export create \ --schema-only \ --to s3://my-bucket/snapshots/prod-schema-only +Schema-only snapshots cannot be resumed with data; use `--force` to recreate. + # Export with specific format (default: parquet) greptime export create \ --format csv \ @@ -173,7 +176,9 @@ The manifest is a JSON file containing snapshot metadata and chunk index: - `snapshot_id`: Unique identifier (UUID) - `catalog`, `schemas`: Catalog and schema list - `time_range`: Overall time range covered +- `schema_only`: Whether the snapshot contains schema only - `chunks[]`: Array of chunk metadata +- `format`: Data format for exported files - `checksum`: Snapshot-level SHA256 checksum **Chunk metadata structure**: @@ -182,7 +187,7 @@ Each chunk entry in the manifest contains: - `id`: Chunk identifier (sequential number) - `time_range`: Start and end timestamps -- `status`: Export status (Pending, Completed, Failed) +- `status`: Export status (Pending, InProgress, Completed, Failed) - `files`: List of data files in the chunk directory - `checksum`: Chunk-level checksum for integrity verification @@ -292,9 +297,9 @@ Checksums are verified during import before data is written to the database. **Resume capability**: -- Manifest tracks chunk status (Pending, Completed, Failed) +- Manifest tracks chunk status (Pending, InProgress, Completed, Failed) - Export/import automatically resumes when executed on existing snapshot -- Skips completed chunks, retries failed chunks, processes pending chunks +- Skips completed chunks, retries failed/in-progress chunks, processes pending chunks - Works across process restarts - Use `--force` (export only) to delete existing snapshot and start over diff --git a/docs/rfcs/2026-03-16-flow-inc-query.md b/docs/rfcs/2026-03-16-flow-inc-query.md new file mode 100644 index 0000000000..8041d37d2b --- /dev/null +++ b/docs/rfcs/2026-03-16-flow-inc-query.md @@ -0,0 +1,190 @@ +--- +Feature Name: Flow Batching Sequence-Based Incremental Query Plan (Lite) +Tracking Issue: TBD +Date: 2026-03-16 +Author: @discord9 +--- + +# Summary + +This RFC proposes a correctness-first incremental query mode for Flow batching. +Flow queries can read only `seq > checkpoint` and advance checkpoints using per-region correctness watermarks. +When incremental reads are stale or correctness cannot be proven, Flow falls back to full recomputation. + +# Motivation + +Flow batching still needs to repeatedly compute old data in the same time window, so incremental query can improve Flow performance. + +# Goals + +1. Add opt-in incremental reads (`seq > given_seq`) for Flow. +2. Return per-region correctness watermarks for checkpoint advancement. +3. Keep existing query behavior unchanged unless explicitly enabled. +4. Define deterministic fallback for stale or unprovable incremental reads. + +# Non-Goals + +1. No business-schema changes (no synthetic watermark columns in result rows). +2. No global throughput optimization in v1 (correctness first). +3. No observational watermark output when correctness is unprovable. + +# Proposal + +## 1) Query options + +Introduce three `QueryContext` extension keys: + +- `flow.incremental_after_seqs` +- `flow.incremental_mode` +- `flow.return_region_seq` + +These options are opt-in and only affect Flow incremental execution paths. + +## 2) Scan mapping + +When incremental mode is enabled: + +- map `after_seq` to `memtable_min_sequence` (exclusive lower bound) +- keep existing snapshot upper-bound behavior (`memtable_max_sequence`) + +Important limitation in v1: + +- incremental filtering is correctness-proven only for memtable rows +- SST files do not preserve detailed row-level sequence metadata; they only expose coarser file-level sequence information +- therefore `seq > checkpoint` must not assume precise incremental pruning across memtable->SST flush boundaries + +If required incremental parameters are missing or invalid, return argument error. + +## 3) Stale protection + +Add dedicated stale error: + +- `IncrementalQueryStale { region_id, given_seq, min_readable_seq }` + +Behavior: + +- if `given_seq < min_readable_seq`, return stale error +- if `given_seq == min_readable_seq`, query is valid and reads `seq > given_seq` +- if `given_seq > min_readable_seq`, query is also valid and reads `seq > given_seq` + +`IncrementalQueryStale` also covers the case where rows newer than the checkpoint have crossed a memtable->SST flush boundary and sequence-precise incremental exclusion can no longer be proven. +In other words, the flush-boundary case is not a separate fallback category in v1; it is one concrete way an incremental cursor becomes stale. + +## 4) Watermark return + +Extend query metrics with optional per-region watermark map: + +- `region_latest_sequences: Vec<(region_id: u64, latest_sequence: u64)>` + +Rules: + +- only terminal metrics of successful query can advance checkpoints +- for multi-region query, watermark must be complete map or absent +- if correctness is unprovable, business rows may return but watermark is absent + +## 5) Flow state machine + +Checkpoint and watermark state are kept only in flownode memory in v1; they are not persisted as durable flow metadata. +Cold start or flownode restart therefore always re-enters through a full snapshot read. +Only after that full query succeeds with a complete correctness watermark may Flow switch back to incremental mode. + +Flow starts in full mode, then transitions: + +1. Full query succeeds with correctness watermark -> enter incremental mode +2. Incremental query succeeds with correctness watermark -> advance checkpoint +3. Incremental stale/failure -> fallback to full mode +4. Full query without correctness watermark -> remain in full mode + +```mermaid +stateDiagram-v2 + [*] --> FullSnapshot: Flow starts + + state FullSnapshot { + [*] --> RunFull + RunFull --> RunFull: Full query succeeds but watermark is unprovable
no region_latest_sequences returned + } + + FullSnapshot --> Incremental: Full query succeeds and correctness watermark is returned
(checkpoint updated) + + state Incremental { + [*] --> RunInc + RunInc --> RunInc: Incremental succeeds
(checkpoint advances) + } + + Incremental --> FullSnapshot: IncrementalQueryStale
(cursor too old, fallback required) + Incremental --> FullSnapshot: Incremental fails
and fallback policy is triggered + + FullSnapshot --> [*]: Flow stops + Incremental --> [*]: Flow stops +``` + +### Fallback Policy + +Fallback to full mode is deterministic and is triggered by any of the following: + +1. `IncrementalQueryStale` is returned. +2. Incremental query fails with execution errors. +3. Incremental query succeeds but watermark is absent or incomplete for participating regions. + +Policy behavior: + +1. Do not advance any checkpoint in the failed/incomplete round. +2. Switch to full mode for the affected flow/window in the next round. +3. Return to incremental mode only after a full query succeeds with a complete correctness watermark map. + +### Persistence and recovery model + +The v1 design is intentionally correctness-first and keeps the progress cursor lightweight: + +1. Watermarks/checkpoints live only in flownode memory; v1 does not persist them separately. +2. On cold start, the flow re-establishes progress by running a successful full-query snapshot read, then resumes incremental mode only after that round returns a complete correctness watermark map. +3. Sequence-precise incremental correctness is currently limited to rows still visible in memtables. +4. Once relevant rows have been flushed into SST, the system cannot use `seq > checkpoint` alone to prove precise incremental exclusion, because SST lacks detailed row-level sequence metadata. +5. In that case the correct behavior is to fall back to full recomputation, not to continue a best-effort incremental scan. + +# Distributed and Compatibility Requirements + +1. Distributed path must preserve region-level snapshot/read-bound semantics end-to-end. +2. `snapshot_seqs` transport and `flow.*` options must both be carried correctly. + - `snapshot_seqs` means the per-region snapshot upper-bound map: `region_id -> sequence`. +3. New metrics fields must be backward-compatible (old clients ignore unknown fields). + +# Rollout Plan + +## Phase 1 (MVP, correctness first) + +1. Add extension constants and parsing. +2. Add incremental scan mapping and stale detection. +3. Add watermark metrics field and terminal-watermark checkpoint update path. +4. Complete standalone and distributed passthrough. + +## Phase 2 (performance and observability) + +1. Improve batching key strategy with sequence/watermark context. +2. Optimize watermark serialization overhead. +3. Add metrics: incremental hit rate, fallback rate, fallback window size. + +# Testing Plan + +1. Unit tests for incremental bounds and stale detection. +2. Query-path tests for extension mapping and watermark semantics. +3. Flow integration tests for full->incremental->fallback transitions. +4. Distributed tests for end-to-end snapshot/watermark propagation. +5. Compatibility tests for old/new client-server combinations. + +# Risks + +1. Boundary semantic mismatch (`<` vs `<=`) may cause correctness bugs. +2. Incomplete distributed propagation can silently invalidate watermark safety. +3. Frequent fallback can reduce throughput before phase-2 optimizations. +4. Memtable->SST flushes may force more full recomputation than expected until finer-grained SST sequence tracking exists. + +# Alternatives + +1. Put watermark into business rows (rejected: schema pollution). +2. Add new dedicated Flight message type in v1 (deferred to reduce scope). + +# Conclusion + +This plan enables a practical, correctness-first incremental path for Flow batching. +It reuses existing sequence scan capability, adds strict stale handling, and advances checkpoints only from correctness-proven per-region watermarks. diff --git a/docs/rfcs/remote-dyn-filter-rfc.md b/docs/rfcs/remote-dyn-filter-rfc.md new file mode 100644 index 0000000000..5f2243b15c --- /dev/null +++ b/docs/rfcs/remote-dyn-filter-rfc.md @@ -0,0 +1,136 @@ +--- +Feature Name: Remote Dynamic Filter Propagation +Tracking Issue: N/A +Date: 2026-04-08 +Author: @discord9 +--- + +# RFC: Remote Dynamic Filter Propagation + +# Summary + +This RFC proposes a remote dynamic filter propagation mechanism for distributed queries. It lets frontend-produced dynamic filters reach remote datanode scans through a lightweight control plane, while preserving one rule: remote dynamic filters are an optimization only, never a correctness dependency. + +# Motivation + +Today, dynamic filters can improve local execution, but they do not automatically propagate to remote datanode scans in distributed queries. As a result, the frontend may already know that a probe-side scan can be narrowed, while the remote scan still runs with a weaker predicate and loses pruning opportunities. + +We want a minimal design that: + +- propagates dynamic filter updates to remote scans, +- keeps filter identity and lifecycle stable across register/update/unregister, +- and safely degrades when encoding, routing, RPC, or apply logic fails. + +# Details + +The high-level flow is: + +1. A join on the frontend produces an alive dynamic filter. +2. `MergeScanExec` identifies the remote subscribers, generates a stable `filter_id`, and registers the alive filter into a query-scoped registry. +3. The initial remote read establishes the corresponding registration on the datanode side. +4. The frontend registry watches for dynamic filter updates via `wait_update` or generation changes. +5. Later updates and unregister messages are sent through the existing region unary RPC path. +6. The datanode applies these updates to query-scoped remote filter state and scan wrappers. +7. Query finish, cancel, or no-consumer conditions trigger unregister and cleanup. + +## Identity + +The logical identity of a remote dynamic filter is `query_id + filter_id`. + +Region and scan metadata are routing information, not part of filter-state identity. `filter_id` only needs to be stable and unique within a single query. + +The current recommendation is to derive `filter_id` from: + +- `region_id` +- `producer-local ordinal` +- `canonicalized children fingerprint` + +The following should not be included: + +- `partition` +- transport metadata +- memory addresses or temporary runtime object ids + +## Transport + +This design reuses the existing region unary gRPC path: + +- `RegionRequest.body.remote_dyn_filter` +- `RemoteDynFilterRequest.oneof action` + - `update` + - `unregister` + +The initial remote read is responsible for register and scan setup. The unary RPC path is only for later `update` and `unregister` messages. + +## Frontend registry + +The frontend uses a query-engine runtime map: + +- implementation near `src/query/src/dist_plan/remote_dyn_filter_registry.rs` +- storage model: `query_id -> Arc` + +This registry should not live on a single `MergeScanExec`, and it should not be stored in `QueryContext.mutable_session_data`. It is a query execution runtime object that owns watcher tasks, cleanup tail, and fanout state. + +The registry lifecycle has three states: + +- `Active`: accepts registrations and sends updates +- `Closing`: query ended; stop new registrations, send final cleanup messages, drain in-flight RPCs +- `Closed`: watchers stopped, state removable from the runtime map + +The registry may outlive the main query execution briefly for cleanup, but it is not intended to be a long-lived global object. + +## Propagation policy + +Remote dynamic filters should remain a selective optimization, not an automatic fanout for every filter update. + +The frontend may skip remote propagation when the encoded filter becomes too large, fanout cost is too high, or the expected pruning benefit is too small. In those cases, execution should continue with local-only dynamic filtering semantics. + +## Responsibilities + +On the frontend: + +- the join produces alive dynamic filters, +- `MergeScanExec` bridges producers to remote subscribers, +- the registry watches updates and fans out RPCs. + +On the datanode: + +- the unary handler receives `update` and `unregister`, +- query-scoped remote filter state is keyed by `query_id + filter_id`, +- remote wrappers apply updates through existing predicate and scan refresh paths. + +## Failure semantics + +All failures must degrade safely: + +- encode failure -> local-only filter +- RPC failure -> log/metric and degrade +- early update or missing target -> explicit buffer, drop+metric, or retry policy +- decode or remap failure -> disable remote optimization only + +# Alternatives + +## Registry on `MergeScanExec` + +Rejected because lifecycle and cleanup would become fragmented across multiple bridge or exec instances in the same query. + +## Registry in `QueryContext.mutable_session_data` + +Rejected because this is the wrong ownership model. The registry is not session metadata; it is a query runtime object with watcher tasks and cleanup behavior. + +## Long-lived global manager + +Rejected for now because it is heavier than necessary. A query-engine runtime map is sufficient for the current design. + +# Drawbacks + +- The design introduces extra query runtime state and cleanup logic on both frontend and datanode. +- The initial version only covers the current minimal filter form and leaves larger membership propagation to later work. +- A clear policy is still required for updates that arrive before scan registration. + +# Unresolved questions + +1. Should children fingerprint canonicalization become a shared helper? +2. What is the strict timing relationship between `is_complete` and final unregister? +3. Does the runtime map need a background sweep task, or is explicit reap enough? +4. How should large build-side membership evolve beyond `IN` in later work? diff --git a/flake.lock b/flake.lock index bec6e18d9a..3c3ee4bd67 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1770794449, - "narHash": "sha256-1nFkhcZx9+Sdw5OXwJqp5TxvGncqRqLeK781v0XV3WI=", + "lastModified": 1774250935, + "narHash": "sha256-mWID0WFgTnd9hbEeaPNX+YYWF70JN3r7zBouEqERJOE=", "owner": "nix-community", "repo": "fenix", - "rev": "b19d93fdf9761e6101f8cb5765d638bacebd9a1b", + "rev": "64d7705e8c37d650cfb1aa99c24a8ce46597f29e", "type": "github" }, "original": { @@ -41,11 +41,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1770617025, - "narHash": "sha256-1jZvgZoAagZZB6NwGRv2T2ezPy+X6EFDsJm+YSlsvEs=", + "lastModified": 1774244481, + "narHash": "sha256-4XfMXU0DjN83o6HWZoKG9PegCvKvIhNUnRUI19vzTcQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2db38e08fdadcc0ce3232f7279bab59a15b94482", + "rev": "4590696c8693fea477850fe379a01544293ca4e2", "type": "github" }, "original": { @@ -65,11 +65,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1770702974, - "narHash": "sha256-CbvWu72rpGHK5QynoXwuOnVzxX7njF2LYgk8wRSiAQ0=", + "lastModified": 1774221325, + "narHash": "sha256-aEIdkqB8gtQZtEbogdUb5iyfcZpKIlD3FkG8ANu73/I=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "07a594815f7c1d6e7e39f21ddeeedb75b21795f4", + "rev": "b42b63f390a4dab14e6efa34a70e67f5b087cc62", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 6a02f4f05f..8dc84136a0 100644 --- a/flake.nix +++ b/flake.nix @@ -20,7 +20,7 @@ lib = nixpkgs.lib; rustToolchain = fenix.packages.${system}.fromToolchainName { name = (lib.importTOML ./rust-toolchain.toml).toolchain.channel; - sha256 = "sha256-GCGEXGZeJySLND0KU5TdtTrqFV76TF3UdvAHSUegSsk="; + sha256 = "sha256-rboGKQLH4eDuiY01SINOqmXUFUNr9F4awoFZGzib17o="; }; in { diff --git a/grafana/dashboards/metrics/cluster/dashboard.json b/grafana/dashboards/metrics/cluster/dashboard.json index 918eb3c4e8..d56df06a3b 100644 --- a/grafana/dashboards/metrics/cluster/dashboard.json +++ b/grafana/dashboards/metrics/cluster/dashboard.json @@ -25,7 +25,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, - "id": 34, + "id": 33, "links": [], "panels": [ { @@ -927,7 +927,7 @@ "type": "stat" }, { - "collapsed": true, + "collapsed": false, "gridPos": { "h": 1, "w": 24, @@ -935,226 +935,226 @@ "y": 9 }, "id": 275, - "panels": [ + "panels": [], + "title": "Ingestion", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Total ingestion rate.\n\nHere we listed 3 primary protocols:\n\n- Prometheus remote write\n- Greptime's gRPC API (when using our ingest SDK)\n- Log ingestion http API\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "rowsps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 10 + }, + "id": 193, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ { "datasource": { "type": "prometheus", "uid": "${metrics}" }, - "description": "Total ingestion rate.\n\nHere we listed 3 primary protocols:\n\n- Prometheus remote write\n- Greptime's gRPC API (when using our ingest SDK)\n- Log ingestion http API\n", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "rowsps" - }, - "overrides": [] + "editorMode": "code", + "expr": "sum(rate(greptime_table_operator_ingest_rows{instance=~\"$frontend\"}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "ingestion", + "range": true, + "refId": "C" + } + ], + "title": "Total Ingestion Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Total ingestion rate.\n\nHere we listed 3 primary protocols:\n\n- Prometheus remote write\n- Greptime's gRPC API (when using our ingest SDK)\n- Log ingestion http API\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" }, - "gridPos": { - "h": 6, - "w": 24, - "x": 0, - "y": 18 - }, - "id": 193, - "options": { - "legend": { - "calcs": [ - "mean" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" } }, - "pluginVersion": "12.0.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" }, - "editorMode": "code", - "expr": "sum(rate(greptime_table_operator_ingest_rows{instance=~\"$frontend\"}[$__rate_interval]))", - "hide": false, - "instant": false, - "legendFormat": "ingestion", - "range": true, - "refId": "C" - } + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "rowsps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 277, + "options": { + "legend": { + "calcs": [ + "mean" ], - "title": "Total Ingestion Rate", - "type": "timeseries" + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum(rate(greptime_servers_http_logs_ingestion_counter[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "http-logs", + "range": true, + "refId": "http_logs" }, { "datasource": { "type": "prometheus", "uid": "${metrics}" }, - "description": "Total ingestion rate.\n\nHere we listed 3 primary protocols:\n\n- Prometheus remote write\n- Greptime's gRPC API (when using our ingest SDK)\n- Log ingestion http API\n", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "rowsps" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 24, - "x": 0, - "y": 70 - }, - "id": 277, - "options": { - "legend": { - "calcs": [ - "mean" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "12.0.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum(rate(greptime_servers_http_logs_ingestion_counter[$__rate_interval]))", - "hide": false, - "instant": false, - "legendFormat": "http-logs", - "range": true, - "refId": "http_logs" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum(rate(greptime_servers_prometheus_remote_write_samples[$__rate_interval]))", - "hide": false, - "instant": false, - "legendFormat": "prometheus-remote-write", - "range": true, - "refId": "prometheus-remote-write" - } - ], - "title": "Ingestion Rate by Type", - "type": "timeseries" + "editorMode": "code", + "expr": "sum(rate(greptime_servers_prometheus_remote_write_samples[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "prometheus-remote-write", + "range": true, + "refId": "prometheus-remote-write" } ], - "title": "Ingestion", - "type": "row" + "title": "Ingestion Rate by Type", + "type": "timeseries" }, { "collapsed": true, @@ -1162,7 +1162,7 @@ "h": 1, "w": 24, "x": 0, - "y": 10 + "y": 22 }, "id": 276, "panels": [ @@ -1231,7 +1231,7 @@ "h": 6, "w": 24, "x": 0, - "y": 19 + "y": 246 }, "id": 255, "options": { @@ -1303,7 +1303,7 @@ "h": 1, "w": 24, "x": 0, - "y": 11 + "y": 23 }, "id": 274, "panels": [ @@ -1371,7 +1371,7 @@ "h": 10, "w": 12, "x": 0, - "y": 20 + "y": 247 }, "id": 256, "options": { @@ -1486,7 +1486,7 @@ "h": 10, "w": 12, "x": 12, - "y": 20 + "y": 247 }, "id": 262, "options": { @@ -1603,7 +1603,7 @@ "h": 10, "w": 12, "x": 0, - "y": 30 + "y": 257 }, "id": 266, "options": { @@ -1718,7 +1718,7 @@ "h": 10, "w": 12, "x": 12, - "y": 30 + "y": 257 }, "id": 268, "options": { @@ -1835,7 +1835,7 @@ "h": 10, "w": 12, "x": 0, - "y": 40 + "y": 267 }, "id": 269, "options": { @@ -1950,7 +1950,7 @@ "h": 10, "w": 12, "x": 12, - "y": 40 + "y": 267 }, "id": 271, "options": { @@ -2067,7 +2067,7 @@ "h": 10, "w": 12, "x": 0, - "y": 50 + "y": 277 }, "id": 272, "options": { @@ -2182,7 +2182,7 @@ "h": 10, "w": 12, "x": 12, - "y": 50 + "y": 277 }, "id": 273, "options": { @@ -2245,7 +2245,7 @@ "h": 1, "w": 24, "x": 0, - "y": 12 + "y": 24 }, "id": 280, "panels": [ @@ -2314,7 +2314,7 @@ "h": 8, "w": 12, "x": 0, - "y": 61 + "y": 288 }, "id": 281, "options": { @@ -2415,7 +2415,7 @@ "h": 8, "w": 12, "x": 12, - "y": 61 + "y": 288 }, "id": 282, "options": { @@ -2516,7 +2516,7 @@ "h": 8, "w": 12, "x": 0, - "y": 69 + "y": 296 }, "id": 283, "options": { @@ -2617,7 +2617,7 @@ "h": 8, "w": 12, "x": 12, - "y": 69 + "y": 296 }, "id": 284, "options": { @@ -2716,7 +2716,7 @@ "h": 8, "w": 12, "x": 0, - "y": 77 + "y": 304 }, "id": 285, "options": { @@ -2817,7 +2817,7 @@ "h": 8, "w": 12, "x": 12, - "y": 77 + "y": 304 }, "id": 286, "options": { @@ -2919,7 +2919,7 @@ "h": 8, "w": 12, "x": 0, - "y": 85 + "y": 312 }, "id": 287, "options": { @@ -3020,7 +3020,7 @@ "h": 8, "w": 12, "x": 12, - "y": 85 + "y": 312 }, "id": 288, "options": { @@ -3064,7 +3064,7 @@ "h": 1, "w": 24, "x": 0, - "y": 13 + "y": 25 }, "id": 289, "panels": [ @@ -3133,7 +3133,7 @@ "h": 6, "w": 24, "x": 0, - "y": 62 + "y": 289 }, "id": 292, "options": { @@ -3234,7 +3234,7 @@ "h": 8, "w": 12, "x": 0, - "y": 68 + "y": 295 }, "id": 290, "options": { @@ -3335,7 +3335,7 @@ "h": 8, "w": 12, "x": 12, - "y": 68 + "y": 295 }, "id": 291, "options": { @@ -3464,7 +3464,7 @@ "h": 8, "w": 12, "x": 0, - "y": 76 + "y": 303 }, "id": 336, "options": { @@ -3518,2519 +3518,2520 @@ "type": "row" }, { - "collapsed": false, + "collapsed": true, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 14 + "y": 26 }, "id": 293, - "panels": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Request QPS per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 193 + }, + "id": 294, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod, type) (rate(greptime_mito_handle_request_elapsed_count{instance=~\"$datanode\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", + "range": true, + "refId": "A" + } + ], + "title": "Request OPS per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Request P99 per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 193 + }, + "id": 295, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(instance, pod, le, type) (rate(greptime_mito_handle_request_elapsed_bucket{instance=~\"$datanode\"}[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", + "range": true, + "refId": "A" + } + ], + "title": "Request P99 per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Write Buffer per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 225 + }, + "id": 296, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_mito_write_buffer_bytes{instance=~\"$datanode\"}", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]", + "range": true, + "refId": "A" + } + ], + "title": "Write Buffer per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Ingestion size by row counts.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "rowsps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 225 + }, + "id": 297, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by (instance, pod) (rate(greptime_mito_write_rows_total{instance=~\"$datanode\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]", + "range": true, + "refId": "A" + } + ], + "title": "Write Rows per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Flush QPS per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 233 + }, + "id": 298, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod, reason) (rate(greptime_mito_flush_requests_total{instance=~\"$datanode\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{reason}}]", + "range": true, + "refId": "A" + } + ], + "title": "Flush OPS per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Write Stall per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 233 + }, + "id": 299, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod) (greptime_mito_write_stall_total{instance=~\"$datanode\"})", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]", + "range": true, + "refId": "A" + } + ], + "title": "Write Stall per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Read Stage OPS per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 241 + }, + "id": 300, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod) (rate(greptime_mito_read_stage_elapsed_count{instance=~\"$datanode\", stage=\"total\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]", + "range": true, + "refId": "A" + } + ], + "title": "Read Stage OPS per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Read Stage P99 per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "[10.39.145.87:4000]-[mycluster-datanode-1]-[cache_miss_read]" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 241 + }, + "id": 301, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(instance, pod, le, stage) (rate(greptime_mito_read_stage_elapsed_bucket{instance=~\"$datanode\"}[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]", + "range": true, + "refId": "A" + } + ], + "title": "Read Stage P99 per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Write Stage P99 per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 249 + }, + "id": 302, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(instance, pod, le, stage) (rate(greptime_mito_write_stage_elapsed_bucket{instance=~\"$datanode\"}[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]", + "range": true, + "refId": "A" + } + ], + "title": "Write Stage P99 per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Compaction OPS per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 249 + }, + "id": 303, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count{instance=~\"$datanode\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "[{{ instance }}]-[{{pod}}]", + "range": true, + "refId": "A" + } + ], + "title": "Compaction OPS per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Compaction latency by stage", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 257 + }, + "id": 304, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(instance, pod, le, stage) (rate(greptime_mito_compaction_stage_elapsed_bucket{instance=~\"$datanode\"}[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-p99", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod, stage) (rate(greptime_mito_compaction_stage_elapsed_sum{instance=~\"$datanode\"}[$__rate_interval]))/sum by(instance, pod, stage) (rate(greptime_mito_compaction_stage_elapsed_count{instance=~\"$datanode\"}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-avg", + "range": true, + "refId": "B" + } + ], + "title": "Compaction Elapsed Time per Instance by Stage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Compaction P99 per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 257 + }, + "id": 305, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(instance, pod, le,stage) (rate(greptime_mito_compaction_total_elapsed_bucket{instance=~\"$datanode\"}[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-compaction", + "range": true, + "refId": "A" + } + ], + "title": "Compaction P99 per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Write-ahead logs write size as bytes. This chart includes stats of p95 and p99 size by instance, total WAL write rate.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 265 + }, + "id": 306, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum by(le,instance, pod) (rate(raft_engine_write_size_bucket[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-req-size-p95", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(le,instance,pod) (rate(raft_engine_write_size_bucket[$__rate_interval])))", + "hide": false, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-req-size-p99", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by (instance, pod)(rate(raft_engine_write_size_sum[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-throughput", + "range": true, + "refId": "C" + } + ], + "title": "WAL write size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Cached Bytes per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "[10.39.145.79:4000]-[mycluster-datanode-0]-[file]" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 265 + }, + "id": 307, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_mito_cache_bytes{instance=~\"$datanode\"}", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", + "range": true, + "refId": "A" + } + ], + "title": "Cached Bytes per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Ongoing compaction task count", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 273 + }, + "id": 308, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_mito_inflight_compaction_count", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]", + "range": true, + "refId": "A" + } + ], + "title": "Inflight Compaction", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Raft engine (local disk) log store sync latency, p99", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 273 + }, + "id": 310, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(le, type, node, instance, pod) (rate(raft_engine_sync_log_duration_seconds_bucket[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-p99", + "range": true, + "refId": "A" + } + ], + "title": "WAL sync duration seconds", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Write-ahead log operations latency at p99", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 281 + }, + "id": 311, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(le,logstore,optype,instance, pod) (rate(greptime_logstore_op_elapsed_bucket[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{logstore}}]-[{{optype}}]-p99", + "range": true, + "refId": "A" + } + ], + "title": "Log Store op duration seconds", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Ongoing flush task count", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 281 + }, + "id": 312, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_mito_inflight_flush_count", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]", + "range": true, + "refId": "A" + } + ], + "title": "Inflight Flush", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Compaction oinput output bytes", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 289 + }, + "id": 335, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod) (greptime_mito_compaction_input_bytes)", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-input", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod) (greptime_mito_compaction_output_bytes)", + "hide": false, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-output", + "range": true, + "refId": "B" + } + ], + "title": "Compaction Input/Output Bytes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Per-stage elapsed time for region worker to handle bulk insert region requests.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "[192.168.50.8:4000]-[]-[prepare_bulk_request]-AVG", + "[minipc-1:4000]-[]-[bulk_extend]-AVG", + "[minipc-1:4000]-[]-[prepare_bulk_request]-AVG", + "[minipc-1:4000]-[]-[process_bulk_req]-AVG", + "[minipc-1:4000]-[]-[write_bulk]-AVG" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 289 + }, + "id": 337, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum by(le,instance, stage, pod) (rate(greptime_region_worker_handle_write_bucket[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-P95", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, stage, pod) (rate(greptime_region_worker_handle_write_sum[$__rate_interval]))/sum by(instance, stage, pod) (rate(greptime_region_worker_handle_write_count[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-AVG", + "range": true, + "refId": "B" + } + ], + "title": "Region Worker Handle Bulk Insert Requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Compaction oinput output bytes", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 297 + }, + "id": 348, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod) (greptime_mito_memtable_active_series_count)", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-series", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod) (greptime_mito_memtable_field_builder_count)", + "hide": false, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-field_builders", + "range": true, + "refId": "B" + } + ], + "title": "Active Series and Field Builders Count", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Per-stage elapsed time for region worker to decode requests.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 297 + }, + "id": 338, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "histogram_quantile(0.95, sum by(le, instance, stage, pod) (rate(greptime_datanode_convert_region_request_bucket[$__rate_interval])))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-P95", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(le,instance, stage, pod) (rate(greptime_datanode_convert_region_request_sum[$__rate_interval]))/sum by(le,instance, stage, pod) (rate(greptime_datanode_convert_region_request_count[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-AVG", + "range": true, + "refId": "B" + } + ], + "title": "Region Worker Convert Requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "The local cache miss of the datanode.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 305 + }, + "id": 349, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by (instance,pod, type) (rate(greptime_mito_cache_miss{instance=~\"$datanode\"}[$__rate_interval]))", + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", + "range": true, + "refId": "A" + } + ], + "title": "Cache Miss", + "type": "timeseries" + } + ], "title": "Mito Engine", "type": "row" }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Request QPS per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ops" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 15 - }, - "id": 294, - "options": { - "legend": { - "calcs": [ - "mean" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod, type) (rate(greptime_mito_handle_request_elapsed_count{instance=~\"$datanode\"}[$__rate_interval]))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", - "range": true, - "refId": "A" - } - ], - "title": "Request OPS per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Request P99 per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 15 - }, - "id": 295, - "options": { - "legend": { - "calcs": [ - "mean" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(instance, pod, le, type) (rate(greptime_mito_handle_request_elapsed_bucket{instance=~\"$datanode\"}[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", - "range": true, - "refId": "A" - } - ], - "title": "Request P99 per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Write Buffer per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "decbytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 23 - }, - "id": 296, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_mito_write_buffer_bytes{instance=~\"$datanode\"}", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]", - "range": true, - "refId": "A" - } - ], - "title": "Write Buffer per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Ingestion size by row counts.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "rowsps" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 23 - }, - "id": 297, - "options": { - "legend": { - "calcs": [ - "mean" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by (instance, pod) (rate(greptime_mito_write_rows_total{instance=~\"$datanode\"}[$__rate_interval]))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]", - "range": true, - "refId": "A" - } - ], - "title": "Write Rows per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Flush QPS per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ops" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 31 - }, - "id": 298, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod, reason) (rate(greptime_mito_flush_requests_total{instance=~\"$datanode\"}[$__rate_interval]))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{reason}}]", - "range": true, - "refId": "A" - } - ], - "title": "Flush OPS per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Write Stall per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 31 - }, - "id": 299, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod) (greptime_mito_write_stall_total{instance=~\"$datanode\"})", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]", - "range": true, - "refId": "A" - } - ], - "title": "Write Stall per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Read Stage OPS per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ops" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 39 - }, - "id": 300, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod) (rate(greptime_mito_read_stage_elapsed_count{instance=~\"$datanode\", stage=\"total\"}[$__rate_interval]))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]", - "range": true, - "refId": "A" - } - ], - "title": "Read Stage OPS per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Read Stage P99 per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [ - { - "__systemRef": "hideSeriesFrom", - "matcher": { - "id": "byNames", - "options": { - "mode": "exclude", - "names": [ - "[10.39.145.87:4000]-[mycluster-datanode-1]-[cache_miss_read]" - ], - "prefix": "All except:", - "readOnly": true - } - }, - "properties": [ - { - "id": "custom.hideFrom", - "value": { - "legend": false, - "tooltip": false, - "viz": true - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 39 - }, - "id": 301, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(instance, pod, le, stage) (rate(greptime_mito_read_stage_elapsed_bucket{instance=~\"$datanode\"}[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]", - "range": true, - "refId": "A" - } - ], - "title": "Read Stage P99 per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Write Stage P99 per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 47 - }, - "id": 302, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true, - "sortBy": "Last *", - "sortDesc": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(instance, pod, le, stage) (rate(greptime_mito_write_stage_elapsed_bucket{instance=~\"$datanode\"}[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]", - "range": true, - "refId": "A" - } - ], - "title": "Write Stage P99 per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Compaction OPS per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ops" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 47 - }, - "id": 303, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count{instance=~\"$datanode\"}[$__rate_interval]))", - "instant": false, - "legendFormat": "[{{ instance }}]-[{{pod}}]", - "range": true, - "refId": "A" - } - ], - "title": "Compaction OPS per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Compaction latency by stage", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 55 - }, - "id": 304, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(instance, pod, le, stage) (rate(greptime_mito_compaction_stage_elapsed_bucket{instance=~\"$datanode\"}[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-p99", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod, stage) (rate(greptime_mito_compaction_stage_elapsed_sum{instance=~\"$datanode\"}[$__rate_interval]))/sum by(instance, pod, stage) (rate(greptime_mito_compaction_stage_elapsed_count{instance=~\"$datanode\"}[$__rate_interval]))", - "hide": false, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-avg", - "range": true, - "refId": "B" - } - ], - "title": "Compaction Elapsed Time per Instance by Stage", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Compaction P99 per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 55 - }, - "id": 305, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(instance, pod, le,stage) (rate(greptime_mito_compaction_total_elapsed_bucket{instance=~\"$datanode\"}[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-compaction", - "range": true, - "refId": "A" - } - ], - "title": "Compaction P99 per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Write-ahead logs write size as bytes. This chart includes stats of p95 and p99 size by instance, total WAL write rate.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 63 - }, - "id": 306, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.95, sum by(le,instance, pod) (rate(raft_engine_write_size_bucket[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-req-size-p95", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(le,instance,pod) (rate(raft_engine_write_size_bucket[$__rate_interval])))", - "hide": false, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-req-size-p99", - "range": true, - "refId": "B" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by (instance, pod)(rate(raft_engine_write_size_sum[$__rate_interval]))", - "hide": false, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-throughput", - "range": true, - "refId": "C" - } - ], - "title": "WAL write size", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Cached Bytes per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "decbytes" - }, - "overrides": [ - { - "__systemRef": "hideSeriesFrom", - "matcher": { - "id": "byNames", - "options": { - "mode": "exclude", - "names": [ - "[10.39.145.79:4000]-[mycluster-datanode-0]-[file]" - ], - "prefix": "All except:", - "readOnly": true - } - }, - "properties": [ - { - "id": "custom.hideFrom", - "value": { - "legend": false, - "tooltip": false, - "viz": true - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 63 - }, - "id": 307, - "options": { - "legend": { - "calcs": [], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_mito_cache_bytes{instance=~\"$datanode\"}", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", - "range": true, - "refId": "A" - } - ], - "title": "Cached Bytes per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Ongoing compaction task count", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 71 - }, - "id": 308, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_mito_inflight_compaction_count", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]", - "range": true, - "refId": "A" - } - ], - "title": "Inflight Compaction", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Raft engine (local disk) log store sync latency, p99", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 71 - }, - "id": 310, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(le, type, node, instance, pod) (rate(raft_engine_sync_log_duration_seconds_bucket[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-p99", - "range": true, - "refId": "A" - } - ], - "title": "WAL sync duration seconds", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Write-ahead log operations latency at p99", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 79 - }, - "id": 311, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(le,logstore,optype,instance, pod) (rate(greptime_logstore_op_elapsed_bucket[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{logstore}}]-[{{optype}}]-p99", - "range": true, - "refId": "A" - } - ], - "title": "Log Store op duration seconds", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Ongoing flush task count", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 79 - }, - "id": 312, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_mito_inflight_flush_count", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]", - "range": true, - "refId": "A" - } - ], - "title": "Inflight Flush", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Compaction oinput output bytes", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 87 - }, - "id": 335, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod) (greptime_mito_compaction_input_bytes)", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-input", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod) (greptime_mito_compaction_output_bytes)", - "hide": false, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-output", - "range": true, - "refId": "B" - } - ], - "title": "Compaction Input/Output Bytes", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Per-stage elapsed time for region worker to handle bulk insert region requests.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [ - { - "__systemRef": "hideSeriesFrom", - "matcher": { - "id": "byNames", - "options": { - "mode": "exclude", - "names": [ - "[192.168.50.8:4000]-[]-[prepare_bulk_request]-AVG", - "[minipc-1:4000]-[]-[bulk_extend]-AVG", - "[minipc-1:4000]-[]-[prepare_bulk_request]-AVG", - "[minipc-1:4000]-[]-[process_bulk_req]-AVG", - "[minipc-1:4000]-[]-[write_bulk]-AVG" - ], - "prefix": "All except:", - "readOnly": true - } - }, - "properties": [ - { - "id": "custom.hideFrom", - "value": { - "legend": false, - "tooltip": false, - "viz": true - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 87 - }, - "id": 337, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.95, sum by(le,instance, stage, pod) (rate(greptime_region_worker_handle_write_bucket[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-P95", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, stage, pod) (rate(greptime_region_worker_handle_write_sum[$__rate_interval]))/sum by(instance, stage, pod) (rate(greptime_region_worker_handle_write_count[$__rate_interval]))", - "hide": false, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-AVG", - "range": true, - "refId": "B" - } - ], - "title": "Region Worker Handle Bulk Insert Requests", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Compaction oinput output bytes", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 95 - }, - "id": 348, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod) (greptime_mito_memtable_active_series_count)", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-series", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod) (greptime_mito_memtable_field_builder_count)", - "hide": false, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-field_builders", - "range": true, - "refId": "B" - } - ], - "title": "Active Series and Field Builders Count", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Per-stage elapsed time for region worker to decode requests.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 95 - }, - "id": 338, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "builder", - "expr": "histogram_quantile(0.95, sum by(le, instance, stage, pod) (rate(greptime_datanode_convert_region_request_bucket[$__rate_interval])))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-P95", - "range": true, - "refId": "A", - "useBackend": false - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(le,instance, stage, pod) (rate(greptime_datanode_convert_region_request_sum[$__rate_interval]))/sum by(le,instance, stage, pod) (rate(greptime_datanode_convert_region_request_count[$__rate_interval]))", - "hide": false, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-AVG", - "range": true, - "refId": "B" - } - ], - "title": "Region Worker Convert Requests", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "The local cache miss of the datanode.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 103 - }, - "id": 349, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by (instance,pod, type) (rate(greptime_mito_cache_miss{instance=~\"$datanode\"}[$__rate_interval]))", - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", - "range": true, - "refId": "A" - } - ], - "title": "Cache Miss", - "type": "timeseries" - }, { "collapsed": true, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 111 + "y": 27 }, "id": 313, "panels": [ @@ -6098,7 +6099,7 @@ "h": 10, "w": 24, "x": 0, - "y": 232 + "y": 459 }, "id": 314, "options": { @@ -6198,7 +6199,7 @@ "h": 7, "w": 12, "x": 0, - "y": 242 + "y": 469 }, "id": 315, "options": { @@ -6298,7 +6299,7 @@ "h": 7, "w": 12, "x": 12, - "y": 242 + "y": 469 }, "id": 316, "options": { @@ -6398,7 +6399,7 @@ "h": 7, "w": 12, "x": 0, - "y": 249 + "y": 476 }, "id": 317, "options": { @@ -6498,7 +6499,7 @@ "h": 7, "w": 12, "x": 12, - "y": 249 + "y": 476 }, "id": 318, "options": { @@ -6598,7 +6599,7 @@ "h": 7, "w": 12, "x": 0, - "y": 256 + "y": 483 }, "id": 319, "options": { @@ -6699,7 +6700,7 @@ "h": 7, "w": 12, "x": 12, - "y": 256 + "y": 483 }, "id": 320, "options": { @@ -6800,7 +6801,7 @@ "h": 7, "w": 12, "x": 0, - "y": 263 + "y": 490 }, "id": 321, "options": { @@ -6900,7 +6901,7 @@ "h": 7, "w": 12, "x": 12, - "y": 263 + "y": 490 }, "id": 322, "options": { @@ -7000,7 +7001,7 @@ "h": 7, "w": 12, "x": 0, - "y": 270 + "y": 497 }, "id": 323, "options": { @@ -7103,7 +7104,7 @@ "h": 7, "w": 12, "x": 12, - "y": 270 + "y": 497 }, "id": 334, "options": { @@ -7144,1827 +7145,1829 @@ "type": "row" }, { - "collapsed": false, + "collapsed": true, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 112 + "y": 28 }, "id": 351, - "panels": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Triggered region flush total", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 195 + }, + "id": 350, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "meta_triggered_region_flush_total", + "instant": false, + "legendFormat": "{{pod}}-{{topic_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Triggered region flush total", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Triggered region checkpoint total", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 195 + }, + "id": 352, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "meta_triggered_region_checkpoint_total", + "instant": false, + "legendFormat": "{{pod}}-{{topic_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Triggered region checkpoint total", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Topic estimated max replay size", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 227 + }, + "id": 353, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "meta_topic_estimated_replay_size", + "instant": false, + "legendFormat": "{{pod}}-{{topic_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Topic estimated replay size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Kafka logstore's bytes traffic", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 227 + }, + "id": 354, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "rate(greptime_logstore_kafka_client_bytes_total[$__rate_interval])", + "instant": false, + "legendFormat": "{{pod}}-{{logstore}}", + "range": true, + "refId": "A" + } + ], + "title": "Kafka logstore's bytes traffic", + "type": "timeseries" + } + ], "title": "Remote WAL", "type": "row" }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Triggered region flush total", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 113 - }, - "id": 350, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "meta_triggered_region_flush_total", - "instant": false, - "legendFormat": "{{pod}}-{{topic_name}}", - "range": true, - "refId": "A" - } - ], - "title": "Triggered region flush total", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Triggered region checkpoint total", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 113 - }, - "id": 352, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "meta_triggered_region_checkpoint_total", - "instant": false, - "legendFormat": "{{pod}}-{{topic_name}}", - "range": true, - "refId": "A" - } - ], - "title": "Triggered region checkpoint total", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Topic estimated max replay size", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 121 - }, - "id": 353, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "meta_topic_estimated_replay_size", - "instant": false, - "legendFormat": "{{pod}}-{{topic_name}}", - "range": true, - "refId": "A" - } - ], - "title": "Topic estimated replay size", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Kafka logstore's bytes traffic", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 121 - }, - "id": 354, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "rate(greptime_logstore_kafka_client_bytes_total[$__rate_interval])", - "instant": false, - "legendFormat": "{{pod}}-{{logstore}}", - "range": true, - "refId": "A" - } - ], - "title": "Kafka logstore's bytes traffic", - "type": "timeseries" - }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 129 - }, - "id": 324, - "panels": [], - "title": "Metasrv", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Counter of region migration by source and destination", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "axisPlacement": "auto", - "fillOpacity": 70, - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineWidth": 1 - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 130 - }, - "id": 325, - "options": { - "colWidth": 0.9, - "legend": { - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "rowHeight": 0.9, - "showValue": "auto", - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_meta_region_migration_stat{datanode_type=\"src\"}", - "instant": false, - "legendFormat": "from-datanode-{{datanode_id}}", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_meta_region_migration_stat{datanode_type=\"desc\"}", - "hide": false, - "instant": false, - "legendFormat": "to-datanode-{{datanode_id}}", - "range": true, - "refId": "B" - } - ], - "title": "Region migration datanode", - "type": "status-history" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Counter of region migration error", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 130 - }, - "id": 326, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_meta_region_migration_error", - "instant": false, - "legendFormat": "{{pod}}-{{state}}-{{error_type}}", - "range": true, - "refId": "A" - } - ], - "title": "Region migration error", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "binBps" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 138 - }, - "id": 327, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_datanode_load", - "instant": false, - "legendFormat": "Datanode-{{datanode_id}}-writeload", - "range": true, - "refId": "A" - } - ], - "title": "Datanode load", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Displays the rate of SQL executions processed by the Meta service using the RDS backend.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 138 - }, - "id": 339, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "rate(greptime_meta_rds_pg_sql_execute_elapsed_ms_count[$__rate_interval])", - "instant": false, - "legendFormat": "{{pod}} {{op}} {{type}} {{result}} ", - "range": true, - "refId": "A" - } - ], - "title": "Rate of SQL Executions (RDS)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Measures the response time of SQL executions via the RDS backend. ", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ms" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 146 - }, - "id": 340, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.90, sum by(pod, op, type, result, le) (rate(greptime_meta_rds_pg_sql_execute_elapsed_ms_bucket[$__rate_interval])))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "{{pod}} {{op}} {{type}} {{result}} p90", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "SQL Execution Latency (RDS)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Shows latency of Meta handlers by pod and handler name, useful for monitoring handler performance and detecting latency spikes.\n", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 146 - }, - "id": 341, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.90, sum by(pod, le, name) (\n rate(greptime_meta_handler_execute_bucket[$__rate_interval])\n))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "{{pod}} {{name}} p90", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Handler Execution Latency", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Shows p90 heartbeat message sizes, helping track network usage and identify anomalies in heartbeat payload.\n", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 154 - }, - "id": 342, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.9, sum by(pod, le) (greptime_meta_heartbeat_stat_memory_size_bucket))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "{{pod}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Heartbeat Packet Size", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 154 - }, - "id": 345, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "rate(greptime_meta_heartbeat_rate[$__rate_interval])", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "{{pod}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Meta Heartbeat Receive Rate", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 162 - }, - "id": 343, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(pod, le, op, target) (greptime_meta_kv_request_elapsed_bucket))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "{{pod}}-{{op}} p99", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Meta KV Ops Latency", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 162 - }, - "id": 346, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "rate(greptime_meta_kv_request_elapsed_count[$__rate_interval])", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "{{pod}}-{{op}} p99", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Rate of meta KV Ops", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 170 - }, - "id": 347, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_tables_bucket))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "CreateLogicalTables-{{step}} p90", - "range": true, - "refId": "A", - "useBackend": false - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_table))", - "fullMetaSearch": false, - "hide": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "CreateTable-{{step}} p90", - "range": true, - "refId": "B", - "useBackend": false - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_view))", - "fullMetaSearch": false, - "hide": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "CreateView-{{step}} p90", - "range": true, - "refId": "C", - "useBackend": false - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_flow))", - "fullMetaSearch": false, - "hide": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "CreateFlow-{{step}} p90", - "range": true, - "refId": "D", - "useBackend": false - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_drop_table))", - "fullMetaSearch": false, - "hide": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "DropTable-{{step}} p90", - "range": true, - "refId": "E", - "useBackend": false - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_alter_table))", - "fullMetaSearch": false, - "hide": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "AlterTable-{{step}} p90", - "range": true, - "refId": "F", - "useBackend": false - } - ], - "title": "DDL Latency", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Reconciliation stats", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 170 - }, - "id": 355, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_meta_reconciliation_stats", - "hide": false, - "instant": false, - "legendFormat": "{{pod}}-{{table_type}}-{{type}}", - "range": true, - "refId": "A" - } - ], - "title": "Reconciliation stats", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Elapsed of Reconciliation steps", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 178 - }, - "id": 356, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.9, greptime_meta_reconciliation_procedure_bucket)", - "hide": false, - "instant": false, - "legendFormat": "{{procedure_name}}-{{step}}-P90", - "range": true, - "refId": "A" - } - ], - "title": "Reconciliation steps", - "type": "timeseries" - }, { "collapsed": true, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 186 + "y": 29 + }, + "id": 324, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Counter of region migration by source and destination", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 196 + }, + "id": 325, + "options": { + "colWidth": 0.9, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_meta_region_migration_stat{datanode_type=\"src\"}", + "instant": false, + "legendFormat": "from-datanode-{{datanode_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_meta_region_migration_stat{datanode_type=\"desc\"}", + "hide": false, + "instant": false, + "legendFormat": "to-datanode-{{datanode_id}}", + "range": true, + "refId": "B" + } + ], + "title": "Region migration datanode", + "type": "status-history" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Counter of region migration error", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 196 + }, + "id": 326, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_meta_region_migration_error", + "instant": false, + "legendFormat": "{{pod}}-{{state}}-{{error_type}}", + "range": true, + "refId": "A" + } + ], + "title": "Region migration error", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 228 + }, + "id": 327, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_datanode_load", + "instant": false, + "legendFormat": "Datanode-{{datanode_id}}-writeload", + "range": true, + "refId": "A" + } + ], + "title": "Datanode load", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Displays the rate of SQL executions processed by the Meta service using the RDS backend.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 228 + }, + "id": 339, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "rate(greptime_meta_rds_pg_sql_execute_elapsed_ms_count[$__rate_interval])", + "instant": false, + "legendFormat": "{{pod}} {{op}} {{type}} {{result}} ", + "range": true, + "refId": "A" + } + ], + "title": "Rate of SQL Executions (RDS)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Measures the response time of SQL executions via the RDS backend. ", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 236 + }, + "id": 340, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.90, sum by(pod, op, type, result, le) (rate(greptime_meta_rds_pg_sql_execute_elapsed_ms_bucket[$__rate_interval])))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{pod}} {{op}} {{type}} {{result}} p90", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "SQL Execution Latency (RDS)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Shows latency of Meta handlers by pod and handler name, useful for monitoring handler performance and detecting latency spikes.\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 236 + }, + "id": 341, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.90, sum by(pod, le, name) (\n rate(greptime_meta_handler_execute_bucket[$__rate_interval])\n))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{pod}} {{name}} p90", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler Execution Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Shows p90 heartbeat message sizes, helping track network usage and identify anomalies in heartbeat payload.\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 244 + }, + "id": 342, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum by(pod, le) (greptime_meta_heartbeat_stat_memory_size_bucket))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{pod}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Heartbeat Packet Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 244 + }, + "id": 345, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "rate(greptime_meta_heartbeat_rate[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{pod}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Meta Heartbeat Receive Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 252 + }, + "id": 343, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(pod, le, op, target) (greptime_meta_kv_request_elapsed_bucket))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{pod}}-{{op}} p99", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Meta KV Ops Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 252 + }, + "id": 346, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "rate(greptime_meta_kv_request_elapsed_count[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{pod}}-{{op}} p99", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Rate of meta KV Ops", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 260 + }, + "id": 347, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_tables_bucket))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "CreateLogicalTables-{{step}} p90", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_table))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "CreateTable-{{step}} p90", + "range": true, + "refId": "B", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_view))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "CreateView-{{step}} p90", + "range": true, + "refId": "C", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_flow))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "CreateFlow-{{step}} p90", + "range": true, + "refId": "D", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_drop_table))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "DropTable-{{step}} p90", + "range": true, + "refId": "E", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_alter_table))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "AlterTable-{{step}} p90", + "range": true, + "refId": "F", + "useBackend": false + } + ], + "title": "DDL Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Reconciliation stats", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 260 + }, + "id": 355, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_meta_reconciliation_stats", + "hide": false, + "instant": false, + "legendFormat": "{{pod}}-{{table_type}}-{{type}}", + "range": true, + "refId": "A" + } + ], + "title": "Reconciliation stats", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Elapsed of Reconciliation steps", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 268 + }, + "id": 356, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.9, greptime_meta_reconciliation_procedure_bucket)", + "hide": false, + "instant": false, + "legendFormat": "{{procedure_name}}-{{step}}-P90", + "range": true, + "refId": "A" + } + ], + "title": "Reconciliation steps", + "type": "timeseries" + } + ], + "title": "Metasrv", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 30 }, "id": 328, "panels": [ @@ -9031,7 +9034,7 @@ "h": 8, "w": 12, "x": 0, - "y": 3055 + "y": 3282 }, "id": 329, "options": { @@ -9126,7 +9129,7 @@ "h": 8, "w": 12, "x": 12, - "y": 3055 + "y": 3282 }, "id": 330, "options": { @@ -9234,7 +9237,7 @@ "h": 8, "w": 9, "x": 0, - "y": 3063 + "y": 3290 }, "id": 331, "options": { @@ -9342,7 +9345,7 @@ "h": 8, "w": 9, "x": 9, - "y": 3063 + "y": 3290 }, "id": 332, "options": { @@ -9437,7 +9440,7 @@ "h": 8, "w": 6, "x": 18, - "y": 3063 + "y": 3290 }, "id": 333, "options": { @@ -9474,12 +9477,12 @@ "type": "row" }, { - "collapsed": true, + "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 187 + "y": 31 }, "id": 357, "panels": [], @@ -9550,7 +9553,7 @@ "h": 8, "w": 24, "x": 0, - "y": 188 + "y": 32 }, "id": 358, "options": { @@ -9648,7 +9651,7 @@ "h": 8, "w": 12, "x": 0, - "y": 196 + "y": 40 }, "id": 359, "options": { @@ -9761,7 +9764,7 @@ "h": 8, "w": 12, "x": 12, - "y": 196 + "y": 40 }, "id": 360, "options": { @@ -9861,7 +9864,7 @@ "h": 8, "w": 12, "x": 0, - "y": 204 + "y": 48 }, "id": 361, "options": { @@ -9974,7 +9977,7 @@ "h": 8, "w": 12, "x": 12, - "y": 204 + "y": 48 }, "id": 364, "options": { @@ -10074,7 +10077,7 @@ "h": 8, "w": 12, "x": 0, - "y": 212 + "y": 56 }, "id": 363, "options": { @@ -10187,7 +10190,7 @@ "h": 8, "w": 12, "x": 12, - "y": 212 + "y": 56 }, "id": 362, "options": { @@ -10221,6 +10224,477 @@ ], "title": "Save Alert Failure Rate", "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 64 + }, + "id": 366, + "panels": [], + "title": "Hotspot", + "type": "row" + }, + { + "datasource": { + "type": "mysql", + "uid": "${information_schema}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto", + "wrapText": false + }, + "inspect": false + }, + "fieldMinMax": false, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "disk_size" + }, + "properties": [ + { + "id": "unit", + "value": "bytes" + } + ] + } + ] + }, + "gridPos": { + "h": 18, + "w": 24, + "x": 0, + "y": 65 + }, + "id": 365, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": true, + "fields": [ + "region_rows", + "disk_size" + ], + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "table_schema" + } + ] + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "dataset": "information_schema", + "datasource": { + "type": "mysql", + "uid": "${information_schema}" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH table_stats AS (\n SELECT\n table_id,\n COUNT(*) AS region_count,\n SUM(disk_size) AS total_disk_size,\n SUM(region_rows) as total_region_rows\n FROM information_schema.region_statistics\n WHERE region_role = 'Leader'\n GROUP BY table_id\n HAVING COUNT(*) > 1\n)\n\nSELECT\n t.table_schema,\n t.table_name,\n\n r.region_id,\n t.table_id,\n r.region_number,\n\n p.partition_description,\n\n\n ROUND(\n r.disk_size * 100.0\n / NULLIF(ts.total_disk_size, 0),\n 2\n ) AS disk_size_share_percent,\n\n r.disk_size,\n \n ROUND(\n r.region_rows * 100.0\n / NULLIF(ts.total_region_rows, 0),\n 2\n ) AS region_rows_share_percent,\n r.region_rows\n\nFROM information_schema.region_statistics r\n\nJOIN table_stats ts\n ON r.table_id = ts.table_id\n\nJOIN information_schema.tables t\n ON r.table_id = t.table_id\n\nLEFT JOIN information_schema.partitions p\n ON p.table_schema = t.table_schema\n AND p.table_name = t.table_name\n AND p.greptime_partition_id = r.region_id\n\nWHERE r.region_role = 'Leader'\n\nORDER BY region_rows_share_percent DESC\nLIMIT 100;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Hotspot Regions", + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Write load of each datanode over time.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "fieldMinMax": false, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "total_region_rows", + "total_written_bytes_since_open", + "total_disk_size", + "total_sst_size", + "total_sst_num", + "total_memtable_size", + "total_manifest_size", + "total_index_size", + "cluster_disk_ratio_percent", + "cluster_sst_ratio_percent", + "cluster_rows_ratio_percent", + "max_region_disk_size", + "max_region_rows", + "leader_region_count", + "disk_ratio_percent", + "data_size" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 0, + "y": 83 + }, + "id": 368, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "greptime_datanode_history_load", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "datanode-{{datanode_id}}({{instance}})", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Datanode Load(Write)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Distribution of write load across datanodes.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "fieldMinMax": false, + "mappings": [], + "min": 0, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 12, + "y": 83 + }, + "id": 369, + "options": { + "displayLabels": [ + "percent" + ], + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "donut", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "greptime_datanode_history_load", + "format": "time_series", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "datanode-{{datanode_id}}({{instance}})", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Datanode Load(Write) Distribution", + "type": "piechart" + }, + { + "datasource": { + "type": "mysql", + "uid": "${information_schema}" + }, + "description": "Distribution of leader regions and data size across datanodes.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "fieldMinMax": false, + "mappings": [], + "unit": "bytes" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "total_region_rows", + "total_written_bytes_since_open", + "total_disk_size", + "total_sst_size", + "total_sst_num", + "total_memtable_size", + "total_manifest_size", + "total_index_size", + "cluster_disk_ratio_percent", + "cluster_sst_ratio_percent", + "cluster_rows_ratio_percent", + "max_region_disk_size", + "max_region_rows", + "leader_region_count", + "disk_ratio_percent", + "data_size" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 0, + "y": 94 + }, + "id": 367, + "options": { + "displayLabels": [ + "value" + ], + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true, + "values": [ + "percent" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/^data_size$/", + "values": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "dataset": "information_schema", + "datasource": { + "type": "mysql", + "uid": "${information_schema}" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH leader_regions AS (\n SELECT\n CONCAT(\n 'datanode-',\n p.peer_id,\n ' (',\n p.peer_addr,\n ')'\n ) AS datanode,\n r.disk_size\n FROM information_schema.region_statistics r\n JOIN information_schema.region_peers p\n ON r.region_id = p.region_id\n WHERE r.region_role = 'Leader'\n AND p.is_leader = 'Yes'\n)\n\nSELECT\n datanode,\n COUNT(*) AS leader_region_count,\n SUM(disk_size) AS data_size\nFROM leader_regions\nGROUP BY datanode\nORDER BY data_size DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Datanode Data Distribution", + "type": "piechart" } ], "preload": false, @@ -10354,12 +10828,12 @@ ] }, "time": { - "from": "now-1h", + "from": "now-2h", "to": "now" }, "timepicker": {}, "timezone": "", - "title": "GreptimeDB", - "uid": "dejf3k5e7g2kgb", - "version": 15 -} + "title": "GreptimeDB Copy", + "uid": "dflfbxbwvvchsa", + "version": 37 +} \ No newline at end of file diff --git a/grafana/dashboards/metrics/cluster/dashboard.md b/grafana/dashboards/metrics/cluster/dashboard.md index c0bc49770b..c398ea7f90 100644 --- a/grafana/dashboards/metrics/cluster/dashboard.md +++ b/grafana/dashboards/metrics/cluster/dashboard.md @@ -142,3 +142,87 @@ rate(greptime_trigger_save_alert_record_elapsed_bucket[$__rate_interval]) )` | `timeseries` | Elapsed time to persist trigger alert records. | `prometheus` | `s` | `[{{instance}}]-[{{pod}}]-[{{storage_type}}]-p99` | | Save Alert Failure Rate | `rate(greptime_trigger_save_alert_record_failure_count[$__rate_interval])` | `timeseries` | Rate of failures when persisting trigger alert records. | `prometheus` | `none` | `__auto` | +# Hotspot +| Title | Query | Type | Description | Datasource | Unit | Legend Format | +| --- | --- | --- | --- | --- | --- | --- | +| Hotspot Regions | `WITH table_stats AS ( + SELECT + table_id, + COUNT(*) AS region_count, + SUM(disk_size) AS total_disk_size, + SUM(region_rows) as total_region_rows + FROM information_schema.region_statistics + WHERE region_role = 'Leader' + GROUP BY table_id + HAVING COUNT(*) > 1 +) + +SELECT + t.table_schema, + t.table_name, + + r.region_id, + t.table_id, + r.region_number, + + p.partition_description, + + + ROUND( + r.disk_size * 100.0 + / NULLIF(ts.total_disk_size, 0), + 2 + ) AS disk_size_share_percent, + + r.disk_size, + + ROUND( + r.region_rows * 100.0 + / NULLIF(ts.total_region_rows, 0), + 2 + ) AS region_rows_share_percent, + r.region_rows + +FROM information_schema.region_statistics r + +JOIN table_stats ts + ON r.table_id = ts.table_id + +JOIN information_schema.tables t + ON r.table_id = t.table_id + +LEFT JOIN information_schema.partitions p + ON p.table_schema = t.table_schema + AND p.table_name = t.table_name + AND p.greptime_partition_id = r.region_id + +WHERE r.region_role = 'Leader' + +ORDER BY region_rows_share_percent DESC +LIMIT 100;` | `table` | | `mysql` | -- | -- | +| Datanode Load(Write) | `greptime_datanode_history_load` | `timeseries` | Write load of each datanode over time. | `prometheus` | `binBps` | `datanode-{{datanode_id}}({{instance}})` | +| Datanode Load(Write) Distribution | `greptime_datanode_history_load` | `piechart` | Distribution of write load across datanodes. | `prometheus` | `binBps` | `datanode-{{datanode_id}}({{instance}})` | +| Datanode Data Distribution | `WITH leader_regions AS ( + SELECT + CONCAT( + 'datanode-', + p.peer_id, + ' (', + p.peer_addr, + ')' + ) AS datanode, + r.disk_size + FROM information_schema.region_statistics r + JOIN information_schema.region_peers p + ON r.region_id = p.region_id + WHERE r.region_role = 'Leader' + AND p.is_leader = 'Yes' +) + +SELECT + datanode, + COUNT(*) AS leader_region_count, + SUM(disk_size) AS data_size +FROM leader_regions +GROUP BY datanode +ORDER BY data_size DESC;` | `piechart` | Distribution of leader regions and data size across datanodes. | `mysql` | `bytes` | -- | diff --git a/grafana/dashboards/metrics/cluster/dashboard.yaml b/grafana/dashboards/metrics/cluster/dashboard.yaml index 934cf58c0c..2617016083 100644 --- a/grafana/dashboards/metrics/cluster/dashboard.yaml +++ b/grafana/dashboards/metrics/cluster/dashboard.yaml @@ -1153,3 +1153,65 @@ groups: type: prometheus uid: ${metrics} legendFormat: __auto + - title: Hotspot + panels: + - title: Hotspot Regions + type: table + queries: + - expr: "WITH table_stats AS (\n SELECT\n table_id,\n COUNT(*) AS region_count,\n SUM(disk_size) AS total_disk_size,\n SUM(region_rows) as total_region_rows\n FROM information_schema.region_statistics\n WHERE region_role = 'Leader'\n GROUP BY table_id\n HAVING COUNT(*) > 1\n)\n\nSELECT\n t.table_schema,\n t.table_name,\n\n r.region_id,\n t.table_id,\n r.region_number,\n\n p.partition_description,\n\n\n ROUND(\n r.disk_size * 100.0\n / NULLIF(ts.total_disk_size, 0),\n 2\n ) AS disk_size_share_percent,\n\n r.disk_size,\n \n ROUND(\n r.region_rows * 100.0\n / NULLIF(ts.total_region_rows, 0),\n 2\n ) AS region_rows_share_percent,\n r.region_rows\n\nFROM information_schema.region_statistics r\n\nJOIN table_stats ts\n ON r.table_id = ts.table_id\n\nJOIN information_schema.tables t\n ON r.table_id = t.table_id\n\nLEFT JOIN information_schema.partitions p\n ON p.table_schema = t.table_schema\n AND p.table_name = t.table_name\n AND p.greptime_partition_id = r.region_id\n\nWHERE r.region_role = 'Leader'\n\nORDER BY region_rows_share_percent DESC\nLIMIT 100;" + datasource: + type: mysql + uid: ${information_schema} + - title: Datanode Load(Write) + type: timeseries + description: Write load of each datanode over time. + unit: binBps + queries: + - expr: greptime_datanode_history_load + datasource: + type: prometheus + uid: ${metrics} + legendFormat: datanode-{{datanode_id}}({{instance}}) + - title: Datanode Load(Write) Distribution + type: piechart + description: Distribution of write load across datanodes. + unit: binBps + queries: + - expr: greptime_datanode_history_load + datasource: + type: prometheus + uid: ${metrics} + legendFormat: datanode-{{datanode_id}}({{instance}}) + - title: Datanode Data Distribution + type: piechart + description: Distribution of leader regions and data size across datanodes. + unit: bytes + queries: + - expr: |- + WITH leader_regions AS ( + SELECT + CONCAT( + 'datanode-', + p.peer_id, + ' (', + p.peer_addr, + ')' + ) AS datanode, + r.disk_size + FROM information_schema.region_statistics r + JOIN information_schema.region_peers p + ON r.region_id = p.region_id + WHERE r.region_role = 'Leader' + AND p.is_leader = 'Yes' + ) + + SELECT + datanode, + COUNT(*) AS leader_region_count, + SUM(disk_size) AS data_size + FROM leader_regions + GROUP BY datanode + ORDER BY data_size DESC; + datasource: + type: mysql + uid: ${information_schema} diff --git a/grafana/dashboards/metrics/standalone/dashboard.json b/grafana/dashboards/metrics/standalone/dashboard.json index 28f38f9dd7..229bdd30ea 100644 --- a/grafana/dashboards/metrics/standalone/dashboard.json +++ b/grafana/dashboards/metrics/standalone/dashboard.json @@ -25,7 +25,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, - "id": 34, + "id": 33, "links": [], "panels": [ { @@ -927,7 +927,7 @@ "type": "stat" }, { - "collapsed": true, + "collapsed": false, "gridPos": { "h": 1, "w": 24, @@ -935,226 +935,226 @@ "y": 9 }, "id": 275, - "panels": [ + "panels": [], + "title": "Ingestion", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Total ingestion rate.\n\nHere we listed 3 primary protocols:\n\n- Prometheus remote write\n- Greptime's gRPC API (when using our ingest SDK)\n- Log ingestion http API\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "rowsps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 10 + }, + "id": 193, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ { "datasource": { "type": "prometheus", "uid": "${metrics}" }, - "description": "Total ingestion rate.\n\nHere we listed 3 primary protocols:\n\n- Prometheus remote write\n- Greptime's gRPC API (when using our ingest SDK)\n- Log ingestion http API\n", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "rowsps" - }, - "overrides": [] + "editorMode": "code", + "expr": "sum(rate(greptime_table_operator_ingest_rows{}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "ingestion", + "range": true, + "refId": "C" + } + ], + "title": "Total Ingestion Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Total ingestion rate.\n\nHere we listed 3 primary protocols:\n\n- Prometheus remote write\n- Greptime's gRPC API (when using our ingest SDK)\n- Log ingestion http API\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" }, - "gridPos": { - "h": 6, - "w": 24, - "x": 0, - "y": 18 - }, - "id": 193, - "options": { - "legend": { - "calcs": [ - "mean" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" } }, - "pluginVersion": "12.0.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" }, - "editorMode": "code", - "expr": "sum(rate(greptime_table_operator_ingest_rows{}[$__rate_interval]))", - "hide": false, - "instant": false, - "legendFormat": "ingestion", - "range": true, - "refId": "C" - } + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "rowsps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 277, + "options": { + "legend": { + "calcs": [ + "mean" ], - "title": "Total Ingestion Rate", - "type": "timeseries" + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum(rate(greptime_servers_http_logs_ingestion_counter[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "http-logs", + "range": true, + "refId": "http_logs" }, { "datasource": { "type": "prometheus", "uid": "${metrics}" }, - "description": "Total ingestion rate.\n\nHere we listed 3 primary protocols:\n\n- Prometheus remote write\n- Greptime's gRPC API (when using our ingest SDK)\n- Log ingestion http API\n", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "rowsps" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 24, - "x": 0, - "y": 70 - }, - "id": 277, - "options": { - "legend": { - "calcs": [ - "mean" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "12.0.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum(rate(greptime_servers_http_logs_ingestion_counter[$__rate_interval]))", - "hide": false, - "instant": false, - "legendFormat": "http-logs", - "range": true, - "refId": "http_logs" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum(rate(greptime_servers_prometheus_remote_write_samples[$__rate_interval]))", - "hide": false, - "instant": false, - "legendFormat": "prometheus-remote-write", - "range": true, - "refId": "prometheus-remote-write" - } - ], - "title": "Ingestion Rate by Type", - "type": "timeseries" + "editorMode": "code", + "expr": "sum(rate(greptime_servers_prometheus_remote_write_samples[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "prometheus-remote-write", + "range": true, + "refId": "prometheus-remote-write" } ], - "title": "Ingestion", - "type": "row" + "title": "Ingestion Rate by Type", + "type": "timeseries" }, { "collapsed": true, @@ -1162,7 +1162,7 @@ "h": 1, "w": 24, "x": 0, - "y": 10 + "y": 22 }, "id": 276, "panels": [ @@ -1231,7 +1231,7 @@ "h": 6, "w": 24, "x": 0, - "y": 19 + "y": 246 }, "id": 255, "options": { @@ -1303,7 +1303,7 @@ "h": 1, "w": 24, "x": 0, - "y": 11 + "y": 23 }, "id": 274, "panels": [ @@ -1371,7 +1371,7 @@ "h": 10, "w": 12, "x": 0, - "y": 20 + "y": 247 }, "id": 256, "options": { @@ -1486,7 +1486,7 @@ "h": 10, "w": 12, "x": 12, - "y": 20 + "y": 247 }, "id": 262, "options": { @@ -1603,7 +1603,7 @@ "h": 10, "w": 12, "x": 0, - "y": 30 + "y": 257 }, "id": 266, "options": { @@ -1718,7 +1718,7 @@ "h": 10, "w": 12, "x": 12, - "y": 30 + "y": 257 }, "id": 268, "options": { @@ -1835,7 +1835,7 @@ "h": 10, "w": 12, "x": 0, - "y": 40 + "y": 267 }, "id": 269, "options": { @@ -1950,7 +1950,7 @@ "h": 10, "w": 12, "x": 12, - "y": 40 + "y": 267 }, "id": 271, "options": { @@ -2067,7 +2067,7 @@ "h": 10, "w": 12, "x": 0, - "y": 50 + "y": 277 }, "id": 272, "options": { @@ -2182,7 +2182,7 @@ "h": 10, "w": 12, "x": 12, - "y": 50 + "y": 277 }, "id": 273, "options": { @@ -2245,7 +2245,7 @@ "h": 1, "w": 24, "x": 0, - "y": 12 + "y": 24 }, "id": 280, "panels": [ @@ -2314,7 +2314,7 @@ "h": 8, "w": 12, "x": 0, - "y": 61 + "y": 288 }, "id": 281, "options": { @@ -2415,7 +2415,7 @@ "h": 8, "w": 12, "x": 12, - "y": 61 + "y": 288 }, "id": 282, "options": { @@ -2516,7 +2516,7 @@ "h": 8, "w": 12, "x": 0, - "y": 69 + "y": 296 }, "id": 283, "options": { @@ -2617,7 +2617,7 @@ "h": 8, "w": 12, "x": 12, - "y": 69 + "y": 296 }, "id": 284, "options": { @@ -2716,7 +2716,7 @@ "h": 8, "w": 12, "x": 0, - "y": 77 + "y": 304 }, "id": 285, "options": { @@ -2817,7 +2817,7 @@ "h": 8, "w": 12, "x": 12, - "y": 77 + "y": 304 }, "id": 286, "options": { @@ -2919,7 +2919,7 @@ "h": 8, "w": 12, "x": 0, - "y": 85 + "y": 312 }, "id": 287, "options": { @@ -3020,7 +3020,7 @@ "h": 8, "w": 12, "x": 12, - "y": 85 + "y": 312 }, "id": 288, "options": { @@ -3064,7 +3064,7 @@ "h": 1, "w": 24, "x": 0, - "y": 13 + "y": 25 }, "id": 289, "panels": [ @@ -3133,7 +3133,7 @@ "h": 6, "w": 24, "x": 0, - "y": 62 + "y": 289 }, "id": 292, "options": { @@ -3234,7 +3234,7 @@ "h": 8, "w": 12, "x": 0, - "y": 68 + "y": 295 }, "id": 290, "options": { @@ -3335,7 +3335,7 @@ "h": 8, "w": 12, "x": 12, - "y": 68 + "y": 295 }, "id": 291, "options": { @@ -3464,7 +3464,7 @@ "h": 8, "w": 12, "x": 0, - "y": 76 + "y": 303 }, "id": 336, "options": { @@ -3518,2519 +3518,2520 @@ "type": "row" }, { - "collapsed": false, + "collapsed": true, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 14 + "y": 26 }, "id": 293, - "panels": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Request QPS per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 193 + }, + "id": 294, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod, type) (rate(greptime_mito_handle_request_elapsed_count{}[$__rate_interval]))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", + "range": true, + "refId": "A" + } + ], + "title": "Request OPS per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Request P99 per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 193 + }, + "id": 295, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(instance, pod, le, type) (rate(greptime_mito_handle_request_elapsed_bucket{}[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", + "range": true, + "refId": "A" + } + ], + "title": "Request P99 per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Write Buffer per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 225 + }, + "id": 296, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_mito_write_buffer_bytes{}", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]", + "range": true, + "refId": "A" + } + ], + "title": "Write Buffer per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Ingestion size by row counts.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "rowsps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 225 + }, + "id": 297, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by (instance, pod) (rate(greptime_mito_write_rows_total{}[$__rate_interval]))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]", + "range": true, + "refId": "A" + } + ], + "title": "Write Rows per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Flush QPS per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 233 + }, + "id": 298, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod, reason) (rate(greptime_mito_flush_requests_total{}[$__rate_interval]))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{reason}}]", + "range": true, + "refId": "A" + } + ], + "title": "Flush OPS per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Write Stall per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 233 + }, + "id": 299, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod) (greptime_mito_write_stall_total{})", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]", + "range": true, + "refId": "A" + } + ], + "title": "Write Stall per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Read Stage OPS per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 241 + }, + "id": 300, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod) (rate(greptime_mito_read_stage_elapsed_count{ stage=\"total\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]", + "range": true, + "refId": "A" + } + ], + "title": "Read Stage OPS per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Read Stage P99 per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "[10.39.145.87:4000]-[mycluster-datanode-1]-[cache_miss_read]" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 241 + }, + "id": 301, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(instance, pod, le, stage) (rate(greptime_mito_read_stage_elapsed_bucket{}[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]", + "range": true, + "refId": "A" + } + ], + "title": "Read Stage P99 per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Write Stage P99 per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 249 + }, + "id": 302, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(instance, pod, le, stage) (rate(greptime_mito_write_stage_elapsed_bucket{}[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]", + "range": true, + "refId": "A" + } + ], + "title": "Write Stage P99 per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Compaction OPS per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 249 + }, + "id": 303, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count{}[$__rate_interval]))", + "instant": false, + "legendFormat": "[{{ instance }}]-[{{pod}}]", + "range": true, + "refId": "A" + } + ], + "title": "Compaction OPS per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Compaction latency by stage", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 257 + }, + "id": 304, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(instance, pod, le, stage) (rate(greptime_mito_compaction_stage_elapsed_bucket{}[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-p99", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod, stage) (rate(greptime_mito_compaction_stage_elapsed_sum{}[$__rate_interval]))/sum by(instance, pod, stage) (rate(greptime_mito_compaction_stage_elapsed_count{}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-avg", + "range": true, + "refId": "B" + } + ], + "title": "Compaction Elapsed Time per Instance by Stage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Compaction P99 per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 257 + }, + "id": 305, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(instance, pod, le,stage) (rate(greptime_mito_compaction_total_elapsed_bucket{}[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-compaction", + "range": true, + "refId": "A" + } + ], + "title": "Compaction P99 per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Write-ahead logs write size as bytes. This chart includes stats of p95 and p99 size by instance, total WAL write rate.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 265 + }, + "id": 306, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum by(le,instance, pod) (rate(raft_engine_write_size_bucket[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-req-size-p95", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(le,instance,pod) (rate(raft_engine_write_size_bucket[$__rate_interval])))", + "hide": false, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-req-size-p99", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by (instance, pod)(rate(raft_engine_write_size_sum[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-throughput", + "range": true, + "refId": "C" + } + ], + "title": "WAL write size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Cached Bytes per Instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "[10.39.145.79:4000]-[mycluster-datanode-0]-[file]" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 265 + }, + "id": 307, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_mito_cache_bytes{}", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", + "range": true, + "refId": "A" + } + ], + "title": "Cached Bytes per Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Ongoing compaction task count", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 273 + }, + "id": 308, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_mito_inflight_compaction_count", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]", + "range": true, + "refId": "A" + } + ], + "title": "Inflight Compaction", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Raft engine (local disk) log store sync latency, p99", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 273 + }, + "id": 310, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(le, type, node, instance, pod) (rate(raft_engine_sync_log_duration_seconds_bucket[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-p99", + "range": true, + "refId": "A" + } + ], + "title": "WAL sync duration seconds", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Write-ahead log operations latency at p99", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 281 + }, + "id": 311, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(le,logstore,optype,instance, pod) (rate(greptime_logstore_op_elapsed_bucket[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{logstore}}]-[{{optype}}]-p99", + "range": true, + "refId": "A" + } + ], + "title": "Log Store op duration seconds", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Ongoing flush task count", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 281 + }, + "id": 312, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_mito_inflight_flush_count", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]", + "range": true, + "refId": "A" + } + ], + "title": "Inflight Flush", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Compaction oinput output bytes", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 289 + }, + "id": 335, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod) (greptime_mito_compaction_input_bytes)", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-input", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod) (greptime_mito_compaction_output_bytes)", + "hide": false, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-output", + "range": true, + "refId": "B" + } + ], + "title": "Compaction Input/Output Bytes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Per-stage elapsed time for region worker to handle bulk insert region requests.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "[192.168.50.8:4000]-[]-[prepare_bulk_request]-AVG", + "[minipc-1:4000]-[]-[bulk_extend]-AVG", + "[minipc-1:4000]-[]-[prepare_bulk_request]-AVG", + "[minipc-1:4000]-[]-[process_bulk_req]-AVG", + "[minipc-1:4000]-[]-[write_bulk]-AVG" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 289 + }, + "id": 337, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum by(le,instance, stage, pod) (rate(greptime_region_worker_handle_write_bucket[$__rate_interval])))", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-P95", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, stage, pod) (rate(greptime_region_worker_handle_write_sum[$__rate_interval]))/sum by(instance, stage, pod) (rate(greptime_region_worker_handle_write_count[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-AVG", + "range": true, + "refId": "B" + } + ], + "title": "Region Worker Handle Bulk Insert Requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Compaction oinput output bytes", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 297 + }, + "id": 348, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod) (greptime_mito_memtable_active_series_count)", + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-series", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(instance, pod) (greptime_mito_memtable_field_builder_count)", + "hide": false, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-field_builders", + "range": true, + "refId": "B" + } + ], + "title": "Active Series and Field Builders Count", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Per-stage elapsed time for region worker to decode requests.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 297 + }, + "id": 338, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "histogram_quantile(0.95, sum by(le, instance, stage, pod) (rate(greptime_datanode_convert_region_request_bucket[$__rate_interval])))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-P95", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by(le,instance, stage, pod) (rate(greptime_datanode_convert_region_request_sum[$__rate_interval]))/sum by(le,instance, stage, pod) (rate(greptime_datanode_convert_region_request_count[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-AVG", + "range": true, + "refId": "B" + } + ], + "title": "Region Worker Convert Requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "The local cache miss of the datanode.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 305 + }, + "id": 349, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "sum by (instance,pod, type) (rate(greptime_mito_cache_miss{}[$__rate_interval]))", + "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", + "range": true, + "refId": "A" + } + ], + "title": "Cache Miss", + "type": "timeseries" + } + ], "title": "Mito Engine", "type": "row" }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Request QPS per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ops" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 15 - }, - "id": 294, - "options": { - "legend": { - "calcs": [ - "mean" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod, type) (rate(greptime_mito_handle_request_elapsed_count{}[$__rate_interval]))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", - "range": true, - "refId": "A" - } - ], - "title": "Request OPS per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Request P99 per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 15 - }, - "id": 295, - "options": { - "legend": { - "calcs": [ - "mean" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(instance, pod, le, type) (rate(greptime_mito_handle_request_elapsed_bucket{}[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", - "range": true, - "refId": "A" - } - ], - "title": "Request P99 per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Write Buffer per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "decbytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 23 - }, - "id": 296, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_mito_write_buffer_bytes{}", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]", - "range": true, - "refId": "A" - } - ], - "title": "Write Buffer per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Ingestion size by row counts.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "rowsps" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 23 - }, - "id": 297, - "options": { - "legend": { - "calcs": [ - "mean" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by (instance, pod) (rate(greptime_mito_write_rows_total{}[$__rate_interval]))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]", - "range": true, - "refId": "A" - } - ], - "title": "Write Rows per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Flush QPS per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ops" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 31 - }, - "id": 298, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod, reason) (rate(greptime_mito_flush_requests_total{}[$__rate_interval]))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{reason}}]", - "range": true, - "refId": "A" - } - ], - "title": "Flush OPS per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Write Stall per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 31 - }, - "id": 299, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod) (greptime_mito_write_stall_total{})", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]", - "range": true, - "refId": "A" - } - ], - "title": "Write Stall per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Read Stage OPS per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ops" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 39 - }, - "id": 300, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod) (rate(greptime_mito_read_stage_elapsed_count{ stage=\"total\"}[$__rate_interval]))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]", - "range": true, - "refId": "A" - } - ], - "title": "Read Stage OPS per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Read Stage P99 per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [ - { - "__systemRef": "hideSeriesFrom", - "matcher": { - "id": "byNames", - "options": { - "mode": "exclude", - "names": [ - "[10.39.145.87:4000]-[mycluster-datanode-1]-[cache_miss_read]" - ], - "prefix": "All except:", - "readOnly": true - } - }, - "properties": [ - { - "id": "custom.hideFrom", - "value": { - "legend": false, - "tooltip": false, - "viz": true - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 39 - }, - "id": 301, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(instance, pod, le, stage) (rate(greptime_mito_read_stage_elapsed_bucket{}[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]", - "range": true, - "refId": "A" - } - ], - "title": "Read Stage P99 per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Write Stage P99 per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 47 - }, - "id": 302, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true, - "sortBy": "Last *", - "sortDesc": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(instance, pod, le, stage) (rate(greptime_mito_write_stage_elapsed_bucket{}[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]", - "range": true, - "refId": "A" - } - ], - "title": "Write Stage P99 per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Compaction OPS per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ops" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 47 - }, - "id": 303, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count{}[$__rate_interval]))", - "instant": false, - "legendFormat": "[{{ instance }}]-[{{pod}}]", - "range": true, - "refId": "A" - } - ], - "title": "Compaction OPS per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Compaction latency by stage", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 55 - }, - "id": 304, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(instance, pod, le, stage) (rate(greptime_mito_compaction_stage_elapsed_bucket{}[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-p99", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod, stage) (rate(greptime_mito_compaction_stage_elapsed_sum{}[$__rate_interval]))/sum by(instance, pod, stage) (rate(greptime_mito_compaction_stage_elapsed_count{}[$__rate_interval]))", - "hide": false, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-avg", - "range": true, - "refId": "B" - } - ], - "title": "Compaction Elapsed Time per Instance by Stage", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Compaction P99 per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 55 - }, - "id": 305, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(instance, pod, le,stage) (rate(greptime_mito_compaction_total_elapsed_bucket{}[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-compaction", - "range": true, - "refId": "A" - } - ], - "title": "Compaction P99 per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Write-ahead logs write size as bytes. This chart includes stats of p95 and p99 size by instance, total WAL write rate.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 63 - }, - "id": 306, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.95, sum by(le,instance, pod) (rate(raft_engine_write_size_bucket[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-req-size-p95", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(le,instance,pod) (rate(raft_engine_write_size_bucket[$__rate_interval])))", - "hide": false, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-req-size-p99", - "range": true, - "refId": "B" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by (instance, pod)(rate(raft_engine_write_size_sum[$__rate_interval]))", - "hide": false, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-throughput", - "range": true, - "refId": "C" - } - ], - "title": "WAL write size", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Cached Bytes per Instance.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "decbytes" - }, - "overrides": [ - { - "__systemRef": "hideSeriesFrom", - "matcher": { - "id": "byNames", - "options": { - "mode": "exclude", - "names": [ - "[10.39.145.79:4000]-[mycluster-datanode-0]-[file]" - ], - "prefix": "All except:", - "readOnly": true - } - }, - "properties": [ - { - "id": "custom.hideFrom", - "value": { - "legend": false, - "tooltip": false, - "viz": true - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 63 - }, - "id": 307, - "options": { - "legend": { - "calcs": [], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_mito_cache_bytes{}", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", - "range": true, - "refId": "A" - } - ], - "title": "Cached Bytes per Instance", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Ongoing compaction task count", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 71 - }, - "id": 308, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_mito_inflight_compaction_count", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]", - "range": true, - "refId": "A" - } - ], - "title": "Inflight Compaction", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Raft engine (local disk) log store sync latency, p99", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 71 - }, - "id": 310, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(le, type, node, instance, pod) (rate(raft_engine_sync_log_duration_seconds_bucket[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-p99", - "range": true, - "refId": "A" - } - ], - "title": "WAL sync duration seconds", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Write-ahead log operations latency at p99", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 79 - }, - "id": 311, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(le,logstore,optype,instance, pod) (rate(greptime_logstore_op_elapsed_bucket[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{logstore}}]-[{{optype}}]-p99", - "range": true, - "refId": "A" - } - ], - "title": "Log Store op duration seconds", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Ongoing flush task count", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 79 - }, - "id": 312, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_mito_inflight_flush_count", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]", - "range": true, - "refId": "A" - } - ], - "title": "Inflight Flush", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Compaction oinput output bytes", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 87 - }, - "id": 335, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod) (greptime_mito_compaction_input_bytes)", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-input", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod) (greptime_mito_compaction_output_bytes)", - "hide": false, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-output", - "range": true, - "refId": "B" - } - ], - "title": "Compaction Input/Output Bytes", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Per-stage elapsed time for region worker to handle bulk insert region requests.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [ - { - "__systemRef": "hideSeriesFrom", - "matcher": { - "id": "byNames", - "options": { - "mode": "exclude", - "names": [ - "[192.168.50.8:4000]-[]-[prepare_bulk_request]-AVG", - "[minipc-1:4000]-[]-[bulk_extend]-AVG", - "[minipc-1:4000]-[]-[prepare_bulk_request]-AVG", - "[minipc-1:4000]-[]-[process_bulk_req]-AVG", - "[minipc-1:4000]-[]-[write_bulk]-AVG" - ], - "prefix": "All except:", - "readOnly": true - } - }, - "properties": [ - { - "id": "custom.hideFrom", - "value": { - "legend": false, - "tooltip": false, - "viz": true - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 87 - }, - "id": 337, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.95, sum by(le,instance, stage, pod) (rate(greptime_region_worker_handle_write_bucket[$__rate_interval])))", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-P95", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, stage, pod) (rate(greptime_region_worker_handle_write_sum[$__rate_interval]))/sum by(instance, stage, pod) (rate(greptime_region_worker_handle_write_count[$__rate_interval]))", - "hide": false, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-AVG", - "range": true, - "refId": "B" - } - ], - "title": "Region Worker Handle Bulk Insert Requests", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Compaction oinput output bytes", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 95 - }, - "id": 348, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod) (greptime_mito_memtable_active_series_count)", - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-series", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(instance, pod) (greptime_mito_memtable_field_builder_count)", - "hide": false, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-field_builders", - "range": true, - "refId": "B" - } - ], - "title": "Active Series and Field Builders Count", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Per-stage elapsed time for region worker to decode requests.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "points", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 95 - }, - "id": 338, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "builder", - "expr": "histogram_quantile(0.95, sum by(le, instance, stage, pod) (rate(greptime_datanode_convert_region_request_bucket[$__rate_interval])))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-P95", - "range": true, - "refId": "A", - "useBackend": false - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by(le,instance, stage, pod) (rate(greptime_datanode_convert_region_request_sum[$__rate_interval]))/sum by(le,instance, stage, pod) (rate(greptime_datanode_convert_region_request_count[$__rate_interval]))", - "hide": false, - "instant": false, - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{stage}}]-AVG", - "range": true, - "refId": "B" - } - ], - "title": "Region Worker Convert Requests", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "The local cache miss of the datanode.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 103 - }, - "id": 349, - "options": { - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "sum by (instance,pod, type) (rate(greptime_mito_cache_miss{}[$__rate_interval]))", - "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]", - "range": true, - "refId": "A" - } - ], - "title": "Cache Miss", - "type": "timeseries" - }, { "collapsed": true, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 111 + "y": 27 }, "id": 313, "panels": [ @@ -6098,7 +6099,7 @@ "h": 10, "w": 24, "x": 0, - "y": 232 + "y": 459 }, "id": 314, "options": { @@ -6198,7 +6199,7 @@ "h": 7, "w": 12, "x": 0, - "y": 242 + "y": 469 }, "id": 315, "options": { @@ -6298,7 +6299,7 @@ "h": 7, "w": 12, "x": 12, - "y": 242 + "y": 469 }, "id": 316, "options": { @@ -6398,7 +6399,7 @@ "h": 7, "w": 12, "x": 0, - "y": 249 + "y": 476 }, "id": 317, "options": { @@ -6498,7 +6499,7 @@ "h": 7, "w": 12, "x": 12, - "y": 249 + "y": 476 }, "id": 318, "options": { @@ -6598,7 +6599,7 @@ "h": 7, "w": 12, "x": 0, - "y": 256 + "y": 483 }, "id": 319, "options": { @@ -6699,7 +6700,7 @@ "h": 7, "w": 12, "x": 12, - "y": 256 + "y": 483 }, "id": 320, "options": { @@ -6800,7 +6801,7 @@ "h": 7, "w": 12, "x": 0, - "y": 263 + "y": 490 }, "id": 321, "options": { @@ -6900,7 +6901,7 @@ "h": 7, "w": 12, "x": 12, - "y": 263 + "y": 490 }, "id": 322, "options": { @@ -7000,7 +7001,7 @@ "h": 7, "w": 12, "x": 0, - "y": 270 + "y": 497 }, "id": 323, "options": { @@ -7103,7 +7104,7 @@ "h": 7, "w": 12, "x": 12, - "y": 270 + "y": 497 }, "id": 334, "options": { @@ -7144,1827 +7145,1829 @@ "type": "row" }, { - "collapsed": false, + "collapsed": true, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 112 + "y": 28 }, "id": 351, - "panels": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Triggered region flush total", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 195 + }, + "id": 350, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "meta_triggered_region_flush_total", + "instant": false, + "legendFormat": "{{pod}}-{{topic_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Triggered region flush total", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Triggered region checkpoint total", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 195 + }, + "id": 352, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "meta_triggered_region_checkpoint_total", + "instant": false, + "legendFormat": "{{pod}}-{{topic_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Triggered region checkpoint total", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Topic estimated max replay size", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 227 + }, + "id": 353, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "meta_topic_estimated_replay_size", + "instant": false, + "legendFormat": "{{pod}}-{{topic_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Topic estimated replay size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Kafka logstore's bytes traffic", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 227 + }, + "id": 354, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "rate(greptime_logstore_kafka_client_bytes_total[$__rate_interval])", + "instant": false, + "legendFormat": "{{pod}}-{{logstore}}", + "range": true, + "refId": "A" + } + ], + "title": "Kafka logstore's bytes traffic", + "type": "timeseries" + } + ], "title": "Remote WAL", "type": "row" }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Triggered region flush total", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 113 - }, - "id": 350, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "meta_triggered_region_flush_total", - "instant": false, - "legendFormat": "{{pod}}-{{topic_name}}", - "range": true, - "refId": "A" - } - ], - "title": "Triggered region flush total", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Triggered region checkpoint total", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 113 - }, - "id": 352, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "meta_triggered_region_checkpoint_total", - "instant": false, - "legendFormat": "{{pod}}-{{topic_name}}", - "range": true, - "refId": "A" - } - ], - "title": "Triggered region checkpoint total", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Topic estimated max replay size", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 121 - }, - "id": 353, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "meta_topic_estimated_replay_size", - "instant": false, - "legendFormat": "{{pod}}-{{topic_name}}", - "range": true, - "refId": "A" - } - ], - "title": "Topic estimated replay size", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Kafka logstore's bytes traffic", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 121 - }, - "id": 354, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "rate(greptime_logstore_kafka_client_bytes_total[$__rate_interval])", - "instant": false, - "legendFormat": "{{pod}}-{{logstore}}", - "range": true, - "refId": "A" - } - ], - "title": "Kafka logstore's bytes traffic", - "type": "timeseries" - }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 129 - }, - "id": 324, - "panels": [], - "title": "Metasrv", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Counter of region migration by source and destination", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "axisPlacement": "auto", - "fillOpacity": 70, - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineWidth": 1 - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 130 - }, - "id": 325, - "options": { - "colWidth": 0.9, - "legend": { - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "rowHeight": 0.9, - "showValue": "auto", - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_meta_region_migration_stat{datanode_type=\"src\"}", - "instant": false, - "legendFormat": "from-datanode-{{datanode_id}}", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_meta_region_migration_stat{datanode_type=\"desc\"}", - "hide": false, - "instant": false, - "legendFormat": "to-datanode-{{datanode_id}}", - "range": true, - "refId": "B" - } - ], - "title": "Region migration datanode", - "type": "status-history" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Counter of region migration error", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 130 - }, - "id": 326, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_meta_region_migration_error", - "instant": false, - "legendFormat": "{{pod}}-{{state}}-{{error_type}}", - "range": true, - "refId": "A" - } - ], - "title": "Region migration error", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "binBps" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 138 - }, - "id": 327, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_datanode_load", - "instant": false, - "legendFormat": "Datanode-{{datanode_id}}-writeload", - "range": true, - "refId": "A" - } - ], - "title": "Datanode load", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Displays the rate of SQL executions processed by the Meta service using the RDS backend.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 138 - }, - "id": 339, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "rate(greptime_meta_rds_pg_sql_execute_elapsed_ms_count[$__rate_interval])", - "instant": false, - "legendFormat": "{{pod}} {{op}} {{type}} {{result}} ", - "range": true, - "refId": "A" - } - ], - "title": "Rate of SQL Executions (RDS)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Measures the response time of SQL executions via the RDS backend. ", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ms" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 146 - }, - "id": 340, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.90, sum by(pod, op, type, result, le) (rate(greptime_meta_rds_pg_sql_execute_elapsed_ms_bucket[$__rate_interval])))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "{{pod}} {{op}} {{type}} {{result}} p90", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "SQL Execution Latency (RDS)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Shows latency of Meta handlers by pod and handler name, useful for monitoring handler performance and detecting latency spikes.\n", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 146 - }, - "id": 341, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.90, sum by(pod, le, name) (\n rate(greptime_meta_handler_execute_bucket[$__rate_interval])\n))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "{{pod}} {{name}} p90", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Handler Execution Latency", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Shows p90 heartbeat message sizes, helping track network usage and identify anomalies in heartbeat payload.\n", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 154 - }, - "id": 342, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.9, sum by(pod, le) (greptime_meta_heartbeat_stat_memory_size_bucket))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "{{pod}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Heartbeat Packet Size", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 154 - }, - "id": 345, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "rate(greptime_meta_heartbeat_rate[$__rate_interval])", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "{{pod}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Meta Heartbeat Receive Rate", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 162 - }, - "id": 343, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(pod, le, op, target) (greptime_meta_kv_request_elapsed_bucket))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "{{pod}}-{{op}} p99", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Meta KV Ops Latency", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 162 - }, - "id": 346, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "rate(greptime_meta_kv_request_elapsed_count[$__rate_interval])", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "{{pod}}-{{op}} p99", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Rate of meta KV Ops", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 170 - }, - "id": 347, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_tables_bucket))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "CreateLogicalTables-{{step}} p90", - "range": true, - "refId": "A", - "useBackend": false - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_table))", - "fullMetaSearch": false, - "hide": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "CreateTable-{{step}} p90", - "range": true, - "refId": "B", - "useBackend": false - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_view))", - "fullMetaSearch": false, - "hide": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "CreateView-{{step}} p90", - "range": true, - "refId": "C", - "useBackend": false - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_flow))", - "fullMetaSearch": false, - "hide": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "CreateFlow-{{step}} p90", - "range": true, - "refId": "D", - "useBackend": false - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_drop_table))", - "fullMetaSearch": false, - "hide": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "DropTable-{{step}} p90", - "range": true, - "refId": "E", - "useBackend": false - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_alter_table))", - "fullMetaSearch": false, - "hide": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "AlterTable-{{step}} p90", - "range": true, - "refId": "F", - "useBackend": false - } - ], - "title": "DDL Latency", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Reconciliation stats", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 170 - }, - "id": 355, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "greptime_meta_reconciliation_stats", - "hide": false, - "instant": false, - "legendFormat": "{{pod}}-{{table_type}}-{{type}}", - "range": true, - "refId": "A" - } - ], - "title": "Reconciliation stats", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "description": "Elapsed of Reconciliation steps", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 178 - }, - "id": 356, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.6.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${metrics}" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.9, greptime_meta_reconciliation_procedure_bucket)", - "hide": false, - "instant": false, - "legendFormat": "{{procedure_name}}-{{step}}-P90", - "range": true, - "refId": "A" - } - ], - "title": "Reconciliation steps", - "type": "timeseries" - }, { "collapsed": true, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 186 + "y": 29 + }, + "id": 324, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Counter of region migration by source and destination", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 196 + }, + "id": 325, + "options": { + "colWidth": 0.9, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_meta_region_migration_stat{datanode_type=\"src\"}", + "instant": false, + "legendFormat": "from-datanode-{{datanode_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_meta_region_migration_stat{datanode_type=\"desc\"}", + "hide": false, + "instant": false, + "legendFormat": "to-datanode-{{datanode_id}}", + "range": true, + "refId": "B" + } + ], + "title": "Region migration datanode", + "type": "status-history" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Counter of region migration error", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 196 + }, + "id": 326, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_meta_region_migration_error", + "instant": false, + "legendFormat": "{{pod}}-{{state}}-{{error_type}}", + "range": true, + "refId": "A" + } + ], + "title": "Region migration error", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 228 + }, + "id": 327, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_datanode_load", + "instant": false, + "legendFormat": "Datanode-{{datanode_id}}-writeload", + "range": true, + "refId": "A" + } + ], + "title": "Datanode load", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Displays the rate of SQL executions processed by the Meta service using the RDS backend.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 228 + }, + "id": 339, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "rate(greptime_meta_rds_pg_sql_execute_elapsed_ms_count[$__rate_interval])", + "instant": false, + "legendFormat": "{{pod}} {{op}} {{type}} {{result}} ", + "range": true, + "refId": "A" + } + ], + "title": "Rate of SQL Executions (RDS)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Measures the response time of SQL executions via the RDS backend. ", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 236 + }, + "id": 340, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.90, sum by(pod, op, type, result, le) (rate(greptime_meta_rds_pg_sql_execute_elapsed_ms_bucket[$__rate_interval])))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{pod}} {{op}} {{type}} {{result}} p90", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "SQL Execution Latency (RDS)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Shows latency of Meta handlers by pod and handler name, useful for monitoring handler performance and detecting latency spikes.\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 236 + }, + "id": 341, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.90, sum by(pod, le, name) (\n rate(greptime_meta_handler_execute_bucket[$__rate_interval])\n))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{pod}} {{name}} p90", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler Execution Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Shows p90 heartbeat message sizes, helping track network usage and identify anomalies in heartbeat payload.\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 244 + }, + "id": 342, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum by(pod, le) (greptime_meta_heartbeat_stat_memory_size_bucket))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{pod}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Heartbeat Packet Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 244 + }, + "id": 345, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "rate(greptime_meta_heartbeat_rate[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{pod}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Meta Heartbeat Receive Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 252 + }, + "id": 343, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by(pod, le, op, target) (greptime_meta_kv_request_elapsed_bucket))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{pod}}-{{op}} p99", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Meta KV Ops Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 252 + }, + "id": 346, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "rate(greptime_meta_kv_request_elapsed_count[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{pod}}-{{op}} p99", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Rate of meta KV Ops", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Gauge of load information of each datanode, collected via heartbeat between datanode and metasrv. This information is for metasrv to schedule workloads.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 260 + }, + "id": 347, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_tables_bucket))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "CreateLogicalTables-{{step}} p90", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_table))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "CreateTable-{{step}} p90", + "range": true, + "refId": "B", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_view))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "CreateView-{{step}} p90", + "range": true, + "refId": "C", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_create_flow))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "CreateFlow-{{step}} p90", + "range": true, + "refId": "D", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_drop_table))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "DropTable-{{step}} p90", + "range": true, + "refId": "E", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum by(le, pod, step) (greptime_meta_procedure_alter_table))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "AlterTable-{{step}} p90", + "range": true, + "refId": "F", + "useBackend": false + } + ], + "title": "DDL Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Reconciliation stats", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 260 + }, + "id": 355, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "greptime_meta_reconciliation_stats", + "hide": false, + "instant": false, + "legendFormat": "{{pod}}-{{table_type}}-{{type}}", + "range": true, + "refId": "A" + } + ], + "title": "Reconciliation stats", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Elapsed of Reconciliation steps", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 268 + }, + "id": 356, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.9, greptime_meta_reconciliation_procedure_bucket)", + "hide": false, + "instant": false, + "legendFormat": "{{procedure_name}}-{{step}}-P90", + "range": true, + "refId": "A" + } + ], + "title": "Reconciliation steps", + "type": "timeseries" + } + ], + "title": "Metasrv", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 30 }, "id": 328, "panels": [ @@ -9031,7 +9034,7 @@ "h": 8, "w": 12, "x": 0, - "y": 3055 + "y": 3282 }, "id": 329, "options": { @@ -9126,7 +9129,7 @@ "h": 8, "w": 12, "x": 12, - "y": 3055 + "y": 3282 }, "id": 330, "options": { @@ -9234,7 +9237,7 @@ "h": 8, "w": 9, "x": 0, - "y": 3063 + "y": 3290 }, "id": 331, "options": { @@ -9342,7 +9345,7 @@ "h": 8, "w": 9, "x": 9, - "y": 3063 + "y": 3290 }, "id": 332, "options": { @@ -9437,7 +9440,7 @@ "h": 8, "w": 6, "x": 18, - "y": 3063 + "y": 3290 }, "id": 333, "options": { @@ -9474,12 +9477,12 @@ "type": "row" }, { - "collapsed": true, + "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 187 + "y": 31 }, "id": 357, "panels": [], @@ -9550,7 +9553,7 @@ "h": 8, "w": 24, "x": 0, - "y": 188 + "y": 32 }, "id": 358, "options": { @@ -9648,7 +9651,7 @@ "h": 8, "w": 12, "x": 0, - "y": 196 + "y": 40 }, "id": 359, "options": { @@ -9761,7 +9764,7 @@ "h": 8, "w": 12, "x": 12, - "y": 196 + "y": 40 }, "id": 360, "options": { @@ -9861,7 +9864,7 @@ "h": 8, "w": 12, "x": 0, - "y": 204 + "y": 48 }, "id": 361, "options": { @@ -9974,7 +9977,7 @@ "h": 8, "w": 12, "x": 12, - "y": 204 + "y": 48 }, "id": 364, "options": { @@ -10074,7 +10077,7 @@ "h": 8, "w": 12, "x": 0, - "y": 212 + "y": 56 }, "id": 363, "options": { @@ -10187,7 +10190,7 @@ "h": 8, "w": 12, "x": 12, - "y": 212 + "y": 56 }, "id": 362, "options": { @@ -10221,6 +10224,477 @@ ], "title": "Save Alert Failure Rate", "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 64 + }, + "id": 366, + "panels": [], + "title": "Hotspot", + "type": "row" + }, + { + "datasource": { + "type": "mysql", + "uid": "${information_schema}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto", + "wrapText": false + }, + "inspect": false + }, + "fieldMinMax": false, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "disk_size" + }, + "properties": [ + { + "id": "unit", + "value": "bytes" + } + ] + } + ] + }, + "gridPos": { + "h": 18, + "w": 24, + "x": 0, + "y": 65 + }, + "id": 365, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": true, + "fields": [ + "region_rows", + "disk_size" + ], + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "table_schema" + } + ] + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "dataset": "information_schema", + "datasource": { + "type": "mysql", + "uid": "${information_schema}" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH table_stats AS (\n SELECT\n table_id,\n COUNT(*) AS region_count,\n SUM(disk_size) AS total_disk_size,\n SUM(region_rows) as total_region_rows\n FROM information_schema.region_statistics\n WHERE region_role = 'Leader'\n GROUP BY table_id\n HAVING COUNT(*) > 1\n)\n\nSELECT\n t.table_schema,\n t.table_name,\n\n r.region_id,\n t.table_id,\n r.region_number,\n\n p.partition_description,\n\n\n ROUND(\n r.disk_size * 100.0\n / NULLIF(ts.total_disk_size, 0),\n 2\n ) AS disk_size_share_percent,\n\n r.disk_size,\n \n ROUND(\n r.region_rows * 100.0\n / NULLIF(ts.total_region_rows, 0),\n 2\n ) AS region_rows_share_percent,\n r.region_rows\n\nFROM information_schema.region_statistics r\n\nJOIN table_stats ts\n ON r.table_id = ts.table_id\n\nJOIN information_schema.tables t\n ON r.table_id = t.table_id\n\nLEFT JOIN information_schema.partitions p\n ON p.table_schema = t.table_schema\n AND p.table_name = t.table_name\n AND p.greptime_partition_id = r.region_id\n\nWHERE r.region_role = 'Leader'\n\nORDER BY region_rows_share_percent DESC\nLIMIT 100;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Hotspot Regions", + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Write load of each datanode over time.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "fieldMinMax": false, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "total_region_rows", + "total_written_bytes_since_open", + "total_disk_size", + "total_sst_size", + "total_sst_num", + "total_memtable_size", + "total_manifest_size", + "total_index_size", + "cluster_disk_ratio_percent", + "cluster_sst_ratio_percent", + "cluster_rows_ratio_percent", + "max_region_disk_size", + "max_region_rows", + "leader_region_count", + "disk_ratio_percent", + "data_size" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 0, + "y": 83 + }, + "id": 368, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "greptime_datanode_history_load", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "datanode-{{datanode_id}}({{instance}})", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Datanode Load(Write)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "description": "Distribution of write load across datanodes.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "fieldMinMax": false, + "mappings": [], + "min": 0, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 12, + "y": 83 + }, + "id": 369, + "options": { + "displayLabels": [ + "percent" + ], + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "donut", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${metrics}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "greptime_datanode_history_load", + "format": "time_series", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "datanode-{{datanode_id}}({{instance}})", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Datanode Load(Write) Distribution", + "type": "piechart" + }, + { + "datasource": { + "type": "mysql", + "uid": "${information_schema}" + }, + "description": "Distribution of leader regions and data size across datanodes.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "fieldMinMax": false, + "mappings": [], + "unit": "bytes" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "total_region_rows", + "total_written_bytes_since_open", + "total_disk_size", + "total_sst_size", + "total_sst_num", + "total_memtable_size", + "total_manifest_size", + "total_index_size", + "cluster_disk_ratio_percent", + "cluster_sst_ratio_percent", + "cluster_rows_ratio_percent", + "max_region_disk_size", + "max_region_rows", + "leader_region_count", + "disk_ratio_percent", + "data_size" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 0, + "y": 94 + }, + "id": 367, + "options": { + "displayLabels": [ + "value" + ], + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true, + "values": [ + "percent" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/^data_size$/", + "values": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "dataset": "information_schema", + "datasource": { + "type": "mysql", + "uid": "${information_schema}" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH leader_regions AS (\n SELECT\n CONCAT(\n 'datanode-',\n p.peer_id,\n ' (',\n p.peer_addr,\n ')'\n ) AS datanode,\n r.disk_size\n FROM information_schema.region_statistics r\n JOIN information_schema.region_peers p\n ON r.region_id = p.region_id\n WHERE r.region_role = 'Leader'\n AND p.is_leader = 'Yes'\n)\n\nSELECT\n datanode,\n COUNT(*) AS leader_region_count,\n SUM(disk_size) AS data_size\nFROM leader_regions\nGROUP BY datanode\nORDER BY data_size DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Datanode Data Distribution", + "type": "piechart" } ], "preload": false, @@ -10354,12 +10828,12 @@ ] }, "time": { - "from": "now-1h", + "from": "now-2h", "to": "now" }, "timepicker": {}, "timezone": "", - "title": "GreptimeDB", - "uid": "dejf3k5e7g2kgb", - "version": 15 -} + "title": "GreptimeDB Copy", + "uid": "dflfbxbwvvchsa", + "version": 37 +} \ No newline at end of file diff --git a/grafana/dashboards/metrics/standalone/dashboard.md b/grafana/dashboards/metrics/standalone/dashboard.md index 63b45208c5..d2a4ddba0a 100644 --- a/grafana/dashboards/metrics/standalone/dashboard.md +++ b/grafana/dashboards/metrics/standalone/dashboard.md @@ -142,3 +142,87 @@ rate(greptime_trigger_save_alert_record_elapsed_bucket[$__rate_interval]) )` | `timeseries` | Elapsed time to persist trigger alert records. | `prometheus` | `s` | `[{{instance}}]-[{{pod}}]-[{{storage_type}}]-p99` | | Save Alert Failure Rate | `rate(greptime_trigger_save_alert_record_failure_count[$__rate_interval])` | `timeseries` | Rate of failures when persisting trigger alert records. | `prometheus` | `none` | `__auto` | +# Hotspot +| Title | Query | Type | Description | Datasource | Unit | Legend Format | +| --- | --- | --- | --- | --- | --- | --- | +| Hotspot Regions | `WITH table_stats AS ( + SELECT + table_id, + COUNT(*) AS region_count, + SUM(disk_size) AS total_disk_size, + SUM(region_rows) as total_region_rows + FROM information_schema.region_statistics + WHERE region_role = 'Leader' + GROUP BY table_id + HAVING COUNT(*) > 1 +) + +SELECT + t.table_schema, + t.table_name, + + r.region_id, + t.table_id, + r.region_number, + + p.partition_description, + + + ROUND( + r.disk_size * 100.0 + / NULLIF(ts.total_disk_size, 0), + 2 + ) AS disk_size_share_percent, + + r.disk_size, + + ROUND( + r.region_rows * 100.0 + / NULLIF(ts.total_region_rows, 0), + 2 + ) AS region_rows_share_percent, + r.region_rows + +FROM information_schema.region_statistics r + +JOIN table_stats ts + ON r.table_id = ts.table_id + +JOIN information_schema.tables t + ON r.table_id = t.table_id + +LEFT JOIN information_schema.partitions p + ON p.table_schema = t.table_schema + AND p.table_name = t.table_name + AND p.greptime_partition_id = r.region_id + +WHERE r.region_role = 'Leader' + +ORDER BY region_rows_share_percent DESC +LIMIT 100;` | `table` | | `mysql` | -- | -- | +| Datanode Load(Write) | `greptime_datanode_history_load` | `timeseries` | Write load of each datanode over time. | `prometheus` | `binBps` | `datanode-{{datanode_id}}({{instance}})` | +| Datanode Load(Write) Distribution | `greptime_datanode_history_load` | `piechart` | Distribution of write load across datanodes. | `prometheus` | `binBps` | `datanode-{{datanode_id}}({{instance}})` | +| Datanode Data Distribution | `WITH leader_regions AS ( + SELECT + CONCAT( + 'datanode-', + p.peer_id, + ' (', + p.peer_addr, + ')' + ) AS datanode, + r.disk_size + FROM information_schema.region_statistics r + JOIN information_schema.region_peers p + ON r.region_id = p.region_id + WHERE r.region_role = 'Leader' + AND p.is_leader = 'Yes' +) + +SELECT + datanode, + COUNT(*) AS leader_region_count, + SUM(disk_size) AS data_size +FROM leader_regions +GROUP BY datanode +ORDER BY data_size DESC;` | `piechart` | Distribution of leader regions and data size across datanodes. | `mysql` | `bytes` | -- | diff --git a/grafana/dashboards/metrics/standalone/dashboard.yaml b/grafana/dashboards/metrics/standalone/dashboard.yaml index 32f771f66f..db1d2b6a7a 100644 --- a/grafana/dashboards/metrics/standalone/dashboard.yaml +++ b/grafana/dashboards/metrics/standalone/dashboard.yaml @@ -1153,3 +1153,65 @@ groups: type: prometheus uid: ${metrics} legendFormat: __auto + - title: Hotspot + panels: + - title: Hotspot Regions + type: table + queries: + - expr: "WITH table_stats AS (\n SELECT\n table_id,\n COUNT(*) AS region_count,\n SUM(disk_size) AS total_disk_size,\n SUM(region_rows) as total_region_rows\n FROM information_schema.region_statistics\n WHERE region_role = 'Leader'\n GROUP BY table_id\n HAVING COUNT(*) > 1\n)\n\nSELECT\n t.table_schema,\n t.table_name,\n\n r.region_id,\n t.table_id,\n r.region_number,\n\n p.partition_description,\n\n\n ROUND(\n r.disk_size * 100.0\n / NULLIF(ts.total_disk_size, 0),\n 2\n ) AS disk_size_share_percent,\n\n r.disk_size,\n \n ROUND(\n r.region_rows * 100.0\n / NULLIF(ts.total_region_rows, 0),\n 2\n ) AS region_rows_share_percent,\n r.region_rows\n\nFROM information_schema.region_statistics r\n\nJOIN table_stats ts\n ON r.table_id = ts.table_id\n\nJOIN information_schema.tables t\n ON r.table_id = t.table_id\n\nLEFT JOIN information_schema.partitions p\n ON p.table_schema = t.table_schema\n AND p.table_name = t.table_name\n AND p.greptime_partition_id = r.region_id\n\nWHERE r.region_role = 'Leader'\n\nORDER BY region_rows_share_percent DESC\nLIMIT 100;" + datasource: + type: mysql + uid: ${information_schema} + - title: Datanode Load(Write) + type: timeseries + description: Write load of each datanode over time. + unit: binBps + queries: + - expr: greptime_datanode_history_load + datasource: + type: prometheus + uid: ${metrics} + legendFormat: datanode-{{datanode_id}}({{instance}}) + - title: Datanode Load(Write) Distribution + type: piechart + description: Distribution of write load across datanodes. + unit: binBps + queries: + - expr: greptime_datanode_history_load + datasource: + type: prometheus + uid: ${metrics} + legendFormat: datanode-{{datanode_id}}({{instance}}) + - title: Datanode Data Distribution + type: piechart + description: Distribution of leader regions and data size across datanodes. + unit: bytes + queries: + - expr: |- + WITH leader_regions AS ( + SELECT + CONCAT( + 'datanode-', + p.peer_id, + ' (', + p.peer_addr, + ')' + ) AS datanode, + r.disk_size + FROM information_schema.region_statistics r + JOIN information_schema.region_peers p + ON r.region_id = p.region_id + WHERE r.region_role = 'Leader' + AND p.is_leader = 'Yes' + ) + + SELECT + datanode, + COUNT(*) AS leader_region_count, + SUM(disk_size) AS data_size + FROM leader_regions + GROUP BY datanode + ORDER BY data_size DESC; + datasource: + type: mysql + uid: ${information_schema} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 58b88a3894..d16edecca8 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "nightly-2025-10-01" +channel = "nightly-2026-03-21" diff --git a/src/api/src/helper.rs b/src/api/src/helper.rs index 4664c0434b..3b305d90c5 100644 --- a/src/api/src/helper.rs +++ b/src/api/src/helper.rs @@ -444,7 +444,7 @@ impl TryFrom for ColumnDataTypeWrapper { JsonFormat::Jsonb => Some(ColumnDataTypeExtension { type_ext: Some(TypeExt::JsonType(JsonTypeExtension::JsonBinary.into())), }), - JsonFormat::Native(native_type) => { + JsonFormat::Json2(native_type) => { if native_type.is_null() { None } else { @@ -919,6 +919,7 @@ pub fn encode_json_value(value: JsonValue) -> v1::JsonValue { .collect::>(); Some(json_value::Value::Object(JsonObject { entries })) } + JsonVariant::Variant(x) => Some(json_value::Value::Variant(x)), }; v1::JsonValue { value } } @@ -952,6 +953,7 @@ fn decode_json_value(value: &v1::JsonValue) -> JsonValueRef<'_> { }) .collect::>() .into(), + json_value::Value::Variant(x) => x.as_slice().into(), } } diff --git a/src/auth/tests/mod.rs b/src/auth/tests/mod.rs index 65db96a13f..4abbf89e5c 100644 --- a/src/auth/tests/mod.rs +++ b/src/auth/tests/mod.rs @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![feature(assert_matches)] -use std::assert_matches::assert_matches; +use std::assert_matches; use std::sync::Arc; use api::v1::greptime_request::Request; diff --git a/src/cache/Cargo.toml b/src/cache/Cargo.toml index 3c128fe1ad..814eb9e484 100644 --- a/src/cache/Cargo.toml +++ b/src/cache/Cargo.toml @@ -9,6 +9,6 @@ catalog.workspace = true common-error.workspace = true common-macro.workspace = true common-meta.workspace = true -moka.workspace = true +moka = { workspace = true, features = ["future"] } partition.workspace = true snafu.workspace = true diff --git a/src/catalog/src/kvbackend.rs b/src/catalog/src/kvbackend.rs index 334acc999c..77eb3f2048 100644 --- a/src/catalog/src/kvbackend.rs +++ b/src/catalog/src/kvbackend.rs @@ -20,6 +20,6 @@ mod table_cache; pub use builder::{ CatalogManagerConfigurator, CatalogManagerConfiguratorRef, KvBackendCatalogManagerBuilder, }; -pub use client::{CachedKvBackend, CachedKvBackendBuilder, MetaKvBackend}; +pub use client::{CachedKvBackend, CachedKvBackendBuilder, new_read_only_meta_kv_backend}; pub use manager::KvBackendCatalogManager; pub use table_cache::{TableCache, TableCacheRef, new_table_cache}; diff --git a/src/catalog/src/kvbackend/client.rs b/src/catalog/src/kvbackend/client.rs index f74509217f..69b31d6232 100644 --- a/src/catalog/src/kvbackend/client.rs +++ b/src/catalog/src/kvbackend/client.rs @@ -21,7 +21,10 @@ use std::time::Duration; use common_error::ext::BoxedError; use common_meta::cache_invalidator::KvCacheInvalidator; use common_meta::error::Error::CacheNotGet; -use common_meta::error::{CacheNotGetSnafu, Error, ExternalSnafu, GetKvCacheSnafu, Result}; +use common_meta::error::{ + CacheNotGetSnafu, Error, ExternalSnafu, GetKvCacheSnafu, Result, UnsupportedSnafu, +}; +use common_meta::kv_backend::read_only::ReadOnlyKvBackend; use common_meta::kv_backend::txn::{Txn, TxnResponse}; use common_meta::kv_backend::{KvBackend, KvBackendRef, TxnService}; use common_meta::rpc::KeyValue; @@ -357,19 +360,35 @@ impl CachedKvBackend { } #[derive(Debug)] -pub struct MetaKvBackend { - pub client: Arc, +pub(crate) struct MetaKvBackend { + client: Arc, } impl MetaKvBackend { /// Constructs a [MetaKvBackend]. - pub fn new(client: Arc) -> MetaKvBackend { + fn new(client: Arc) -> MetaKvBackend { MetaKvBackend { client } } } +pub fn new_read_only_meta_kv_backend(client: Arc) -> KvBackendRef { + Arc::new(ReadOnlyKvBackend::new(Arc::new(MetaKvBackend::new(client)))) +} + +#[async_trait::async_trait] impl TxnService for MetaKvBackend { type Error = Error; + + async fn txn(&self, _txn: Txn) -> Result { + UnsupportedSnafu { + operation: "MetaKvBackend txn", + } + .fail() + } + + fn max_txn_ops(&self) -> usize { + usize::MAX + } } /// Implement `KvBackend` trait for `MetaKvBackend` instead of opendal's `Accessor` since @@ -465,6 +484,9 @@ mod tests { use std::sync::atomic::{AtomicU32, Ordering}; use async_trait::async_trait; + use common_meta::kv_backend::memory::MemoryKvBackend; + use common_meta::kv_backend::read_only::ReadOnlyKvBackend; + use common_meta::kv_backend::txn::{Txn, TxnOp}; use common_meta::kv_backend::{KvBackend, TxnService}; use common_meta::rpc::KeyValue; use common_meta::rpc::store::{ @@ -473,8 +495,9 @@ mod tests { PutResponse, RangeRequest, RangeResponse, }; use dashmap::DashMap; + use meta_client::client::MetaClientBuilder; - use super::CachedKvBackend; + use super::{CachedKvBackend, new_read_only_meta_kv_backend}; #[derive(Default)] pub struct SimpleKvBackend { @@ -579,6 +602,62 @@ mod tests { } } + #[tokio::test] + async fn test_cached_kv_backend_rejects_writes_with_read_only_inner() { + let inner = Arc::new(MemoryKvBackend::::new()); + let cached_kv = CachedKvBackend::wrap(Arc::new(ReadOnlyKvBackend::new(inner))); + + let err = cached_kv + .put(PutRequest { + key: b"k1".to_vec(), + value: b"v1".to_vec(), + prev_kv: false, + }) + .await + .unwrap_err(); + + assert!(matches!( + err, + common_meta::error::Error::ReadOnlyKvBackend { .. } + )); + } + + #[tokio::test] + async fn test_read_only_meta_kv_backend_rejects_writes() { + let meta_client = Arc::new(MetaClientBuilder::frontend_default_options().build()); + let backend = new_read_only_meta_kv_backend(meta_client); + + let err = backend + .put(PutRequest { + key: b"k1".to_vec(), + value: b"v1".to_vec(), + prev_kv: false, + }) + .await + .unwrap_err(); + + assert!(matches!( + err, + common_meta::error::Error::ReadOnlyKvBackend { .. } + )); + } + + #[tokio::test] + async fn test_read_only_meta_kv_backend_does_not_emulate_txn() { + let meta_client = Arc::new(MetaClientBuilder::frontend_default_options().build()); + let backend = new_read_only_meta_kv_backend(meta_client); + + let result = backend + .txn(Txn::new().and_then(vec![TxnOp::Get(b"k1".to_vec())])) + .await; + let err = match result { + Ok(_) => panic!("expected unsupported txn error"), + Err(err) => err, + }; + + assert!(matches!(err, common_meta::error::Error::Unsupported { .. })); + } + async fn add_some_vals(kv_backend: &impl KvBackend) { kv_backend .put(PutRequest { diff --git a/src/catalog/src/kvbackend/table_cache.rs b/src/catalog/src/kvbackend/table_cache.rs index ea328c3e17..42b3fbc74b 100644 --- a/src/catalog/src/kvbackend/table_cache.rs +++ b/src/catalog/src/kvbackend/table_cache.rs @@ -65,11 +65,13 @@ fn init_factory( fn invalidator<'a>( cache: &'a Cache, - ident: &'a CacheIdent, + idents: &'a [&CacheIdent], ) -> BoxFuture<'a, MetaResult<()>> { Box::pin(async move { - if let CacheIdent::TableName(table_name) = ident { - cache.invalidate(table_name).await + for ident in idents { + if let CacheIdent::TableName(table_name) = ident { + cache.invalidate(table_name).await + } } Ok(()) }) diff --git a/src/catalog/src/lib.rs b/src/catalog/src/lib.rs index 9c31e809fd..a701473551 100644 --- a/src/catalog/src/lib.rs +++ b/src/catalog/src/lib.rs @@ -12,9 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![feature(assert_matches)] -#![feature(try_blocks)] - use std::any::Any; use std::fmt::{Debug, Formatter}; use std::sync::Arc; diff --git a/src/catalog/src/memory/manager.rs b/src/catalog/src/memory/manager.rs index 571cd06468..6e747f62ed 100644 --- a/src/catalog/src/memory/manager.rs +++ b/src/catalog/src/memory/manager.rs @@ -132,15 +132,13 @@ impl CatalogManager for MemoryCatalogManager { table_name: &str, _query_ctx: Option<&QueryContext>, ) -> Result> { - let result = try { - self.catalogs - .read() - .unwrap() - .get(catalog)? - .get(schema)? - .get(table_name) - .cloned()? - }; + let catalogs = self.catalogs.read().unwrap(); + let result = catalogs + .get(catalog) + .and_then(|c| c.get(schema)) + .and_then(|s| s.get(table_name)) + .cloned(); + Ok(result) } @@ -149,8 +147,8 @@ impl CatalogManager for MemoryCatalogManager { .catalogs .read() .unwrap() - .iter() - .flat_map(|(_, schema_entries)| schema_entries.values()) + .values() + .flat_map(|schema_entries| schema_entries.values()) .flat_map(|tables| tables.values()) .find(|t| t.table_info().ident.table_id == table_id) .map(|t| t.table_info())) diff --git a/src/catalog/src/process_manager.rs b/src/catalog/src/process_manager.rs index 1eebfec627..49be14933c 100644 --- a/src/catalog/src/process_manager.rs +++ b/src/catalog/src/process_manager.rs @@ -58,6 +58,8 @@ pub enum QueryStatement { Sql(Statement), // The optional string is the alias of the PromQL query. Promql(EvalStmt, Option), + /// Logical plan with original query string + Plan(String), } impl Display for QueryStatement { @@ -71,6 +73,7 @@ impl Display for QueryStatement { write!(f, "{}", eval_stmt) } } + QueryStatement::Plan(query) => write!(f, "{}", query), } } } @@ -170,7 +173,7 @@ impl ProcessManager { let mut processes = vec![]; if let Some(remote_frontend_selector) = self.frontend_selector.as_ref() { let frontends = remote_frontend_selector - .select(|node| node.peer.addr != self.server_addr) + .select(|peer| peer.addr != self.server_addr) .await .context(error::InvokeFrontendSnafu)?; for mut f in frontends { @@ -208,7 +211,7 @@ impl ProcessManager { .frontend_selector .as_ref() .context(error::MetaClientMissingSnafu)? - .select(|node| node.peer.addr == server_addr) + .select(|peer| peer.addr == server_addr) .await .context(error::InvokeFrontendSnafu)?; ensure!( @@ -369,6 +372,9 @@ impl SlowQueryTimer { QueryStatement::Sql(stmt) => { slow_query_event.query = stmt.to_string(); } + QueryStatement::Plan(query) => { + slow_query_event.query = query.clone(); + } } match self.record_type { @@ -395,7 +401,7 @@ impl SlowQueryTimer { impl Drop for SlowQueryTimer { fn drop(&mut self) { - // Calculate the elaspsed duration since the timer is created. + // Calculate the elapsed duration since the timer is created. let elapsed = self.start.elapsed(); if elapsed > self.threshold { // Only capture a portion of slow queries based on sample_ratio. diff --git a/src/catalog/src/system_schema.rs b/src/catalog/src/system_schema.rs index 00b96e5292..6c6a5fc202 100644 --- a/src/catalog/src/system_schema.rs +++ b/src/catalog/src/system_schema.rs @@ -139,12 +139,16 @@ impl DataSource for SystemTableDataSource { &self, request: ScanRequest, ) -> std::result::Result { - let projected_schema = match &request.projection { + let projection = request + .projection_input + .as_ref() + .map(|input| input.projection.clone()); + + let projected_schema = match projection.as_ref() { Some(projection) => self.try_project(projection)?, None => self.table.schema(), }; - let projection = request.projection.clone(); let stream = self .table .to_stream(request) diff --git a/src/catalog/src/system_schema/information_schema/flows.rs b/src/catalog/src/system_schema/information_schema/flows.rs index a751716620..8002b17888 100644 --- a/src/catalog/src/system_schema/information_schema/flows.rs +++ b/src/catalog/src/system_schema/information_schema/flows.rs @@ -16,6 +16,7 @@ use std::sync::{Arc, Weak}; use common_catalog::consts::INFORMATION_SCHEMA_FLOW_TABLE_ID; use common_error::ext::BoxedError; +use common_meta::ddl::create_flow::FlowType; use common_meta::key::FlowId; use common_meta::key::flow::FlowMetadataManager; use common_meta::key::flow::flow_info::FlowInfoValue; @@ -71,6 +72,7 @@ pub const CREATED_TIME: &str = "created_time"; pub const UPDATED_TIME: &str = "updated_time"; pub const LAST_EXECUTION_TIME: &str = "last_execution_time"; pub const SOURCE_TABLE_NAMES: &str = "source_table_names"; +pub const FLOWNODE_ADDRS: &str = "flownode_addrs"; /// The `information_schema.flows` to provides information about flows in databases. #[derive(Debug)] @@ -95,7 +97,8 @@ impl InformationSchemaFlows { } } - /// for complex fields(including [`SOURCE_TABLE_IDS`], [`FLOWNODE_IDS`] and [`OPTIONS`]), it will be serialized to json string for now + /// for complex fields(including [`SOURCE_TABLE_IDS`], [`FLOWNODE_IDS`], [`OPTIONS`] and + /// [`FLOWNODE_ADDRS`]), it will be serialized to json string for now /// TODO(discord9): use a better way to store complex fields like json type pub(crate) fn schema() -> SchemaRef { Arc::new(Schema::new( @@ -119,6 +122,7 @@ impl InformationSchemaFlows { true, ), (SOURCE_TABLE_NAMES, CDT::string_datatype(), true), + (FLOWNODE_ADDRS, CDT::string_datatype(), true), ] .into_iter() .map(|(name, ty, nullable)| ColumnSchema::new(name, ty, nullable)) @@ -165,6 +169,10 @@ impl InformationSchemaFlows { expire_after: flow_info.expire_after(), eval_interval: flow_info.eval_interval(), comment, + flow_options: sql::statements::OptionMap::from_filtered_string_map( + flow_info.options(), + &[FlowType::FLOW_TYPE_KEY], + ), query, }; @@ -239,6 +247,7 @@ struct InformationSchemaFlowsBuilder { updated_time: TimestampMillisecondVectorBuilder, last_execution_time: TimestampMillisecondVectorBuilder, source_table_names: StringVectorBuilder, + flownode_addr_groups: StringVectorBuilder, } impl InformationSchemaFlowsBuilder { @@ -269,6 +278,7 @@ impl InformationSchemaFlowsBuilder { updated_time: TimestampMillisecondVectorBuilder::with_capacity(INIT_CAPACITY), last_execution_time: TimestampMillisecondVectorBuilder::with_capacity(INIT_CAPACITY), source_table_names: StringVectorBuilder::with_capacity(INIT_CAPACITY), + flownode_addr_groups: StringVectorBuilder::with_capacity(INIT_CAPACITY), } } @@ -378,6 +388,21 @@ impl InformationSchemaFlowsBuilder { .get(&flow_id) .map(|v| TimestampMillisecond::new(*v)) })); + let flownode_addrs = self + .flow_metadata_manager + .flownode_addrs(flow_id) + .await + .map_err(BoxedError::new) + .context(InternalSnafu)?; + if flownode_addrs.is_empty() { + self.flownode_addr_groups.push(None); + } else { + let flownode_addrs_json = + serde_json::to_string(&flownode_addrs).with_context(|_| JsonSnafu { + input: format!("{:?}", flownode_addrs), + })?; + self.flownode_addr_groups.push(Some(&flownode_addrs_json)); + } let mut source_table_names = vec![]; let catalog_manager = self @@ -413,6 +438,7 @@ impl InformationSchemaFlowsBuilder { Arc::new(self.updated_time.finish()), Arc::new(self.last_execution_time.finish()), Arc::new(self.source_table_names.finish()), + Arc::new(self.flownode_addr_groups.finish()), ]; RecordBatch::new(self.schema.clone(), columns).context(CreateRecordBatchSnafu) } diff --git a/src/catalog/src/system_schema/information_schema/region_peers.rs b/src/catalog/src/system_schema/information_schema/region_peers.rs index 5bc91d207e..9eddb061f8 100644 --- a/src/catalog/src/system_schema/information_schema/region_peers.rs +++ b/src/catalog/src/system_schema/information_schema/region_peers.rs @@ -267,7 +267,7 @@ impl InformationSchemaRegionPeersBuilder { ]; if !predicates.eval(&row) { - return; + continue; } self.table_catalogs.push(Some(table_catalog)); @@ -331,3 +331,87 @@ impl DfPartitionStream for InformationSchemaRegionPeers { )) } } + +#[cfg(test)] +mod tests { + use api::v1::meta::Peer; + use arrow::array::AsArray; + use common_meta::rpc::router::{Region, RegionRoute}; + use datafusion::common::ScalarValue; + use datafusion::logical_expr::{BinaryExpr, Expr, Operator, col}; + use store_api::storage::{RegionId, ScanRequest}; + + use super::*; + + fn new_region_route(table_id: u32, region_number: u32, peer_id: u64) -> RegionRoute { + RegionRoute { + region: Region { + id: RegionId::new(table_id, region_number), + ..Default::default() + }, + leader_peer: Some(Peer { + id: peer_id, + addr: format!("127.0.0.1:{}", 3000 + peer_id), + }), + follower_peers: vec![], + leader_state: None, + leader_down_since: None, + write_route_policy: None, + } + } + + #[test] + fn test_add_region_peers_predicate_filters_correctly() { + let schema = InformationSchemaRegionPeers::schema(); + let mut builder = InformationSchemaRegionPeersBuilder::new( + schema, + "greptime".to_string(), + Weak::::new(), + ); + + let table_id = 1; + // 3 regions: region_number 0, 1, 2 + let routes = vec![ + new_region_route(table_id, 0, 1), + new_region_route(table_id, 1, 2), + new_region_route(table_id, 2, 3), + ]; + + // Build a predicate that matches only the last region (region_number=2). + // With the old `return` bug, encountering the first non-matching region + // (region_number=0) would exit add_region_peers entirely, so region_number=2 + // would never be found. + let target_region_id = RegionId::new(table_id, 2).as_u64(); + let filter = Expr::BinaryExpr(BinaryExpr::new( + Box::new(col(REGION_ID)), + Operator::Eq, + Box::new(Expr::Literal( + ScalarValue::UInt64(Some(target_region_id)), + None, + )), + )); + let request = ScanRequest { + filters: vec![filter], + ..Default::default() + }; + let predicates = Predicates::from_scan_request(&Some(request)); + + builder.add_region_peers( + "greptime", + "public", + "test_table", + &predicates, + table_id, + &routes, + ); + + let batch = builder.finish().unwrap(); + // Should have exactly 1 row for the matching region + assert_eq!(batch.num_rows(), 1); + // Verify it's the correct region + let region_id_col = batch + .column(3) + .as_primitive::(); + assert_eq!(region_id_col.value(0), target_region_id); + } +} diff --git a/src/catalog/src/system_schema/information_schema/ssts.rs b/src/catalog/src/system_schema/information_schema/ssts.rs index 1c0d507a29..520fc6d485 100644 --- a/src/catalog/src/system_schema/information_schema/ssts.rs +++ b/src/catalog/src/system_schema/information_schema/ssts.rs @@ -63,7 +63,7 @@ impl InformationTable for InformationSchemaSstsManifest { } fn to_stream(&self, request: ScanRequest) -> Result { - let schema = if let Some(p) = &request.projection { + let schema = if let Some(p) = request.projection_indices() { Arc::new(self.schema.try_project(p).context(ProjectSchemaSnafu)?) } else { self.schema.clone() @@ -117,7 +117,7 @@ impl InformationTable for InformationSchemaSstsStorage { } fn to_stream(&self, request: ScanRequest) -> Result { - let schema = if let Some(p) = &request.projection { + let schema = if let Some(p) = request.projection_indices() { Arc::new(self.schema.try_project(p).context(ProjectSchemaSnafu)?) } else { self.schema.clone() @@ -172,7 +172,7 @@ impl InformationTable for InformationSchemaSstsIndexMeta { } fn to_stream(&self, request: ScanRequest) -> Result { - let schema = if let Some(p) = &request.projection { + let schema = if let Some(p) = request.projection_indices() { Arc::new(self.schema.try_project(p).context(ProjectSchemaSnafu)?) } else { self.schema.clone() diff --git a/src/catalog/src/system_schema/information_schema/tables.rs b/src/catalog/src/system_schema/information_schema/tables.rs index 248fb243dd..6175c17d39 100644 --- a/src/catalog/src/system_schema/information_schema/tables.rs +++ b/src/catalog/src/system_schema/information_schema/tables.rs @@ -372,22 +372,16 @@ impl InformationSchemaTablesBuilder { self.table_types.push(Some(table_type_text)); self.table_ids.push(Some(table_id)); - let data_length = region_stats.iter().map(|stat| stat.sst_size).sum(); - let table_rows = region_stats.iter().map(|stat| stat.num_rows).sum(); - let index_length = region_stats.iter().map(|stat| stat.index_size).sum(); + let data_length: u64 = region_stats.iter().map(|stat| stat.sst_size).sum(); + let table_rows: u64 = region_stats.iter().map(|stat| stat.num_rows).sum(); + let index_length: u64 = region_stats.iter().map(|stat| stat.index_size).sum(); - // It's not precise, but it is acceptable for long-term data storage. - let avg_row_length = if table_rows > 0 { - let total_data_length = data_length - + region_stats - .iter() - .map(|stat| stat.memtable_size) - .sum::(); - - total_data_length / table_rows - } else { - 0 - }; + let total_data_length: u64 = data_length + + region_stats + .iter() + .map(|stat| stat.memtable_size) + .sum::(); + let avg_row_length = total_data_length.checked_div(table_rows).unwrap_or(0); self.data_length.push(Some(data_length)); self.index_length.push(Some(index_length)); diff --git a/src/catalog/src/system_schema/pg_catalog.rs b/src/catalog/src/system_schema/pg_catalog.rs index 08aad2d6dd..feec46ff90 100644 --- a/src/catalog/src/system_schema/pg_catalog.rs +++ b/src/catalog/src/system_schema/pg_catalog.rs @@ -74,12 +74,10 @@ impl PGCatalogProvider { ) .expect("Failed to initialize PgCatalogSchemaProvider"); - let mut table_ids = HashMap::new(); - let mut table_id = PG_CATALOG_TABLE_ID_START; - for name in PG_CATALOG_TABLES { - table_ids.insert(*name, table_id); - table_id += 1; - } + let table_ids: HashMap<_, _> = (PG_CATALOG_TABLE_ID_START..) + .zip(PG_CATALOG_TABLES.iter()) + .map(|(id, name)| (*name, id)) + .collect(); let mut provider = Self { catalog_name, diff --git a/src/catalog/src/table_source.rs b/src/catalog/src/table_source.rs index 132e02fe14..fd78cc2573 100644 --- a/src/catalog/src/table_source.rs +++ b/src/catalog/src/table_source.rs @@ -15,7 +15,6 @@ use std::collections::HashMap; use std::sync::Arc; -use bytes::Bytes; use common_catalog::format_full_table_name; use common_query::logical_plan::{SubstraitPlanDecoderRef, rename_logical_plan_columns}; use datafusion::common::{ResolvedTableReference, TableReference}; @@ -151,7 +150,7 @@ impl DfTableSourceProvider { let catalog_list = Arc::new(DummyCatalogList::new(self.catalog_manager.clone())); let logical_plan = self .plan_decoder - .decode(Bytes::from(view_info.view_info.clone()), catalog_list, true) + .decode(view_info.view_info.clone().into(), catalog_list, false) .await .context(DecodePlanSnafu { name: &table.table_info().name, @@ -191,7 +190,7 @@ impl DfTableSourceProvider { plan_columns .iter() .map(|c| c.as_str()) - .zip(columns.into_iter()) + .zip(columns) .collect(), ) .context(ProjectViewColumnsSnafu)? diff --git a/src/cli/Cargo.toml b/src/cli/Cargo.toml index 46e79efd00..8c4150cd46 100644 --- a/src/cli/Cargo.toml +++ b/src/cli/Cargo.toml @@ -44,6 +44,7 @@ common-version.workspace = true common-wal.workspace = true datatypes.workspace = true etcd-client.workspace = true +fs2.workspace = true futures.workspace = true humantime.workspace = true meta-client.workspace = true @@ -65,6 +66,8 @@ store-api.workspace = true table.workspace = true tokio.workspace = true tracing-appender.workspace = true +url.workspace = true +uuid.workspace = true [dev-dependencies] common-meta = { workspace = true, features = ["testing"] } @@ -72,4 +75,3 @@ common-test-util.workspace = true common-version.workspace = true serde.workspace = true tempfile.workspace = true -url.workspace = true diff --git a/src/cli/src/common/object_store.rs b/src/cli/src/common/object_store.rs index 0b8372e509..129fb29cb7 100644 --- a/src/cli/src/common/object_store.rs +++ b/src/cli/src/common/object_store.rs @@ -220,18 +220,8 @@ impl PrefixedAzblobConnection { name: "AzBlob", required: [ (&self.azblob_container, "container"), - (&self.azblob_root, "root"), - (&self.azblob_account_name, "account name"), (&self.azblob_endpoint, "endpoint"), - ], - custom_validator: |missing: &mut Vec<&str>| { - // account_key is only required if sas_token is not provided - if self.azblob_sas_token.is_none() - && self.azblob_account_key.is_empty() - { - missing.push("account key (when sas_token is not provided)"); - } - } + ] ) } } diff --git a/src/cli/src/common/store.rs b/src/cli/src/common/store.rs index 373c96a37a..34baaba4ff 100644 --- a/src/cli/src/common/store.rs +++ b/src/cli/src/common/store.rs @@ -153,17 +153,11 @@ impl StoreConfig { BackendImpl::PostgresStore => { let table_name = &self.meta_table_name; let tls_config = self.tls_config(); - let pool = meta_srv::utils::postgres::create_postgres_pool( + Ok(meta_srv::utils::postgres::build_postgres_kv_backend( store_addrs, None, tls_config, - ) - .await - .map_err(BoxedError::new)?; - let schema_name = self.meta_schema_name.as_deref(); - Ok(common_meta::kv_backend::rds::PgStore::with_pg_pool( - pool, - schema_name, + self.meta_schema_name.as_deref(), table_name, max_txn_ops, self.auto_create_schema, @@ -175,12 +169,9 @@ impl StoreConfig { BackendImpl::MysqlStore => { let table_name = &self.meta_table_name; let tls_config = self.tls_config(); - let pool = - meta_srv::utils::mysql::create_mysql_pool(store_addrs, tls_config.as_ref()) - .await - .map_err(BoxedError::new)?; - Ok(common_meta::kv_backend::rds::MySqlStore::with_mysql_pool( - pool, + Ok(meta_srv::utils::mysql::build_mysql_kv_backend( + store_addrs, + tls_config.as_ref(), table_name, max_txn_ops, ) diff --git a/src/cli/src/data.rs b/src/cli/src/data.rs index 5966040a3b..114886542e 100644 --- a/src/cli/src/data.rs +++ b/src/cli/src/data.rs @@ -13,7 +13,12 @@ // limitations under the License. mod export; +pub mod export_v2; mod import; +pub mod import_v2; +pub(crate) mod path; +pub mod snapshot_storage; +pub(crate) mod sql; mod storage_export; use clap::Subcommand; @@ -22,15 +27,24 @@ use common_error::ext::BoxedError; use crate::Tool; use crate::data::export::ExportCommand; +use crate::data::export_v2::ExportV2Command; use crate::data::import::ImportCommand; +use crate::data::import_v2::ImportV2Command; pub(crate) const COPY_PATH_PLACEHOLDER: &str = ""; /// Command for data operations including exporting data from and importing data into GreptimeDB. #[derive(Subcommand)] pub enum DataCommand { + /// Export data (V1 - legacy). Export(ExportCommand), + /// Import data (V1 - legacy). Import(ImportCommand), + /// Export V2 - JSON-based schema export with manifest support. + #[clap(subcommand)] + ExportV2(ExportV2Command), + /// Import V2 - Import from V2 snapshot. + ImportV2(ImportV2Command), } impl DataCommand { @@ -38,6 +52,8 @@ impl DataCommand { match self { DataCommand::Export(cmd) => cmd.build().await, DataCommand::Import(cmd) => cmd.build().await, + DataCommand::ExportV2(cmd) => cmd.build().await, + DataCommand::ImportV2(cmd) => cmd.build().await, } } } diff --git a/src/cli/src/data/export.rs b/src/cli/src/data/export.rs index 1cdb159336..148c27316e 100644 --- a/src/cli/src/data/export.rs +++ b/src/cli/src/data/export.rs @@ -107,13 +107,16 @@ pub struct ExportCommand { #[clap(long, value_parser = humantime::parse_duration)] timeout: Option, - /// The proxy server address to connect, if set, will override the system proxy. + /// The proxy server address to connect. /// - /// The default behavior will use the system proxy if neither `proxy` nor `no_proxy` is set. + /// If set, it overrides the system proxy unless `--no-proxy` is specified. + /// If neither `--proxy` nor `--no-proxy` is set, system proxy (env) may be used. #[clap(long)] proxy: Option, - /// Disable proxy server, if set, will not use any proxy. + /// Disable all proxy usage (ignores `--proxy` and system proxy). + /// + /// When set and `--proxy` is not provided, this explicitly disables system proxy. #[clap(long)] no_proxy: bool, @@ -173,6 +176,7 @@ impl ExportCommand { // Treats `None` as `0s` to disable server-side default timeout. self.timeout.unwrap_or_default(), proxy, + self.no_proxy, ); Ok(Box::new(Export { @@ -454,8 +458,10 @@ impl Export { /// build operator with preference for file system async fn build_prefer_fs_operator(&self) -> Result { - if self.storage_type.is_remote_storage() && self.ddl_local_dir.is_some() { - let root = self.ddl_local_dir.as_ref().unwrap().clone(); + if self.storage_type.is_remote_storage() + && let Some(ddl_local_dir) = &self.ddl_local_dir + { + let root = ddl_local_dir.clone(); let op = new_fs_object_store(&root).map_err(|e| Error::Other { source: e, location: snafu::location!(), @@ -1078,7 +1084,7 @@ mod tests { #[tokio::test] async fn test_export_command_build_with_azblob_empty_account_name() { - // Test Azure Blob with empty account_name + // account_name is optional for Azure Blob validation let cmd = ExportCommand::parse_from([ "export", "--addr", @@ -1086,30 +1092,19 @@ mod tests { "--azblob", "--azblob-container", "test-container", - "--azblob-root", - "test-root", "--azblob-account-name", "", // Empty account name - "--azblob-account-key", - MOCK_AZBLOB_ACCOUNT_KEY_B64, "--azblob-endpoint", "https://account.blob.core.windows.net", ]); let result = cmd.build().await; - assert!(result.is_err()); - if let Err(err) = result { - assert!( - err.to_string().contains("AzBlob account name must be set"), - "Actual error: {}", - err - ); - } + assert!(result.is_ok(), "Empty account_name should succeed"); } #[tokio::test] async fn test_export_command_build_with_azblob_missing_account_key() { - // Missing account key + // account_key is optional for Azure Blob validation let cmd = ExportCommand::parse_from([ "export", "--addr", @@ -1117,24 +1112,12 @@ mod tests { "--azblob", "--azblob-container", "test-container", - "--azblob-root", - "test-root", - "--azblob-account-name", - "test-account", "--azblob-endpoint", "https://account.blob.core.windows.net", ]); let result = cmd.build().await; - assert!(result.is_err()); - if let Err(err) = result { - assert!( - err.to_string() - .contains("AzBlob account key (when sas_token is not provided) must be set"), - "Actual error: {}", - err - ); - } + assert!(result.is_ok(), "Missing account_key should succeed"); } // ==================== Gap 3: Boundary cases ==================== @@ -1232,21 +1215,58 @@ mod tests { "--azblob", "--azblob-container", "test-container", - "--azblob-root", - "test-root", - "--azblob-account-name", - "test-account", - "--azblob-account-key", - MOCK_AZBLOB_ACCOUNT_KEY_B64, "--azblob-endpoint", "https://account.blob.core.windows.net", - // No sas_token ]); let result = cmd.build().await; assert!(result.is_ok(), "Minimal AzBlob config should succeed"); } + #[tokio::test] + async fn test_export_command_build_with_azblob_missing_endpoint() { + let cmd = ExportCommand::parse_from([ + "export", + "--addr", + "127.0.0.1:4000", + "--azblob", + "--azblob-container", + "test-container", + ]); + + let result = cmd.build().await; + assert!(result.is_err()); + if let Err(err) = result { + assert!( + err.to_string().contains("AzBlob endpoint must be set"), + "Actual error: {}", + err + ); + } + } + + #[tokio::test] + async fn test_export_command_build_with_azblob_missing_container() { + let cmd = ExportCommand::parse_from([ + "export", + "--addr", + "127.0.0.1:4000", + "--azblob", + "--azblob-endpoint", + "https://account.blob.core.windows.net", + ]); + + let result = cmd.build().await; + assert!(result.is_err()); + if let Err(err) = result { + assert!( + err.to_string().contains("AzBlob container must be set"), + "Actual error: {}", + err + ); + } + } + #[tokio::test] async fn test_export_command_build_with_local_and_s3() { // Both output-dir and S3 - S3 should take precedence @@ -1281,7 +1301,7 @@ mod tests { #[tokio::test] async fn test_export_command_build_with_azblob_only_sas_token() { - // Azure Blob with sas_token but no account_key - should succeed + // Azure Blob with sas_token but no credentials - should still succeed let cmd = ExportCommand::parse_from([ "export", "--addr", @@ -1289,15 +1309,10 @@ mod tests { "--azblob", "--azblob-container", "test-container", - "--azblob-root", - "test-root", - "--azblob-account-name", - "test-account", "--azblob-endpoint", "https://account.blob.core.windows.net", "--azblob-sas-token", "test-sas-token", - // No account_key ]); let result = cmd.build().await; @@ -1318,10 +1333,6 @@ mod tests { "--azblob", "--azblob-container", "test-container", - "--azblob-root", - "test-root", - "--azblob-account-name", - "test-account", "--azblob-account-key", "", // Empty account_key is OK if sas_token is provided "--azblob-endpoint", diff --git a/src/cli/src/data/export_v2.rs b/src/cli/src/data/export_v2.rs new file mode 100644 index 0000000000..9bdf3c63f5 --- /dev/null +++ b/src/cli/src/data/export_v2.rs @@ -0,0 +1,52 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Export V2 module. +//! +//! This module provides the V2 implementation of database export functionality, +//! featuring: +//! - JSON-based schema export (version-agnostic) +//! - Manifest-based snapshot management +//! - Support for multiple storage backends (S3, OSS, GCS, Azure Blob, local FS) +//! - Resume capability for interrupted exports +//! +//! # Example +//! +//! ```bash +//! # Export schema only +//! greptime cli data export-v2 create \ +//! --addr 127.0.0.1:4000 \ +//! --to file:///tmp/snapshot \ +//! --schema-only +//! +//! # Export with time range +//! greptime cli data export-v2 create \ +//! --addr 127.0.0.1:4000 \ +//! --to s3://bucket/snapshots/prod-20250101 \ +//! --start-time 2025-01-01T00:00:00Z \ +//! --end-time 2025-01-31T23:59:59Z +//! ``` + +mod chunker; +mod command; +mod coordinator; +pub(crate) mod data; +pub mod error; +pub mod extractor; +pub mod manifest; +pub mod schema; +pub use command::ExportV2Command; + +#[cfg(test)] +mod tests; diff --git a/src/cli/src/data/export_v2/chunker.rs b/src/cli/src/data/export_v2/chunker.rs new file mode 100644 index 0000000000..260d95fae9 --- /dev/null +++ b/src/cli/src/data/export_v2/chunker.rs @@ -0,0 +1,103 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::time::Duration; + +use chrono::Duration as ChronoDuration; + +use crate::data::export_v2::manifest::{ChunkMeta, TimeRange}; + +pub fn generate_chunks(time_range: &TimeRange, window: Duration) -> Vec { + let (Some(start), Some(end)) = (time_range.start, time_range.end) else { + return vec![ChunkMeta::new(1, time_range.clone())]; + }; + + if start == end { + return vec![ChunkMeta::skipped(1, time_range.clone())]; + } + + if start > end { + return Vec::new(); + } + + let window = match ChronoDuration::from_std(window) { + Ok(window) if window > ChronoDuration::zero() => window, + _ => return vec![ChunkMeta::new(1, time_range.clone())], + }; + + let mut chunks = Vec::new(); + let mut cursor = start; + let mut id = 1; + + while cursor < end { + let next = cursor + .checked_add_signed(window) + .map_or(end, |timestamp| timestamp.min(end)); + chunks.push(ChunkMeta::new(id, TimeRange::new(Some(cursor), Some(next)))); + id += 1; + cursor = next; + } + + chunks +} + +#[cfg(test)] +mod tests { + use chrono::{TimeZone, Utc}; + + use super::*; + use crate::data::export_v2::manifest::ChunkStatus; + + #[test] + fn test_generate_chunks_unbounded() { + let range = TimeRange::unbounded(); + let chunks = generate_chunks(&range, Duration::from_secs(3600)); + assert_eq!(chunks.len(), 1); + assert_eq!(chunks[0].time_range, range); + } + + #[test] + fn test_generate_chunks_split() { + let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(); + let end = Utc.with_ymd_and_hms(2025, 1, 1, 3, 0, 0).unwrap(); + let range = TimeRange::new(Some(start), Some(end)); + + let chunks = generate_chunks(&range, Duration::from_secs(3600)); + assert_eq!(chunks.len(), 3); + assert_eq!(chunks[0].time_range.start, Some(start)); + assert_eq!( + chunks[2].time_range.end, + Some(Utc.with_ymd_and_hms(2025, 1, 1, 3, 0, 0).unwrap()) + ); + } + + #[test] + fn test_generate_chunks_empty_range() { + let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(); + let range = TimeRange::new(Some(start), Some(start)); + let chunks = generate_chunks(&range, Duration::from_secs(3600)); + assert_eq!(chunks.len(), 1); + assert_eq!(chunks[0].status, ChunkStatus::Skipped); + assert_eq!(chunks[0].time_range, range); + } + + #[test] + fn test_generate_chunks_invalid_range_is_empty() { + let start = Utc.with_ymd_and_hms(2025, 1, 1, 1, 0, 0).unwrap(); + let end = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(); + let range = TimeRange::new(Some(start), Some(end)); + let chunks = generate_chunks(&range, Duration::from_secs(3600)); + assert!(chunks.is_empty()); + } +} diff --git a/src/cli/src/data/export_v2/command.rs b/src/cli/src/data/export_v2/command.rs new file mode 100644 index 0000000000..a7da6d3862 --- /dev/null +++ b/src/cli/src/data/export_v2/command.rs @@ -0,0 +1,1194 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Export V2 CLI commands. + +use std::collections::HashSet; +use std::time::Duration; + +use async_trait::async_trait; +use clap::{Parser, Subcommand}; +use common_error::ext::BoxedError; +use common_telemetry::info; +use serde_json::Value; +use snafu::{OptionExt, ResultExt}; + +use crate::Tool; +use crate::common::ObjectStoreConfig; +use crate::data::export_v2::coordinator::export_data; +use crate::data::export_v2::error::{ + ChunkTimeWindowRequiresBoundsSnafu, DatabaseSnafu, EmptyResultSnafu, + ManifestVersionMismatchSnafu, Result, ResumeConfigMismatchSnafu, SchemaOnlyArgsNotAllowedSnafu, + SchemaOnlyModeMismatchSnafu, UnexpectedValueTypeSnafu, +}; +use crate::data::export_v2::extractor::SchemaExtractor; +use crate::data::export_v2::manifest::{ + ChunkMeta, DataFormat, MANIFEST_FILE, MANIFEST_VERSION, Manifest, TimeRange, +}; +use crate::data::path::ddl_path_for_schema; +use crate::data::snapshot_storage::{OpenDalStorage, SnapshotStorage, validate_uri}; +use crate::data::sql::{escape_sql_identifier, escape_sql_literal}; +use crate::database::{DatabaseClient, parse_proxy_opts}; + +/// Export V2 commands. +#[derive(Debug, Subcommand)] +pub enum ExportV2Command { + /// Create a new snapshot. + Create(ExportCreateCommand), + /// List snapshots under a parent location. + List(ExportListCommand), +} + +impl ExportV2Command { + pub async fn build(&self) -> std::result::Result, BoxedError> { + match self { + ExportV2Command::Create(cmd) => cmd.build().await, + ExportV2Command::List(cmd) => cmd.build().await, + } + } +} + +/// List snapshots under a parent location. +#[derive(Debug, Parser)] +pub struct ExportListCommand { + /// Parent storage location whose direct subdirectories are snapshots. + #[clap(long)] + location: String, + + /// Object store configuration for remote storage backends. + #[clap(flatten)] + storage: ObjectStoreConfig, +} + +impl ExportListCommand { + pub async fn build(&self) -> std::result::Result, BoxedError> { + validate_uri(&self.location).map_err(BoxedError::new)?; + let storage = OpenDalStorage::from_parent_uri(&self.location, &self.storage) + .map_err(BoxedError::new)?; + + Ok(Box::new(ExportList { + location: self.location.clone(), + storage, + })) + } +} + +/// Export list tool implementation. +pub struct ExportList { + location: String, + storage: OpenDalStorage, +} + +#[async_trait] +impl Tool for ExportList { + async fn do_work(&self) -> std::result::Result<(), BoxedError> { + self.run().await.map_err(BoxedError::new) + } +} + +impl ExportList { + async fn run(&self) -> Result<()> { + let result = scan_snapshots(&self.storage).await?; + + println!("Scanning: {}", self.location); + if result.snapshots.is_empty() { + println!("No snapshots found."); + } else { + print_snapshot_list(&result.snapshots, result.unreadable.len()); + } + print_unreadable_warnings(&result.unreadable); + + Ok(()) + } +} + +/// Create a new snapshot. +#[derive(Debug, Parser)] +pub struct ExportCreateCommand { + /// Server address to connect (e.g., 127.0.0.1:4000). + #[clap(long)] + addr: String, + + /// Target storage location (e.g., s3://bucket/path, file:///tmp/backup). + #[clap(long)] + to: String, + + /// Catalog name. + #[clap(long, default_value = "greptime")] + catalog: String, + + /// Schema list to export (default: all non-system schemas). + /// Can be specified multiple times or comma-separated. + #[clap(long, value_delimiter = ',')] + schemas: Vec, + + /// Export schema only, no data. + #[clap(long)] + schema_only: bool, + + /// Time range start (ISO 8601 format, e.g., 2024-01-01T00:00:00Z). + #[clap(long)] + start_time: Option, + + /// Time range end (ISO 8601 format, e.g., 2024-12-31T23:59:59Z). + #[clap(long)] + end_time: Option, + + /// Chunk time window (e.g., 1h, 6h, 1d, 7d). + /// Requires both --start-time and --end-time when specified. + #[clap(long, value_parser = humantime::parse_duration)] + chunk_time_window: Option, + + /// Data format: parquet, csv, json. + #[clap(long, value_enum, default_value = "parquet")] + format: DataFormat, + + /// Delete existing snapshot and recreate. + #[clap(long)] + force: bool, + + /// Parallelism for COPY DATABASE execution (server-side, per schema per chunk). + #[clap(long, default_value = "1")] + parallelism: usize, + + /// Basic authentication (user:password). + #[clap(long)] + auth_basic: Option, + + /// Request timeout. + #[clap(long, value_parser = humantime::parse_duration)] + timeout: Option, + + /// Proxy server address. + /// + /// If set, it overrides the system proxy unless `--no-proxy` is specified. + /// If neither `--proxy` nor `--no-proxy` is set, system proxy (env) may be used. + #[clap(long)] + proxy: Option, + + /// Disable all proxy usage (ignores `--proxy` and system proxy). + /// + /// When set and `--proxy` is not provided, this explicitly disables system proxy. + #[clap(long)] + no_proxy: bool, + + /// Object store configuration for remote storage backends. + #[clap(flatten)] + storage: ObjectStoreConfig, +} + +impl ExportCreateCommand { + pub async fn build(&self) -> std::result::Result, BoxedError> { + // Validate URI format + validate_uri(&self.to).map_err(BoxedError::new)?; + + let time_range = TimeRange::parse(self.start_time.as_deref(), self.end_time.as_deref()) + .map_err(BoxedError::new)?; + if self.chunk_time_window.is_some() && !time_range.is_bounded() { + return ChunkTimeWindowRequiresBoundsSnafu + .fail() + .map_err(BoxedError::new); + } + if self.schema_only { + let mut invalid_args = Vec::new(); + if self.start_time.is_some() { + invalid_args.push("--start-time"); + } + if self.end_time.is_some() { + invalid_args.push("--end-time"); + } + if self.chunk_time_window.is_some() { + invalid_args.push("--chunk-time-window"); + } + if self.format != DataFormat::Parquet { + invalid_args.push("--format"); + } + if self.parallelism != 1 { + invalid_args.push("--parallelism"); + } + if !invalid_args.is_empty() { + return SchemaOnlyArgsNotAllowedSnafu { + args: invalid_args.join(", "), + } + .fail() + .map_err(BoxedError::new); + } + } + + // Parse schemas (empty vec means all schemas) + let schemas = if self.schemas.is_empty() { + None + } else { + Some(self.schemas.clone()) + }; + + // Build storage + let storage = OpenDalStorage::from_uri(&self.to, &self.storage).map_err(BoxedError::new)?; + + // Build database client + let proxy = parse_proxy_opts(self.proxy.clone(), self.no_proxy)?; + let database_client = DatabaseClient::new( + self.addr.clone(), + self.catalog.clone(), + self.auth_basic.clone(), + self.timeout.unwrap_or(Duration::from_secs(60)), + proxy, + self.no_proxy, + ); + + Ok(Box::new(ExportCreate { + config: ExportConfig { + catalog: self.catalog.clone(), + schemas, + schema_only: self.schema_only, + format: self.format, + force: self.force, + time_range, + chunk_time_window: self.chunk_time_window, + parallelism: self.parallelism, + snapshot_uri: self.to.clone(), + storage_config: self.storage.clone(), + }, + storage: Box::new(storage), + database_client, + })) + } +} + +/// Export tool implementation. +pub struct ExportCreate { + config: ExportConfig, + storage: Box, + database_client: DatabaseClient, +} + +struct ExportConfig { + catalog: String, + schemas: Option>, + schema_only: bool, + format: DataFormat, + force: bool, + time_range: TimeRange, + chunk_time_window: Option, + parallelism: usize, + snapshot_uri: String, + storage_config: ObjectStoreConfig, +} + +#[async_trait] +impl Tool for ExportCreate { + async fn do_work(&self) -> std::result::Result<(), BoxedError> { + self.run().await.map_err(BoxedError::new) + } +} + +impl ExportCreate { + async fn run(&self) -> Result<()> { + // 1. Check if snapshot exists + let exists = self.storage.exists().await?; + + if exists { + if self.config.force { + info!("Deleting existing snapshot (--force)"); + self.storage.delete_snapshot().await?; + } else { + // Resume mode - read existing manifest + let mut manifest = self.storage.read_manifest().await?; + + // Check version compatibility + if manifest.version != MANIFEST_VERSION { + return ManifestVersionMismatchSnafu { + expected: MANIFEST_VERSION, + found: manifest.version, + } + .fail(); + } + + validate_resume_config(&manifest, &self.config)?; + + info!( + "Resuming existing snapshot: {} (completed: {}/{} chunks)", + manifest.snapshot_id, + manifest.completed_count(), + manifest.chunks.len() + ); + + if manifest.is_complete() { + info!("Snapshot is already complete"); + return Ok(()); + } + + if manifest.schema_only { + return Ok(()); + } + + export_data( + self.storage.as_ref(), + &self.database_client, + &self.config.snapshot_uri, + &self.config.storage_config, + &mut manifest, + self.config.parallelism, + ) + .await?; + return Ok(()); + } + } + + // 2. Get schema list + let extractor = SchemaExtractor::new(&self.database_client, &self.config.catalog); + let schema_snapshot = extractor.extract(self.config.schemas.as_deref()).await?; + + let schema_names: Vec = schema_snapshot + .schemas + .iter() + .map(|s| s.name.clone()) + .collect(); + info!("Exporting schemas: {:?}", schema_names); + + // 3. Create manifest + let mut manifest = Manifest::new_for_export( + self.config.catalog.clone(), + schema_names.clone(), + self.config.schema_only, + self.config.time_range.clone(), + self.config.format, + self.config.chunk_time_window, + )?; + + // 4. Write schema files + self.storage.write_schema(&schema_snapshot).await?; + info!("Exported {} schemas", schema_snapshot.schemas.len()); + + // 5. Export DDL files for import recovery. + let ddl_by_schema = self.build_ddl_by_schema(&schema_names).await?; + for (schema, ddl) in ddl_by_schema { + let ddl_path = ddl_path_for_schema(&schema); + self.storage.write_text(&ddl_path, &ddl).await?; + info!("Exported DDL for schema {} to {}", schema, ddl_path); + } + + // 6. Write manifest after schema artifacts and before any data export. + // + // The manifest is the snapshot commit point: only write it after the schema + // index and all DDL files are durable, so a crash cannot leave a "valid" + // snapshot that is missing required schema artifacts. For full exports we + // still need the manifest before data copy starts, because chunk resume is + // tracked by updating this manifest in place. + self.storage.write_manifest(&manifest).await?; + info!("Snapshot created: {}", manifest.snapshot_id); + + if !self.config.schema_only { + export_data( + self.storage.as_ref(), + &self.database_client, + &self.config.snapshot_uri, + &self.config.storage_config, + &mut manifest, + self.config.parallelism, + ) + .await?; + } + + Ok(()) + } + + async fn build_ddl_by_schema(&self, schema_names: &[String]) -> Result> { + let mut schemas = schema_names.to_vec(); + schemas.sort(); + + let mut ddl_by_schema = Vec::with_capacity(schemas.len()); + for schema in schemas { + let create_database = self.show_create("DATABASE", &schema, None).await?; + + let (mut physical_tables, mut tables, mut views) = + self.get_schema_objects(&schema).await?; + physical_tables.sort(); + let mut physical_ddls = Vec::with_capacity(physical_tables.len()); + for table in physical_tables { + physical_ddls.push(self.show_create("TABLE", &schema, Some(&table)).await?); + } + + tables.sort(); + let mut table_ddls = Vec::with_capacity(tables.len()); + for table in tables { + table_ddls.push(self.show_create("TABLE", &schema, Some(&table)).await?); + } + + views.sort(); + let mut view_ddls = Vec::with_capacity(views.len()); + for view in views { + view_ddls.push(self.show_create("VIEW", &schema, Some(&view)).await?); + } + + let ddl = build_schema_ddl( + &schema, + create_database, + physical_ddls, + table_ddls, + view_ddls, + ); + ddl_by_schema.push((schema, ddl)); + } + + Ok(ddl_by_schema) + } + + async fn get_schema_objects( + &self, + schema: &str, + ) -> Result<(Vec, Vec, Vec)> { + let physical_tables = self.get_metric_physical_tables(schema).await?; + let physical_set: HashSet<&str> = physical_tables.iter().map(String::as_str).collect(); + let sql = format!( + "SELECT table_name, table_type FROM information_schema.tables \ + WHERE table_catalog = '{}' AND table_schema = '{}' \ + AND (table_type = 'BASE TABLE' OR table_type = 'VIEW')", + escape_sql_literal(&self.config.catalog), + escape_sql_literal(schema) + ); + let records: Option>> = self + .database_client + .sql_in_public(&sql) + .await + .context(DatabaseSnafu)?; + + let mut tables = Vec::new(); + let mut views = Vec::new(); + if let Some(rows) = records { + for row in rows { + let name = match row.first() { + Some(Value::String(name)) => name.clone(), + _ => return UnexpectedValueTypeSnafu.fail(), + }; + let table_type = match row.get(1) { + Some(Value::String(table_type)) => table_type.as_str(), + _ => return UnexpectedValueTypeSnafu.fail(), + }; + if !physical_set.contains(name.as_str()) { + if table_type == "VIEW" { + views.push(name); + } else { + tables.push(name); + } + } + } + } + + Ok((physical_tables, tables, views)) + } + + async fn get_metric_physical_tables(&self, schema: &str) -> Result> { + let sql = format!( + "SELECT DISTINCT table_name FROM information_schema.columns \ + WHERE table_catalog = '{}' AND table_schema = '{}' AND column_name = '__tsid'", + escape_sql_literal(&self.config.catalog), + escape_sql_literal(schema) + ); + let records: Option>> = self + .database_client + .sql_in_public(&sql) + .await + .context(DatabaseSnafu)?; + + let mut tables = HashSet::new(); + if let Some(rows) = records { + for row in rows { + let name = match row.first() { + Some(Value::String(name)) => name.clone(), + _ => return UnexpectedValueTypeSnafu.fail(), + }; + tables.insert(name); + } + } + + Ok(tables.into_iter().collect()) + } + + async fn show_create( + &self, + show_type: &str, + schema: &str, + table: Option<&str>, + ) -> Result { + let sql = match table { + Some(table) => format!( + r#"SHOW CREATE {} "{}"."{}"."{}""#, + show_type, + escape_sql_identifier(&self.config.catalog), + escape_sql_identifier(schema), + escape_sql_identifier(table) + ), + None => format!( + r#"SHOW CREATE {} "{}"."{}""#, + show_type, + escape_sql_identifier(&self.config.catalog), + escape_sql_identifier(schema) + ), + }; + + let records: Option>> = self + .database_client + .sql_in_public(&sql) + .await + .context(DatabaseSnafu)?; + let rows = records.context(EmptyResultSnafu)?; + let row = rows.first().context(EmptyResultSnafu)?; + let Some(Value::String(create)) = row.get(1) else { + return UnexpectedValueTypeSnafu.fail(); + }; + + Ok(format!("{};\n", create)) + } +} + +fn build_schema_ddl( + schema: &str, + create_database: String, + physical_tables: Vec, + tables: Vec, + views: Vec, +) -> String { + let mut ddl = String::new(); + ddl.push_str(&format!("-- Schema: {}\n", schema)); + ddl.push_str(&create_database); + for stmt in physical_tables { + ddl.push_str(&stmt); + } + for stmt in tables { + ddl.push_str(&stmt); + } + for stmt in views { + ddl.push_str(&stmt); + } + ddl.push('\n'); + ddl +} + +fn validate_resume_config(manifest: &Manifest, config: &ExportConfig) -> Result<()> { + if manifest.schema_only != config.schema_only { + return SchemaOnlyModeMismatchSnafu { + existing_schema_only: manifest.schema_only, + requested_schema_only: config.schema_only, + } + .fail(); + } + + if manifest.catalog != config.catalog { + return ResumeConfigMismatchSnafu { + field: "catalog", + existing: manifest.catalog.clone(), + requested: config.catalog.clone(), + } + .fail(); + } + + // If no schema filter is provided on resume, inherit the existing snapshot + // selection instead of reinterpreting the request as "all schemas". + if let Some(requested_schemas) = &config.schemas + && !schema_selection_matches(&manifest.schemas, requested_schemas) + { + return ResumeConfigMismatchSnafu { + field: "schemas", + existing: format_schema_selection(&manifest.schemas), + requested: format_schema_selection(requested_schemas), + } + .fail(); + } + + if manifest.time_range != config.time_range { + return ResumeConfigMismatchSnafu { + field: "time_range", + existing: format!("{:?}", manifest.time_range), + requested: format!("{:?}", config.time_range), + } + .fail(); + } + + if manifest.format != config.format { + return ResumeConfigMismatchSnafu { + field: "format", + existing: manifest.format.to_string(), + requested: config.format.to_string(), + } + .fail(); + } + + let expected_plan = Manifest::new_for_export( + manifest.catalog.clone(), + manifest.schemas.clone(), + config.schema_only, + config.time_range.clone(), + config.format, + config.chunk_time_window, + )?; + if !chunk_plan_matches(manifest, &expected_plan) { + return ResumeConfigMismatchSnafu { + field: "chunk plan", + existing: format_chunk_plan(&manifest.chunks), + requested: format_chunk_plan(&expected_plan.chunks), + } + .fail(); + } + + Ok(()) +} + +fn schema_selection_matches(existing: &[String], requested: &[String]) -> bool { + canonical_schema_selection(existing) == canonical_schema_selection(requested) +} + +fn canonical_schema_selection(schemas: &[String]) -> Vec { + let mut canonicalized = Vec::new(); + let mut seen = HashSet::new(); + + for schema in schemas { + let normalized = schema.to_ascii_lowercase(); + if seen.insert(normalized.clone()) { + canonicalized.push(normalized); + } + } + + canonicalized.sort(); + canonicalized +} + +fn format_schema_selection(schemas: &[String]) -> String { + format!("[{}]", schemas.join(", ")) +} + +fn chunk_plan_matches(existing: &Manifest, expected: &Manifest) -> bool { + existing.chunks.len() == expected.chunks.len() + && existing + .chunks + .iter() + .zip(&expected.chunks) + .all(|(left, right)| left.id == right.id && left.time_range == right.time_range) +} + +fn format_chunk_plan(chunks: &[ChunkMeta]) -> String { + let items = chunks + .iter() + .map(|chunk| format!("#{}:{:?}", chunk.id, chunk.time_range)) + .collect::>(); + format!("[{}]", items.join(", ")) +} + +#[derive(Debug)] +struct SnapshotListEntry { + path: String, + manifest: Manifest, +} + +#[derive(Debug, Default)] +struct SnapshotScanResult { + snapshots: Vec, + unreadable: Vec, +} + +async fn scan_snapshots(storage: &OpenDalStorage) -> Result { + let mut result = SnapshotScanResult::default(); + for dir in storage.list_direct_child_dirs().await? { + let manifest_path = format!("{}/{}", dir.trim_matches('/'), MANIFEST_FILE); + let Some(data) = storage.read_file_if_exists(&manifest_path).await? else { + continue; + }; + + match serde_json::from_slice::(&data) { + Ok(manifest) => result.snapshots.push(SnapshotListEntry { + path: format!("{}/", dir.trim_matches('/')), + manifest, + }), + Err(_) => result + .unreadable + .push(format!("{}/", dir.trim_matches('/'))), + } + } + + result + .snapshots + .sort_by_key(|entry| std::cmp::Reverse(entry.manifest.created_at)); + result.unreadable.sort(); + Ok(result) +} + +fn print_snapshot_list(snapshots: &[SnapshotListEntry], unreadable_count: usize) { + if unreadable_count == 0 { + println!("Found {} snapshots:", snapshots.len()); + } else { + println!( + "Found {} snapshots ({} {} skipped: unreadable manifest):", + snapshots.len(), + unreadable_count, + directory_word(unreadable_count) + ); + } + println!(); + println!( + " {:<24} {:<36} {:<19} {:<9} {:<7} {:<6} Status", + "Path", "ID", "Created", "Catalog", "Schemas", "Chunks" + ); + println!( + " {:<24} {:<36} {:<19} {:<9} {:<7} {:<6} {:<10}", + "-".repeat(24), + "-".repeat(36), + "-".repeat(19), + "-".repeat(9), + "-".repeat(7), + "-".repeat(6), + "-".repeat(10) + ); + for entry in snapshots { + let manifest = &entry.manifest; + println!( + " {:<24} {:<36} {:<19} {:<9} {:<7} {:<6} {}", + entry.path, + manifest.snapshot_id, + manifest.created_at.format("%Y-%m-%d %H:%M:%S"), + manifest.catalog, + manifest.schemas.len(), + format_list_chunks(manifest), + snapshot_status(manifest) + ); + } +} + +fn print_unreadable_warnings(unreadable: &[String]) { + if unreadable.is_empty() { + return; + } + + println!(); + println!( + "Warning: {} {} had corrupt/unreadable manifest.json:", + unreadable.len(), + directory_word(unreadable.len()) + ); + for path in unreadable { + println!(" - {}", path); + } +} + +fn directory_word(count: usize) -> &'static str { + if count == 1 { + "directory" + } else { + "directories" + } +} + +fn snapshot_status(manifest: &Manifest) -> &'static str { + if manifest.schema_only { + "schema-only" + } else if manifest.is_complete() { + "complete" + } else { + "incomplete" + } +} + +fn format_list_chunks(manifest: &Manifest) -> String { + let total = manifest.chunks.len(); + if total == 0 { + return "0".to_string(); + } + + format!( + "{}/{}", + manifest.completed_count() + manifest.skipped_count(), + total + ) +} + +#[cfg(test)] +mod tests { + use chrono::TimeZone; + use clap::Parser; + use tempfile::tempdir; + use url::Url; + + use super::*; + use crate::data::path::ddl_path_for_schema; + + #[test] + fn test_ddl_path_for_schema() { + assert_eq!(ddl_path_for_schema("public"), "schema/ddl/public.sql"); + assert_eq!( + ddl_path_for_schema("../evil"), + "schema/ddl/%2E%2E%2Fevil.sql" + ); + } + + #[test] + fn test_build_schema_ddl_order() { + let ddl = build_schema_ddl( + "public", + "CREATE DATABASE public;\n".to_string(), + vec!["PHYSICAL;\n".to_string()], + vec!["TABLE;\n".to_string()], + vec!["VIEW;\n".to_string()], + ); + + let db_pos = ddl.find("CREATE DATABASE").unwrap(); + let physical_pos = ddl.find("PHYSICAL;").unwrap(); + let table_pos = ddl.find("TABLE;").unwrap(); + let view_pos = ddl.find("VIEW;").unwrap(); + assert!(db_pos < physical_pos); + assert!(physical_pos < table_pos); + assert!(table_pos < view_pos); + } + + #[tokio::test] + async fn test_build_rejects_chunk_window_without_bounds() { + let cmd = ExportCreateCommand::parse_from([ + "export-v2-create", + "--addr", + "127.0.0.1:4000", + "--to", + "file:///tmp/export-v2-test", + "--chunk-time-window", + "1h", + ]); + + let result = cmd.build().await; + assert!(result.is_err()); + let error = result.err().unwrap().to_string(); + + assert!(error.contains("chunk_time_window requires both --start-time and --end-time")); + } + + #[tokio::test] + async fn test_build_rejects_data_export_args_in_schema_only_mode() { + let cmd = ExportCreateCommand::parse_from([ + "export-v2-create", + "--addr", + "127.0.0.1:4000", + "--to", + "file:///tmp/export-v2-test", + "--schema-only", + "--start-time", + "2024-01-01T00:00:00Z", + "--end-time", + "2024-01-02T00:00:00Z", + "--chunk-time-window", + "1h", + "--format", + "csv", + "--parallelism", + "2", + ]); + + let error = cmd.build().await.err().unwrap().to_string(); + + assert!(error.contains("--schema-only cannot be used with data export arguments")); + assert!(error.contains("--start-time")); + assert!(error.contains("--end-time")); + assert!(error.contains("--chunk-time-window")); + assert!(error.contains("--format")); + assert!(error.contains("--parallelism")); + } + + #[test] + fn test_schema_only_mode_mismatch_error_message() { + let error = crate::data::export_v2::error::SchemaOnlyModeMismatchSnafu { + existing_schema_only: false, + requested_schema_only: true, + } + .build() + .to_string(); + + assert!(error.contains("existing: false")); + assert!(error.contains("requested: true")); + } + + #[test] + fn test_validate_resume_config_rejects_catalog_mismatch() { + let manifest = Manifest::new_for_export( + "greptime".to_string(), + vec!["public".to_string()], + false, + TimeRange::unbounded(), + DataFormat::Parquet, + None, + ) + .unwrap(); + let config = ExportConfig { + catalog: "other".to_string(), + schemas: None, + schema_only: false, + format: DataFormat::Parquet, + force: false, + time_range: TimeRange::unbounded(), + chunk_time_window: None, + parallelism: 1, + snapshot_uri: "file:///tmp/snapshot".to_string(), + storage_config: ObjectStoreConfig::default(), + }; + + let error = validate_resume_config(&manifest, &config) + .err() + .unwrap() + .to_string(); + assert!(error.contains("catalog")); + } + + #[test] + fn test_validate_resume_config_accepts_schema_selection_with_different_case_and_order() { + let manifest = Manifest::new_for_export( + "greptime".to_string(), + vec!["public".to_string(), "analytics".to_string()], + false, + TimeRange::unbounded(), + DataFormat::Parquet, + None, + ) + .unwrap(); + let config = ExportConfig { + catalog: "greptime".to_string(), + schemas: Some(vec![ + "ANALYTICS".to_string(), + "PUBLIC".to_string(), + "public".to_string(), + ]), + schema_only: false, + format: DataFormat::Parquet, + force: false, + time_range: TimeRange::unbounded(), + chunk_time_window: None, + parallelism: 1, + snapshot_uri: "file:///tmp/snapshot".to_string(), + storage_config: ObjectStoreConfig::default(), + }; + + assert!(validate_resume_config(&manifest, &config).is_ok()); + } + + #[test] + fn test_validate_resume_config_rejects_chunk_plan_mismatch() { + let start = chrono::Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(); + let end = chrono::Utc.with_ymd_and_hms(2025, 1, 1, 2, 0, 0).unwrap(); + let time_range = TimeRange::new(Some(start), Some(end)); + let manifest = Manifest::new_for_export( + "greptime".to_string(), + vec!["public".to_string()], + false, + time_range.clone(), + DataFormat::Parquet, + None, + ) + .unwrap(); + let config = ExportConfig { + catalog: "greptime".to_string(), + schemas: None, + schema_only: false, + format: DataFormat::Parquet, + force: false, + time_range, + chunk_time_window: Some(Duration::from_secs(3600)), + parallelism: 1, + snapshot_uri: "file:///tmp/snapshot".to_string(), + storage_config: ObjectStoreConfig::default(), + }; + + let error = validate_resume_config(&manifest, &config) + .err() + .unwrap() + .to_string(); + assert!(error.contains("chunk plan")); + } + + #[test] + fn test_validate_resume_config_rejects_format_mismatch() { + let manifest = Manifest::new_for_export( + "greptime".to_string(), + vec!["public".to_string()], + false, + TimeRange::unbounded(), + DataFormat::Parquet, + None, + ) + .unwrap(); + let config = ExportConfig { + catalog: "greptime".to_string(), + schemas: None, + schema_only: false, + format: DataFormat::Csv, + force: false, + time_range: TimeRange::unbounded(), + chunk_time_window: None, + parallelism: 1, + snapshot_uri: "file:///tmp/snapshot".to_string(), + storage_config: ObjectStoreConfig::default(), + }; + + let error = validate_resume_config(&manifest, &config) + .err() + .unwrap() + .to_string(); + assert!(error.contains("format")); + } + + #[test] + fn test_validate_resume_config_rejects_time_range_mismatch() { + let start = chrono::Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(); + let end = chrono::Utc.with_ymd_and_hms(2025, 1, 1, 1, 0, 0).unwrap(); + let manifest = Manifest::new_for_export( + "greptime".to_string(), + vec!["public".to_string()], + false, + TimeRange::new(Some(start), Some(end)), + DataFormat::Parquet, + None, + ) + .unwrap(); + let config = ExportConfig { + catalog: "greptime".to_string(), + schemas: None, + schema_only: false, + format: DataFormat::Parquet, + force: false, + time_range: TimeRange::new(Some(start), Some(start)), + chunk_time_window: None, + parallelism: 1, + snapshot_uri: "file:///tmp/snapshot".to_string(), + storage_config: ObjectStoreConfig::default(), + }; + + let error = validate_resume_config(&manifest, &config) + .err() + .unwrap() + .to_string(); + assert!(error.contains("time_range")); + } + + #[tokio::test] + async fn test_scan_snapshots_sorts_and_tracks_unreadable_manifests() { + let dir = tempdir().unwrap(); + write_test_manifest( + dir.path(), + "older", + test_manifest( + chrono::Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(), + false, + true, + ), + ); + write_test_manifest( + dir.path(), + "newer", + test_manifest( + chrono::Utc.with_ymd_and_hms(2026, 2, 1, 0, 0, 0).unwrap(), + false, + true, + ), + ); + + std::fs::create_dir_all(dir.path().join("empty-dir")).unwrap(); + std::fs::create_dir_all(dir.path().join("not-snapshot")).unwrap(); + std::fs::write(dir.path().join("not-snapshot").join("data.txt"), "x").unwrap(); + std::fs::create_dir_all(dir.path().join("broken")).unwrap(); + std::fs::write(dir.path().join("broken").join(MANIFEST_FILE), "{not-json").unwrap(); + + let uri = Url::from_directory_path(dir.path()).unwrap().to_string(); + let storage = OpenDalStorage::from_file_uri(&uri).unwrap(); + let result = scan_snapshots(&storage).await.unwrap(); + + assert_eq!(result.snapshots.len(), 2); + assert_eq!( + result.snapshots[0].manifest.created_at, + chrono::Utc.with_ymd_and_hms(2026, 2, 1, 0, 0, 0).unwrap() + ); + assert_eq!( + result.snapshots[1].manifest.created_at, + chrono::Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap() + ); + assert_eq!(result.unreadable, vec!["broken/".to_string()]); + assert_eq!(result.snapshots[0].path, "newer/"); + assert_eq!(result.snapshots[1].path, "older/"); + } + + #[test] + fn test_snapshot_list_status_and_chunk_summary() { + let schema_only = test_manifest( + chrono::Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(), + true, + true, + ); + assert_eq!(snapshot_status(&schema_only), "schema-only"); + assert_eq!(format_list_chunks(&schema_only), "0"); + + let complete = test_manifest( + chrono::Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(), + false, + true, + ); + assert_eq!(snapshot_status(&complete), "complete"); + assert_eq!(format_list_chunks(&complete), "2/2"); + + let incomplete = test_manifest( + chrono::Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(), + false, + false, + ); + assert_eq!(snapshot_status(&incomplete), "incomplete"); + assert_eq!(format_list_chunks(&incomplete), "1/2"); + } + + fn write_test_manifest(root: &std::path::Path, dir: &str, manifest: Manifest) { + let snapshot_dir = root.join(dir); + std::fs::create_dir_all(&snapshot_dir).unwrap(); + std::fs::write( + snapshot_dir.join(MANIFEST_FILE), + serde_json::to_vec_pretty(&manifest).unwrap(), + ) + .unwrap(); + } + + fn test_manifest( + created_at: chrono::DateTime, + schema_only: bool, + complete: bool, + ) -> Manifest { + let mut manifest = Manifest::new_for_export( + "greptime".to_string(), + vec!["public".to_string(), "analytics".to_string()], + schema_only, + TimeRange::unbounded(), + DataFormat::Parquet, + None, + ) + .unwrap(); + manifest.created_at = created_at; + manifest.updated_at = created_at; + + if !schema_only { + manifest.chunks.clear(); + let mut first = ChunkMeta::new(1, TimeRange::unbounded()); + first.mark_completed(vec!["data/public/chunk_1/file.parquet".to_string()], None); + manifest.chunks.push(first); + + if complete { + manifest + .chunks + .push(ChunkMeta::skipped(2, TimeRange::unbounded())); + } else { + manifest + .chunks + .push(ChunkMeta::new(2, TimeRange::unbounded())); + } + } + + manifest + } +} diff --git a/src/cli/src/data/export_v2/coordinator.rs b/src/cli/src/data/export_v2/coordinator.rs new file mode 100644 index 0000000000..d96c01d693 --- /dev/null +++ b/src/cli/src/data/export_v2/coordinator.rs @@ -0,0 +1,166 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use common_telemetry::info; + +use crate::common::ObjectStoreConfig; +use crate::data::export_v2::data::{CopyOptions, build_copy_target, execute_copy_database}; +use crate::data::export_v2::error::Result; +use crate::data::export_v2::manifest::{ChunkStatus, DataFormat, Manifest, TimeRange}; +use crate::data::path::data_dir_for_schema_chunk; +use crate::data::snapshot_storage::{SnapshotStorage, StorageScheme}; +use crate::database::DatabaseClient; + +struct ExportContext<'a> { + storage: &'a dyn SnapshotStorage, + database_client: &'a DatabaseClient, + snapshot_uri: &'a str, + storage_config: &'a ObjectStoreConfig, + catalog: &'a str, + schemas: &'a [String], + format: DataFormat, + parallelism: usize, +} + +pub async fn export_data( + storage: &dyn SnapshotStorage, + database_client: &DatabaseClient, + snapshot_uri: &str, + storage_config: &ObjectStoreConfig, + manifest: &mut Manifest, + parallelism: usize, +) -> Result<()> { + if manifest.chunks.is_empty() { + return Ok(()); + } + + for idx in 0..manifest.chunks.len() { + if matches!( + manifest.chunks[idx].status, + ChunkStatus::Completed | ChunkStatus::Skipped + ) { + continue; + } + + let (chunk_id, time_range) = mark_chunk_in_progress(manifest, idx); + manifest.touch(); + storage.write_manifest(manifest).await?; + + let context = ExportContext { + storage, + database_client, + snapshot_uri, + storage_config, + catalog: &manifest.catalog, + schemas: &manifest.schemas, + format: manifest.format, + parallelism, + }; + let export_result = export_chunk(&context, chunk_id, time_range).await; + + let result = match export_result { + Ok(files) => { + mark_chunk_completed(manifest, idx, files); + Ok(()) + } + Err(err) => { + mark_chunk_failed(manifest, idx, err.to_string()); + Err(err) + } + }; + + manifest.touch(); + storage.write_manifest(manifest).await?; + + result?; + } + + Ok(()) +} + +fn mark_chunk_in_progress(manifest: &mut Manifest, idx: usize) -> (u32, TimeRange) { + let chunk = &mut manifest.chunks[idx]; + chunk.mark_in_progress(); + (chunk.id, chunk.time_range.clone()) +} + +fn mark_chunk_completed(manifest: &mut Manifest, idx: usize, files: Vec) { + let chunk = &mut manifest.chunks[idx]; + if files.is_empty() { + chunk.mark_skipped(); + } else { + chunk.mark_completed(files, None); + } +} + +fn mark_chunk_failed(manifest: &mut Manifest, idx: usize, error: String) { + let chunk = &mut manifest.chunks[idx]; + chunk.mark_failed(error); +} + +async fn export_chunk( + context: &ExportContext<'_>, + chunk_id: u32, + time_range: TimeRange, +) -> Result> { + let scheme = StorageScheme::from_uri(context.snapshot_uri)?; + let needs_dir = matches!(scheme, StorageScheme::File); + let copy_options = CopyOptions { + format: context.format, + time_range, + parallelism: context.parallelism, + }; + + for schema in context.schemas { + let prefix = data_dir_for_schema_chunk(schema, chunk_id); + if needs_dir { + context.storage.create_dir_all(&prefix).await?; + } + + let target = build_copy_target( + context.snapshot_uri, + context.storage_config, + schema, + chunk_id, + )?; + execute_copy_database( + context.database_client, + context.catalog, + schema, + &target, + ©_options, + ) + .await?; + } + + let files = list_chunk_files(context.storage, context.schemas, chunk_id).await?; + info!("Collected {} files for chunk {}", files.len(), chunk_id); + Ok(files) +} + +async fn list_chunk_files( + storage: &dyn SnapshotStorage, + schemas: &[String], + chunk_id: u32, +) -> Result> { + let mut files = Vec::new(); + + for schema in schemas { + let prefix = data_dir_for_schema_chunk(schema, chunk_id); + files.extend(storage.list_files_recursive(&prefix).await?); + } + + files.sort(); + Ok(files) +} diff --git a/src/cli/src/data/export_v2/data.rs b/src/cli/src/data/export_v2/data.rs new file mode 100644 index 0000000000..dce924a717 --- /dev/null +++ b/src/cli/src/data/export_v2/data.rs @@ -0,0 +1,538 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use common_base::secrets::{ExposeSecret, SecretString}; +use common_telemetry::info; +use object_store::util::{join_path, normalize_path}; +use snafu::ResultExt; +use url::Url; + +use crate::common::ObjectStoreConfig; +use crate::data::export_v2::error::{DatabaseSnafu, InvalidUriSnafu, Result, UrlParseSnafu}; +use crate::data::export_v2::manifest::{DataFormat, TimeRange}; +use crate::data::path::data_dir_for_schema_chunk; +use crate::data::snapshot_storage::StorageScheme; +use crate::data::sql::{escape_sql_identifier, escape_sql_literal}; +use crate::database::DatabaseClient; + +pub(super) struct CopyOptions { + pub(super) format: DataFormat, + pub(super) time_range: TimeRange, + pub(super) parallelism: usize, +} + +pub(super) struct CopyTarget { + pub(super) location: String, + pub(super) connection: String, + secrets: Vec>, +} + +pub(crate) struct CopySource { + pub(crate) location: String, + pub(crate) connection: String, + secrets: Vec>, +} + +impl CopyTarget { + fn mask_sql(&self, sql: &str) -> String { + mask_secrets(sql, &self.secrets) + } +} + +impl CopySource { + fn mask_sql(&self, sql: &str) -> String { + mask_secrets(sql, &self.secrets) + } +} + +pub(super) fn build_copy_target( + snapshot_uri: &str, + storage: &ObjectStoreConfig, + schema: &str, + chunk_id: u32, +) -> Result { + let location = build_copy_location(snapshot_uri, storage, schema, chunk_id)?; + Ok(CopyTarget { + location: location.location, + connection: location.connection, + secrets: location.secrets, + }) +} + +pub(crate) fn build_copy_source( + snapshot_uri: &str, + storage: &ObjectStoreConfig, + schema: &str, + chunk_id: u32, +) -> Result { + let location = build_copy_location(snapshot_uri, storage, schema, chunk_id)?; + Ok(CopySource { + location: location.location, + connection: location.connection, + secrets: location.secrets, + }) +} + +struct CopyLocation { + location: String, + connection: String, + secrets: Vec>, +} + +fn build_copy_location( + snapshot_uri: &str, + storage: &ObjectStoreConfig, + schema: &str, + chunk_id: u32, +) -> Result { + let url = Url::parse(snapshot_uri).context(UrlParseSnafu)?; + let scheme = StorageScheme::from_uri(snapshot_uri)?; + let suffix = data_dir_for_schema_chunk(schema, chunk_id); + + match scheme { + StorageScheme::File => { + let root = url.to_file_path().map_err(|_| { + InvalidUriSnafu { + uri: snapshot_uri, + reason: "file:// URI must use an absolute path like file:///tmp/backup", + } + .build() + })?; + let location = normalize_path(&format!("{}/{}", root.to_string_lossy(), suffix)); + Ok(CopyLocation { + location, + connection: String::new(), + secrets: Vec::new(), + }) + } + StorageScheme::S3 => { + let (bucket, root) = extract_bucket_root(&url, snapshot_uri)?; + let location = format!("s3://{}/{}", bucket, join_root(&root, &suffix)); + let (connection, secrets) = build_s3_connection(storage); + Ok(CopyLocation { + location, + connection, + secrets, + }) + } + StorageScheme::Oss => { + let (bucket, root) = extract_bucket_root(&url, snapshot_uri)?; + let location = format!("oss://{}/{}", bucket, join_root(&root, &suffix)); + let (connection, secrets) = build_oss_connection(storage); + Ok(CopyLocation { + location, + connection, + secrets, + }) + } + StorageScheme::Gcs => { + let (bucket, root) = extract_bucket_root(&url, snapshot_uri)?; + let location = format!("gcs://{}/{}", bucket, join_root(&root, &suffix)); + let (connection, secrets) = build_gcs_connection(storage, snapshot_uri)?; + Ok(CopyLocation { + location, + connection, + secrets, + }) + } + StorageScheme::Azblob => { + let (bucket, root) = extract_bucket_root(&url, snapshot_uri)?; + let location = format!("azblob://{}/{}", bucket, join_root(&root, &suffix)); + let (connection, secrets) = build_azblob_connection(storage); + Ok(CopyLocation { + location, + connection, + secrets, + }) + } + } +} + +pub(super) async fn execute_copy_database( + database_client: &DatabaseClient, + catalog: &str, + schema: &str, + target: &CopyTarget, + options: &CopyOptions, +) -> Result<()> { + let with_options = build_with_options(options); + let sql = format!( + r#"COPY DATABASE "{}"."{}" TO '{}' WITH ({}){};"#, + escape_sql_identifier(catalog), + escape_sql_identifier(schema), + escape_sql_literal(&target.location), + with_options, + target.connection + ); + let safe_sql = target.mask_sql(&sql); + info!("Executing sql: {}", safe_sql); + database_client + .sql_in_public(&sql) + .await + .context(DatabaseSnafu)?; + Ok(()) +} + +pub(crate) async fn execute_copy_database_from( + database_client: &DatabaseClient, + catalog: &str, + schema: &str, + source: &CopySource, + format: DataFormat, +) -> Result<()> { + let sql = format!( + r#"COPY DATABASE "{}"."{}" FROM '{}' WITH (FORMAT='{}'){};"#, + escape_sql_identifier(catalog), + escape_sql_identifier(schema), + escape_sql_literal(&source.location), + format, + source.connection + ); + let safe_sql = source.mask_sql(&sql); + info!("Executing sql: {}", safe_sql); + database_client + .sql_in_public(&sql) + .await + .context(DatabaseSnafu)?; + Ok(()) +} + +fn build_with_options(options: &CopyOptions) -> String { + let mut parts = vec![format!("FORMAT='{}'", options.format)]; + if let Some(start) = options.time_range.start { + parts.push(format!( + "START_TIME='{}'", + escape_sql_literal(&start.to_rfc3339()) + )); + } + if let Some(end) = options.time_range.end { + parts.push(format!( + "END_TIME='{}'", + escape_sql_literal(&end.to_rfc3339()) + )); + } + parts.push(format!("PARALLELISM={}", options.parallelism)); + parts.join(", ") +} + +fn extract_bucket_root(url: &Url, snapshot_uri: &str) -> Result<(String, String)> { + let bucket = url.host_str().unwrap_or("").to_string(); + if bucket.is_empty() { + return InvalidUriSnafu { + uri: snapshot_uri, + reason: "URI must include bucket/container in host", + } + .fail(); + } + let root = url + .path() + .trim_start_matches('/') + .trim_end_matches('/') + .to_string(); + Ok((bucket, root)) +} + +fn join_root(root: &str, suffix: &str) -> String { + join_path(root, suffix).trim_start_matches('/').to_string() +} + +fn build_s3_connection(storage: &ObjectStoreConfig) -> (String, Vec>) { + let access_key_id = expose_optional_secret(&storage.s3.s3_access_key_id); + let secret_access_key = expose_optional_secret(&storage.s3.s3_secret_access_key); + + let mut options = Vec::new(); + if let Some(access_key_id) = &access_key_id { + options.push(format!( + "ACCESS_KEY_ID='{}'", + escape_sql_literal(access_key_id) + )); + } + if let Some(secret_access_key) = &secret_access_key { + options.push(format!( + "SECRET_ACCESS_KEY='{}'", + escape_sql_literal(secret_access_key) + )); + } + if let Some(region) = &storage.s3.s3_region { + options.push(format!("REGION='{}'", escape_sql_literal(region))); + } + if let Some(endpoint) = &storage.s3.s3_endpoint { + options.push(format!("ENDPOINT='{}'", escape_sql_literal(endpoint))); + } + + let secrets = vec![access_key_id, secret_access_key]; + let connection = if options.is_empty() { + String::new() + } else { + format!(" CONNECTION ({})", options.join(", ")) + }; + (connection, secrets) +} + +fn build_oss_connection(storage: &ObjectStoreConfig) -> (String, Vec>) { + let access_key_id = expose_optional_secret(&storage.oss.oss_access_key_id); + let access_key_secret = expose_optional_secret(&storage.oss.oss_access_key_secret); + + let mut options = Vec::new(); + if let Some(access_key_id) = &access_key_id { + options.push(format!( + "ACCESS_KEY_ID='{}'", + escape_sql_literal(access_key_id) + )); + } + if let Some(access_key_secret) = &access_key_secret { + options.push(format!( + "ACCESS_KEY_SECRET='{}'", + escape_sql_literal(access_key_secret) + )); + } + if !storage.oss.oss_endpoint.is_empty() { + options.push(format!( + "ENDPOINT='{}'", + escape_sql_literal(&storage.oss.oss_endpoint) + )); + } + + let secrets = vec![access_key_id, access_key_secret]; + let connection = if options.is_empty() { + String::new() + } else { + format!(" CONNECTION ({})", options.join(", ")) + }; + (connection, secrets) +} + +fn build_gcs_connection( + storage: &ObjectStoreConfig, + snapshot_uri: &str, +) -> Result<(String, Vec>)> { + let credential_path = expose_optional_secret(&storage.gcs.gcs_credential_path); + let credential = expose_optional_secret(&storage.gcs.gcs_credential); + + if credential.is_none() && credential_path.is_some() { + return InvalidUriSnafu { + uri: snapshot_uri, + reason: "gcs_credential_path is not supported for server-side COPY; provide gcs_credential or rely on server-side ADC", + } + .fail(); + } + + let mut options = Vec::new(); + if let Some(credential) = &credential { + options.push(format!("CREDENTIAL='{}'", escape_sql_literal(credential))); + } + if !storage.gcs.gcs_scope.is_empty() { + options.push(format!( + "SCOPE='{}'", + escape_sql_literal(&storage.gcs.gcs_scope) + )); + } + if !storage.gcs.gcs_endpoint.is_empty() { + options.push(format!( + "ENDPOINT='{}'", + escape_sql_literal(&storage.gcs.gcs_endpoint) + )); + } + + let connection = if options.is_empty() { + String::new() + } else { + format!(" CONNECTION ({})", options.join(", ")) + }; + let secrets = vec![credential_path, credential]; + Ok((connection, secrets)) +} + +fn build_azblob_connection(storage: &ObjectStoreConfig) -> (String, Vec>) { + let account_name = expose_optional_secret(&storage.azblob.azblob_account_name); + let account_key = expose_optional_secret(&storage.azblob.azblob_account_key); + let sas_token = storage.azblob.azblob_sas_token.clone(); + + let mut options = Vec::new(); + if let Some(account_name) = &account_name { + options.push(format!( + "ACCOUNT_NAME='{}'", + escape_sql_literal(account_name) + )); + } + if let Some(account_key) = &account_key { + options.push(format!("ACCOUNT_KEY='{}'", escape_sql_literal(account_key))); + } + if let Some(sas_token) = &sas_token { + options.push(format!("SAS_TOKEN='{}'", escape_sql_literal(sas_token))); + } + if !storage.azblob.azblob_endpoint.is_empty() { + options.push(format!( + "ENDPOINT='{}'", + escape_sql_literal(&storage.azblob.azblob_endpoint) + )); + } + + let secrets = vec![account_name, account_key, sas_token]; + let connection = if options.is_empty() { + String::new() + } else { + format!(" CONNECTION ({})", options.join(", ")) + }; + (connection, secrets) +} + +fn expose_optional_secret(secret: &Option) -> Option { + secret.as_ref().map(|s| s.expose_secret().to_owned()) +} + +fn mask_secrets(sql: &str, secrets: &[Option]) -> String { + let mut masked = sql.to_string(); + for secret in secrets { + if let Some(secret) = secret + && !secret.is_empty() + { + let escaped = escape_sql_literal(secret); + if escaped != *secret { + masked = masked.replace(&escaped, "[REDACTED]"); + } + masked = masked.replace(secret, "[REDACTED]"); + } + } + masked +} + +#[cfg(test)] +mod tests { + use common_base::secrets::SecretString; + use common_test_util::temp_dir::create_temp_dir; + + use super::*; + use crate::common::{PrefixedAzblobConnection, PrefixedGcsConnection, PrefixedOssConnection}; + + #[test] + fn test_build_oss_connection_includes_endpoint() { + let storage = ObjectStoreConfig { + oss: PrefixedOssConnection { + oss_endpoint: "https://oss.example.com".to_string(), + oss_access_key_id: Some(SecretString::from("key_id".to_string())), + oss_access_key_secret: Some(SecretString::from("key_secret".to_string())), + ..Default::default() + }, + ..Default::default() + }; + + let (connection, _) = build_oss_connection(&storage); + assert!(connection.contains("ENDPOINT='https://oss.example.com'")); + } + + #[test] + fn test_build_gcs_connection_uses_scope_and_inline_credential() { + let storage = ObjectStoreConfig { + gcs: PrefixedGcsConnection { + gcs_scope: "scope-a".to_string(), + gcs_endpoint: "https://storage.googleapis.com".to_string(), + gcs_credential: Some(SecretString::from("credential-json".to_string())), + ..Default::default() + }, + ..Default::default() + }; + + let (connection, _) = build_gcs_connection(&storage, "gcs://bucket/root").unwrap(); + assert!(connection.contains("CREDENTIAL='credential-json'")); + assert!(connection.contains("SCOPE='scope-a'")); + assert!(connection.contains("ENDPOINT='https://storage.googleapis.com'")); + assert!(!connection.contains("CREDENTIAL_PATH")); + } + + #[test] + fn test_build_gcs_connection_rejects_credential_path_only() { + let storage = ObjectStoreConfig { + gcs: PrefixedGcsConnection { + gcs_scope: "scope-a".to_string(), + gcs_credential_path: Some(SecretString::from("/tmp/creds.json".to_string())), + ..Default::default() + }, + ..Default::default() + }; + + let error = build_gcs_connection(&storage, "gcs://bucket/root") + .expect_err("credential_path-only should be rejected") + .to_string(); + assert!(error.contains("gcs_credential_path is not supported")); + } + + #[test] + fn test_build_azblob_connection_includes_endpoint() { + let storage = ObjectStoreConfig { + azblob: PrefixedAzblobConnection { + azblob_account_name: Some(SecretString::from("account".to_string())), + azblob_account_key: Some(SecretString::from("key".to_string())), + azblob_endpoint: "https://blob.example.com".to_string(), + ..Default::default() + }, + ..Default::default() + }; + + let (connection, _) = build_azblob_connection(&storage); + assert!(connection.contains("ENDPOINT='https://blob.example.com'")); + } + + #[test] + fn test_build_azblob_connection_redacts_sas_token() { + let storage = ObjectStoreConfig { + azblob: PrefixedAzblobConnection { + azblob_account_name: Some(SecretString::from("account".to_string())), + azblob_account_key: Some(SecretString::from("key".to_string())), + azblob_sas_token: Some("sig=secret-token".to_string()), + ..Default::default() + }, + ..Default::default() + }; + + let (connection, secrets) = build_azblob_connection(&storage); + let masked = mask_secrets(&connection, &secrets); + + assert!(connection.contains("SAS_TOKEN='sig=secret-token'")); + assert!(masked.contains("SAS_TOKEN='[REDACTED]'")); + assert!(!masked.contains("sig=secret-token")); + } + + #[test] + fn test_mask_secrets_redacts_sql_escaped_literals() { + let sql = + "COPY DATABASE \"greptime\".\"public\" TO 's3://bucket' CONNECTION (SECRET='ab''cd');"; + let masked = mask_secrets(sql, &[Some("ab'cd".to_string())]); + + assert!(!masked.contains("ab'cd")); + assert!(!masked.contains("ab''cd")); + assert!(masked.contains("SECRET='[REDACTED]'")); + } + + #[test] + fn test_build_copy_target_decodes_file_uri_path() { + let storage = ObjectStoreConfig::default(); + let snapshot_root = create_temp_dir("my backup"); + let snapshot_uri = Url::from_file_path(snapshot_root.path()) + .expect("absolute platform path should convert to file:// URI") + .to_string(); + let expected = normalize_path(&format!( + "{}/{}", + snapshot_root.path().to_string_lossy(), + data_dir_for_schema_chunk("public", 7) + )); + let target = build_copy_target(&snapshot_uri, &storage, "public", 7) + .expect("file:// copy target should be built"); + + assert!(snapshot_uri.contains("%20")); + assert!(!target.location.contains("%20")); + assert!(target.location.contains("my backup")); + assert_eq!(target.location, expected); + } +} diff --git a/src/cli/src/data/export_v2/error.rs b/src/cli/src/data/export_v2/error.rs new file mode 100644 index 0000000000..ec860fecfa --- /dev/null +++ b/src/cli/src/data/export_v2/error.rs @@ -0,0 +1,223 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::any::Any; + +use common_error::ext::ErrorExt; +use common_error::status_code::StatusCode; +use common_macro::stack_trace_debug; +use snafu::{Location, Snafu}; + +#[derive(Snafu)] +#[snafu(visibility(pub))] +#[stack_trace_debug] +pub enum Error { + #[snafu(display("Invalid URI '{}': {}", uri, reason))] + InvalidUri { + uri: String, + reason: String, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Unsupported storage scheme: {}", scheme))] + UnsupportedScheme { + scheme: String, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Storage operation '{}' failed", operation))] + StorageOperation { + operation: String, + #[snafu(source)] + error: object_store::Error, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Failed to parse manifest"))] + ManifestParse { + #[snafu(source)] + error: serde_json::Error, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Failed to serialize manifest"))] + ManifestSerialize { + #[snafu(source)] + error: serde_json::Error, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Failed to decode text file as UTF-8"))] + TextDecode { + #[snafu(source)] + error: std::string::FromUtf8Error, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display( + "Cannot resume snapshot with a different schema_only mode (existing: {}, requested: {}). Use --force to recreate.", + existing_schema_only, + requested_schema_only + ))] + SchemaOnlyModeMismatch { + existing_schema_only: bool, + requested_schema_only: bool, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display( + "Cannot resume snapshot with different {} (existing: {}, requested: {}). Use --force to recreate.", + field, + existing, + requested + ))] + ResumeConfigMismatch { + field: String, + existing: String, + requested: String, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Failed to parse time: invalid format: {}", input))] + TimeParseInvalidFormat { + input: String, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Failed to parse time: end_time is before start_time"))] + TimeParseEndBeforeStart { + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display( + "chunk_time_window requires both --start-time and --end-time to be specified" + ))] + ChunkTimeWindowRequiresBounds { + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("--schema-only cannot be used with data export arguments: {}", args))] + SchemaOnlyArgsNotAllowed { + args: String, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Empty result from query"))] + EmptyResult { + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Unexpected value type in query result"))] + UnexpectedValueType { + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Database error"))] + Database { + #[snafu(source)] + error: crate::error::Error, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Snapshot not found at '{}'", uri))] + SnapshotNotFound { + uri: String, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Schema '{}' not found in catalog '{}'", schema, catalog))] + SchemaNotFound { + catalog: String, + schema: String, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Failed to parse URL"))] + UrlParse { + #[snafu(source)] + error: url::ParseError, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Failed to build object store"))] + BuildObjectStore { + #[snafu(source)] + error: object_store::Error, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Manifest version mismatch: expected {}, found {}", expected, found))] + ManifestVersionMismatch { + expected: u32, + found: u32, + #[snafu(implicit)] + location: Location, + }, +} + +pub type Result = std::result::Result; + +impl ErrorExt for Error { + fn status_code(&self) -> StatusCode { + match self { + Error::InvalidUri { .. } + | Error::UnsupportedScheme { .. } + | Error::SchemaOnlyModeMismatch { .. } + | Error::ResumeConfigMismatch { .. } + | Error::ManifestVersionMismatch { .. } + | Error::SchemaOnlyArgsNotAllowed { .. } => StatusCode::InvalidArguments, + Error::TimeParseInvalidFormat { .. } + | Error::TimeParseEndBeforeStart { .. } + | Error::ChunkTimeWindowRequiresBounds { .. } => StatusCode::InvalidArguments, + + Error::StorageOperation { .. } + | Error::ManifestParse { .. } + | Error::ManifestSerialize { .. } + | Error::TextDecode { .. } + | Error::BuildObjectStore { .. } => StatusCode::StorageUnavailable, + + Error::EmptyResult { .. } + | Error::UnexpectedValueType { .. } + | Error::UrlParse { .. } => StatusCode::Internal, + + Error::Database { error, .. } => error.status_code(), + + Error::SnapshotNotFound { .. } => StatusCode::InvalidArguments, + Error::SchemaNotFound { .. } => StatusCode::DatabaseNotFound, + } + } + + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/src/cli/src/data/export_v2/extractor.rs b/src/cli/src/data/export_v2/extractor.rs new file mode 100644 index 0000000000..ae15b199af --- /dev/null +++ b/src/cli/src/data/export_v2/extractor.rs @@ -0,0 +1,254 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Schema extraction from information_schema. +//! +//! For V2 DDL-only snapshots, extractor only persists the schema index. + +use std::collections::{HashMap, HashSet}; + +use serde_json::Value; +use snafu::ResultExt; + +use crate::data::export_v2::error::{ + DatabaseSnafu, EmptyResultSnafu, Result, SchemaNotFoundSnafu, UnexpectedValueTypeSnafu, +}; +use crate::data::export_v2::schema::{SchemaDefinition, SchemaSnapshot}; +use crate::data::sql::escape_sql_literal; +use crate::database::DatabaseClient; + +/// System schemas that should be excluded from export. +const SYSTEM_SCHEMAS: &[&str] = &["information_schema", "pg_catalog"]; + +/// Extracts schema definitions from information_schema. +pub struct SchemaExtractor<'a> { + client: &'a DatabaseClient, + catalog: &'a str, +} + +impl<'a> SchemaExtractor<'a> { + /// Creates a new schema extractor. + pub fn new(client: &'a DatabaseClient, catalog: &'a str) -> Self { + Self { client, catalog } + } + + /// Extracts the schema index for the given schemas. + /// + /// If `schemas` is None, extracts all non-system schemas. + pub async fn extract(&self, schemas: Option<&[String]>) -> Result { + let mut snapshot = SchemaSnapshot::new(); + + let schema_names = match schemas { + Some(names) => self.validate_schemas(names).await?, + None => self.get_all_schemas().await?, + }; + + for schema_name in &schema_names { + let schema_def = self.extract_schema_definition(schema_name).await?; + snapshot.add_schema(schema_def); + } + + Ok(snapshot) + } + + /// Gets all non-system schemas in the catalog. + async fn get_all_schemas(&self) -> Result> { + let sql = format!( + "SELECT schema_name FROM information_schema.schemata \ + WHERE catalog_name = '{}'", + escape_sql_literal(self.catalog) + ); + + let records = self.query(&sql).await?; + let mut schemas = Vec::new(); + + for row in records { + let name = extract_string(&row, 0)?; + if !SYSTEM_SCHEMAS.contains(&name.as_str()) { + schemas.push(name); + } + } + + Ok(schemas) + } + + /// Validates that all specified schemas exist. + async fn validate_schemas(&self, schemas: &[String]) -> Result> { + let all_schemas = self.get_all_schemas().await?; + dedupe_canonicalized_schemas(schemas, &all_schemas, self.catalog) + } + + /// Extracts schema (database) definition. + async fn extract_schema_definition(&self, schema: &str) -> Result { + let sql = format!( + "SELECT schema_name, options FROM information_schema.schemata \ + WHERE catalog_name = '{}' AND schema_name = '{}'", + escape_sql_literal(self.catalog), + escape_sql_literal(schema) + ); + + let records = self.query(&sql).await?; + if records.is_empty() { + return SchemaNotFoundSnafu { + catalog: self.catalog, + schema, + } + .fail(); + } + + let name = extract_string(&records[0], 0)?; + let options = extract_optional_string(&records[0], 1) + .map(|opts| parse_options(&opts)) + .unwrap_or_default(); + + Ok(SchemaDefinition { + catalog: self.catalog.to_string(), + name, + options, + }) + } + + /// Executes a SQL query and returns the results. + async fn query(&self, sql: &str) -> Result>> { + self.client + .sql_in_public(sql) + .await + .context(DatabaseSnafu)? + .ok_or_else(|| EmptyResultSnafu.build()) + } +} + +/// Extracts a string value from a row. +fn extract_string(row: &[Value], index: usize) -> Result { + match row.get(index) { + Some(Value::String(s)) => Ok(s.clone()), + Some(Value::Null) => UnexpectedValueTypeSnafu.fail(), + _ => UnexpectedValueTypeSnafu.fail(), + } +} + +/// Extracts an optional string value from a row. +fn extract_optional_string(row: &[Value], index: usize) -> Option { + match row.get(index) { + Some(Value::String(s)) if !s.is_empty() => Some(s.clone()), + _ => None, + } +} + +/// Parses options string into a HashMap. +fn parse_options(options_str: &str) -> HashMap { + if let Ok(map) = serde_json::from_str::>(options_str) { + return map; + } + + let mut options = HashMap::new(); + for line in options_str.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + + if let Some((key, value)) = parse_quoted_option_line(trimmed) { + options.insert(key, value); + continue; + } + + for part in trimmed.split_whitespace() { + if let Some((key, value)) = part.split_once('=') { + options.insert(key.to_string(), value.to_string()); + } + } + } + options +} + +fn parse_quoted_option_line(line: &str) -> Option<(String, String)> { + let key = line.strip_prefix('\'')?; + let (key, rest) = key.split_once("'='")?; + let value = rest.strip_suffix('\'')?; + Some((key.to_string(), value.to_string())) +} + +fn dedupe_canonicalized_schemas( + requested: &[String], + available: &[String], + catalog: &str, +) -> Result> { + let mut canonicalized = Vec::new(); + let mut seen = HashSet::new(); + + for schema in requested { + let Some(canonical) = available.iter().find(|s| s.eq_ignore_ascii_case(schema)) else { + return SchemaNotFoundSnafu { catalog, schema }.fail(); + }; + + if seen.insert(canonical.to_ascii_lowercase()) { + canonicalized.push(canonical.clone()); + } + } + + Ok(canonicalized) +} + +#[cfg(test)] +mod tests { + use serde_json::Value; + + use super::*; + + #[test] + fn test_parse_options_json() { + let opts = r#"{"ttl": "30d", "custom": "value"}"#; + let parsed = parse_options(opts); + assert_eq!(parsed.get("ttl"), Some(&"30d".to_string())); + assert_eq!(parsed.get("custom"), Some(&"value".to_string())); + } + + #[test] + fn test_parse_options_key_value() { + let opts = "ttl=30d custom=value"; + let parsed = parse_options(opts); + assert_eq!(parsed.get("ttl"), Some(&"30d".to_string())); + assert_eq!(parsed.get("custom"), Some(&"value".to_string())); + } + + #[test] + fn test_parse_options_schema_display_format() { + let opts = "'ttl'='30d'\n'custom'='value with spaces'\n"; + let parsed = parse_options(opts); + assert_eq!(parsed.get("ttl"), Some(&"30d".to_string())); + assert_eq!(parsed.get("custom"), Some(&"value with spaces".to_string())); + } + + #[test] + fn test_extract_string_rejects_null() { + let row = vec![Value::Null]; + assert!(extract_string(&row, 0).is_err()); + } + + #[test] + fn test_dedupe_canonicalized_schemas() { + let available = vec!["public".to_string(), "test_db".to_string()]; + let requested = vec![ + "PUBLIC".to_string(), + "public".to_string(), + "Test_Db".to_string(), + ]; + + let canonicalized = dedupe_canonicalized_schemas(&requested, &available, "greptime") + .expect("schemas should be canonicalized"); + + assert_eq!(canonicalized, vec!["public", "test_db"]); + } +} diff --git a/src/cli/src/data/export_v2/manifest.rs b/src/cli/src/data/export_v2/manifest.rs new file mode 100644 index 0000000000..918288bb51 --- /dev/null +++ b/src/cli/src/data/export_v2/manifest.rs @@ -0,0 +1,569 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Manifest data structures for Export/Import V2. + +use std::time::Duration; +use std::{fmt, str}; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::data::export_v2::chunker::generate_chunks; +use crate::data::export_v2::error::{ + ChunkTimeWindowRequiresBoundsSnafu, Result as ExportResult, TimeParseEndBeforeStartSnafu, + TimeParseInvalidFormatSnafu, +}; + +/// Current manifest format version. +pub const MANIFEST_VERSION: u32 = 1; + +/// Manifest file name within snapshot directory. +pub const MANIFEST_FILE: &str = "manifest.json"; + +/// Time range for data export (half-open interval: [start, end)). +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TimeRange { + /// Start time (inclusive). None means earliest available data. + #[serde(skip_serializing_if = "Option::is_none")] + pub start: Option>, + /// End time (exclusive). None means current time. + #[serde(skip_serializing_if = "Option::is_none")] + pub end: Option>, +} + +impl TimeRange { + /// Creates a new time range with specified bounds. + pub fn new(start: Option>, end: Option>) -> Self { + Self { start, end } + } + + /// Creates an unbounded time range (all data). + pub fn unbounded() -> Self { + Self { + start: None, + end: None, + } + } + + /// Returns true if this time range is unbounded. + pub fn is_unbounded(&self) -> bool { + self.start.is_none() && self.end.is_none() + } + + /// Returns true if both bounds are specified. + pub fn is_bounded(&self) -> bool { + self.start.is_some() && self.end.is_some() + } + + /// Parses a time range from optional RFC3339 strings. + pub fn parse(start: Option<&str>, end: Option<&str>) -> ExportResult { + let start = start.map(parse_time).transpose()?; + let end = end.map(parse_time).transpose()?; + + if let (Some(start), Some(end)) = (start, end) + && end < start + { + return TimeParseEndBeforeStartSnafu.fail(); + } + + Ok(Self::new(start, end)) + } +} + +fn parse_time(input: &str) -> ExportResult> { + DateTime::parse_from_rfc3339(input) + .map(|dt| dt.with_timezone(&Utc)) + .map_err(|_| TimeParseInvalidFormatSnafu { input }.build()) +} + +impl Default for TimeRange { + fn default() -> Self { + Self::unbounded() + } +} + +/// Status of a chunk during export/import. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum ChunkStatus { + /// Chunk is pending export. + #[default] + Pending, + /// Chunk export is in progress. + InProgress, + /// Chunk export completed successfully. + Completed, + /// Chunk had no data to export. + Skipped, + /// Chunk export failed. + Failed, +} + +/// Metadata for a single chunk of exported data. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChunkMeta { + /// Chunk identifier (sequential number starting from 1). + pub id: u32, + /// Time range covered by this chunk. + pub time_range: TimeRange, + /// Export status. + pub status: ChunkStatus, + /// List of data files in this chunk (relative paths from snapshot root). + #[serde(default)] + pub files: Vec, + /// SHA256 checksum of all files in this chunk (aggregated). + #[serde(skip_serializing_if = "Option::is_none")] + pub checksum: Option, + /// Error message if status is Failed. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl ChunkMeta { + /// Creates a new pending chunk with the given id and time range. + pub fn new(id: u32, time_range: TimeRange) -> Self { + Self { + id, + time_range, + status: ChunkStatus::Pending, + files: vec![], + checksum: None, + error: None, + } + } + + /// Creates a skipped chunk with the given id and time range. + pub fn skipped(id: u32, time_range: TimeRange) -> Self { + let mut chunk = Self::new(id, time_range); + chunk.mark_skipped(); + chunk + } + + /// Marks this chunk as in progress. + pub fn mark_in_progress(&mut self) { + self.status = ChunkStatus::InProgress; + self.error = None; + } + + /// Marks this chunk as completed with the given files and checksum. + pub fn mark_completed(&mut self, files: Vec, checksum: Option) { + self.status = ChunkStatus::Completed; + self.files = files; + self.checksum = checksum; + self.error = None; + } + + /// Marks this chunk as skipped because no data files were produced. + pub fn mark_skipped(&mut self) { + self.status = ChunkStatus::Skipped; + self.files.clear(); + self.checksum = None; + self.error = None; + } + + /// Marks this chunk as failed with the given error message. + pub fn mark_failed(&mut self, error: String) { + self.status = ChunkStatus::Failed; + self.error = Some(error); + } +} + +/// Supported data formats for export. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, clap::ValueEnum)] +#[serde(rename_all = "lowercase")] +#[value(rename_all = "lowercase")] +pub enum DataFormat { + /// Apache Parquet format (default, recommended for production). + #[default] + Parquet, + /// CSV format (human-readable). + Csv, + /// JSON format (structured text). + Json, +} + +impl fmt::Display for DataFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DataFormat::Parquet => write!(f, "parquet"), + DataFormat::Csv => write!(f, "csv"), + DataFormat::Json => write!(f, "json"), + } + } +} + +impl str::FromStr for DataFormat { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "parquet" => Ok(DataFormat::Parquet), + "csv" => Ok(DataFormat::Csv), + "json" => Ok(DataFormat::Json), + _ => Err(format!( + "invalid format '{}': expected one of parquet, csv, json", + s + )), + } + } +} + +/// Snapshot manifest containing all metadata. +/// +/// The manifest is stored as `manifest.json` in the snapshot root directory. +/// It contains: +/// - Snapshot identification (UUID, timestamps) +/// - Scope (catalog, schemas, time range) +/// - Export configuration (format, schema_only) +/// - Chunk metadata for resume support +/// - Integrity checksums +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Manifest { + /// Manifest format version for compatibility checking. + pub version: u32, + /// Unique snapshot identifier. + pub snapshot_id: Uuid, + /// Catalog name. + pub catalog: String, + /// List of schemas included in this snapshot. + pub schemas: Vec, + /// Overall time range covered by this snapshot. + pub time_range: TimeRange, + /// Whether this is a schema-only snapshot (no data). + pub schema_only: bool, + /// Data format used for export. + pub format: DataFormat, + /// Chunk metadata (empty for schema-only snapshots). + #[serde(default)] + pub chunks: Vec, + /// Snapshot-level SHA256 checksum (aggregated from all chunks). + #[serde(skip_serializing_if = "Option::is_none")] + pub checksum: Option, + /// Creation timestamp. + pub created_at: DateTime, + /// Last updated timestamp. + pub updated_at: DateTime, +} + +impl Manifest { + pub fn new_for_export( + catalog: String, + schemas: Vec, + schema_only: bool, + time_range: TimeRange, + format: DataFormat, + chunk_time_window: Option, + ) -> ExportResult { + if chunk_time_window.is_some() && !time_range.is_bounded() { + return ChunkTimeWindowRequiresBoundsSnafu.fail(); + } + + let mut manifest = if schema_only { + Self::new_schema_only(catalog, schemas) + } else { + Self::new_full(catalog, schemas, time_range, format) + }; + + if !schema_only { + manifest.chunks = match chunk_time_window { + Some(window) => generate_chunks(&manifest.time_range, window), + None => generate_single_chunk(&manifest.time_range), + }; + manifest.touch(); + } + + Ok(manifest) + } + + /// Creates a new manifest for schema-only export. + pub fn new_schema_only(catalog: String, schemas: Vec) -> Self { + let now = Utc::now(); + Self { + version: MANIFEST_VERSION, + snapshot_id: Uuid::new_v4(), + catalog, + schemas, + time_range: TimeRange::unbounded(), + schema_only: true, + format: DataFormat::Parquet, + chunks: vec![], + checksum: None, + created_at: now, + updated_at: now, + } + } + + /// Creates a new manifest for full export with time range and format. + pub fn new_full( + catalog: String, + schemas: Vec, + time_range: TimeRange, + format: DataFormat, + ) -> Self { + let now = Utc::now(); + Self { + version: MANIFEST_VERSION, + snapshot_id: Uuid::new_v4(), + catalog, + schemas, + time_range, + schema_only: false, + format, + chunks: vec![], + checksum: None, + created_at: now, + updated_at: now, + } + } + + /// Returns true if all chunks are completed (or if schema-only). + pub fn is_complete(&self) -> bool { + self.schema_only + || (!self.chunks.is_empty() + && self + .chunks + .iter() + .all(|c| matches!(c.status, ChunkStatus::Completed | ChunkStatus::Skipped))) + } + + /// Returns the number of pending chunks. + pub fn pending_count(&self) -> usize { + self.chunks + .iter() + .filter(|c| c.status == ChunkStatus::Pending) + .count() + } + + /// Returns the number of in-progress chunks. + pub fn in_progress_count(&self) -> usize { + self.chunks + .iter() + .filter(|c| c.status == ChunkStatus::InProgress) + .count() + } + + /// Returns the number of completed chunks. + pub fn completed_count(&self) -> usize { + self.chunks + .iter() + .filter(|c| c.status == ChunkStatus::Completed) + .count() + } + + /// Returns the number of skipped chunks. + pub fn skipped_count(&self) -> usize { + self.chunks + .iter() + .filter(|c| c.status == ChunkStatus::Skipped) + .count() + } + + /// Returns the number of failed chunks. + pub fn failed_count(&self) -> usize { + self.chunks + .iter() + .filter(|c| c.status == ChunkStatus::Failed) + .count() + } + + /// Updates the `updated_at` timestamp to now. + pub fn touch(&mut self) { + self.updated_at = Utc::now(); + } + + /// Adds a chunk to the manifest. + pub fn add_chunk(&mut self, chunk: ChunkMeta) { + self.chunks.push(chunk); + self.touch(); + } + + /// Updates a chunk by id. + pub fn update_chunk(&mut self, id: u32, updater: impl FnOnce(&mut ChunkMeta)) { + if let Some(chunk) = self.chunks.iter_mut().find(|c| c.id == id) { + updater(chunk); + self.touch(); + } + } +} + +fn generate_single_chunk(time_range: &TimeRange) -> Vec { + if let (Some(start), Some(end)) = (time_range.start, time_range.end) { + if start == end { + return vec![ChunkMeta::skipped(1, time_range.clone())]; + } + if start > end { + return Vec::new(); + } + } + vec![ChunkMeta::new(1, time_range.clone())] +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use chrono::{TimeZone, Utc}; + + use super::*; + + #[test] + fn test_time_range_serialization() { + let range = TimeRange::unbounded(); + let json = serde_json::to_string(&range).unwrap(); + assert_eq!(json, "{}"); + + let range: TimeRange = serde_json::from_str("{}").unwrap(); + assert!(range.is_unbounded()); + } + + #[test] + fn test_manifest_schema_only() { + let manifest = + Manifest::new_schema_only("greptime".to_string(), vec!["public".to_string()]); + + assert_eq!(manifest.version, MANIFEST_VERSION); + assert!(manifest.schema_only); + assert!(manifest.chunks.is_empty()); + assert!(manifest.is_complete()); + } + + #[test] + fn test_generate_single_chunk_zero_width_range_is_skipped() { + let ts = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(); + let chunks = generate_single_chunk(&TimeRange::new(Some(ts), Some(ts))); + + assert_eq!(chunks.len(), 1); + assert_eq!(chunks[0].status, ChunkStatus::Skipped); + assert_eq!(chunks[0].time_range.start, Some(ts)); + assert_eq!(chunks[0].time_range.end, Some(ts)); + } + + #[test] + fn test_generate_single_chunk_invalid_range_is_empty() { + let start = Utc.with_ymd_and_hms(2025, 1, 1, 1, 0, 0).unwrap(); + let end = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(); + let chunks = generate_single_chunk(&TimeRange::new(Some(start), Some(end))); + + assert!(chunks.is_empty()); + } + + #[test] + fn test_manifest_full() { + let manifest = Manifest::new_full( + "greptime".to_string(), + vec!["public".to_string()], + TimeRange::unbounded(), + DataFormat::Parquet, + ); + + assert!(!manifest.schema_only); + assert!(manifest.chunks.is_empty()); + assert!(!manifest.is_complete()); + } + + #[test] + fn test_data_format_parsing() { + assert_eq!( + "parquet".parse::().unwrap(), + DataFormat::Parquet + ); + assert_eq!("CSV".parse::().unwrap(), DataFormat::Csv); + assert_eq!("JSON".parse::().unwrap(), DataFormat::Json); + assert!("invalid".parse::().is_err()); + } + + #[test] + fn test_chunk_status_transitions() { + let mut chunk = ChunkMeta::new(1, TimeRange::unbounded()); + assert_eq!(chunk.status, ChunkStatus::Pending); + + chunk.mark_in_progress(); + assert_eq!(chunk.status, ChunkStatus::InProgress); + + chunk.mark_completed( + vec!["file1.parquet".to_string()], + Some("abc123".to_string()), + ); + assert_eq!(chunk.status, ChunkStatus::Completed); + assert_eq!(chunk.files.len(), 1); + + chunk.mark_skipped(); + assert_eq!(chunk.status, ChunkStatus::Skipped); + assert!(chunk.files.is_empty()); + } + + #[test] + fn test_manifest_is_complete_when_chunks_are_completed_or_skipped() { + let mut manifest = Manifest::new_full( + "greptime".to_string(), + vec!["public".to_string()], + TimeRange::unbounded(), + DataFormat::Parquet, + ); + manifest.add_chunk(ChunkMeta::new(1, TimeRange::unbounded())); + manifest.add_chunk(ChunkMeta::new(2, TimeRange::unbounded())); + + manifest.update_chunk(1, |chunk| { + chunk.mark_completed(vec!["a.parquet".to_string()], None) + }); + manifest.update_chunk(2, |chunk| chunk.mark_skipped()); + + assert!(manifest.is_complete()); + assert_eq!(manifest.completed_count(), 1); + assert_eq!(manifest.skipped_count(), 1); + } + + #[test] + fn test_manifest_chunk_time_window_none_single_chunk() { + let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(); + let end = Utc.with_ymd_and_hms(2025, 1, 2, 0, 0, 0).unwrap(); + let range = TimeRange::new(Some(start), Some(end)); + let manifest = Manifest::new_for_export( + "greptime".to_string(), + vec!["public".to_string()], + false, + range.clone(), + DataFormat::Parquet, + None, + ) + .unwrap(); + + assert_eq!(manifest.chunks.len(), 1); + assert_eq!(manifest.chunks[0].time_range, range); + } + + #[test] + fn test_time_range_parse_requires_order() { + let result = TimeRange::parse(Some("2025-01-02T00:00:00Z"), Some("2025-01-01T00:00:00Z")); + assert!(result.is_err()); + } + + #[test] + fn test_new_for_export_with_chunk_window_requires_bounded_range() { + let result = Manifest::new_for_export( + "greptime".to_string(), + vec!["public".to_string()], + false, + TimeRange::new( + None, + Some(Utc.with_ymd_and_hms(2025, 1, 2, 0, 0, 0).unwrap()), + ), + DataFormat::Parquet, + Some(Duration::from_secs(3600)), + ); + assert!(result.is_err()); + } +} diff --git a/src/cli/src/data/export_v2/schema.rs b/src/cli/src/data/export_v2/schema.rs new file mode 100644 index 0000000000..1aab6ac900 --- /dev/null +++ b/src/cli/src/data/export_v2/schema.rs @@ -0,0 +1,98 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Minimal schema index structures for Export/Import V2. +//! +//! The canonical schema representation is the per-schema DDL file under +//! `schema/ddl/`. `schemas.json` only records which schemas exist in a snapshot. + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +/// Schema directory name within snapshot. +pub const SCHEMA_DIR: &str = "schema"; + +/// DDL directory name within schema directory. +pub const DDL_DIR: &str = "ddl"; + +/// Schema definition file name. +pub const SCHEMAS_FILE: &str = "schemas.json"; + +/// Schema (database) definition. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SchemaDefinition { + /// Catalog name. + pub catalog: String, + /// Schema (database) name. + pub name: String, + /// Schema options (if any). + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub options: HashMap, +} + +/// Minimal schema index stored in a snapshot. +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct SchemaSnapshot { + /// Schema (database) definitions. + pub schemas: Vec, +} + +impl SchemaSnapshot { + /// Creates an empty schema snapshot. + pub fn new() -> Self { + Self::default() + } + + /// Adds a schema definition. + pub fn add_schema(&mut self, schema: SchemaDefinition) { + self.schemas.push(schema); + } + + /// Filters the snapshot to only include specified schemas. + pub fn filter_schemas(&self, schemas: &[String]) -> Self { + Self { + schemas: self + .schemas + .iter() + .filter(|s| schemas.contains(&s.name)) + .cloned() + .collect(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_schema_snapshot_filter() { + let mut snapshot = SchemaSnapshot::new(); + snapshot.add_schema(SchemaDefinition { + catalog: "greptime".to_string(), + name: "public".to_string(), + options: HashMap::new(), + }); + snapshot.add_schema(SchemaDefinition { + catalog: "greptime".to_string(), + name: "private".to_string(), + options: HashMap::new(), + }); + + let filtered = snapshot.filter_schemas(&["public".to_string()]); + assert_eq!(filtered.schemas.len(), 1); + assert_eq!(filtered.schemas[0].name, "public"); + } +} diff --git a/src/cli/src/data/export_v2/tests.rs b/src/cli/src/data/export_v2/tests.rs new file mode 100644 index 0000000000..92f45c6aec --- /dev/null +++ b/src/cli/src/data/export_v2/tests.rs @@ -0,0 +1,885 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::env; +use std::time::Duration; + +use clap::Parser; +use common_error::ext::BoxedError; +use serde_json::Value; +use snafu::ResultExt; +use tempfile::tempdir; +use url::Url; + +use super::command::ExportCreateCommand; +use crate::common::ObjectStoreConfig; +use crate::data::export_v2::manifest::ChunkStatus; +use crate::data::import_v2::ImportV2Command; +use crate::data::snapshot_storage::{OpenDalStorage, SnapshotStorage}; +use crate::data::sql::escape_sql_identifier; +use crate::database::DatabaseClient; +use crate::error::{FileIoSnafu, InvalidArgumentsSnafu, OtherSnafu, Result}; + +async fn query_count(database_client: &DatabaseClient, schema: &str, table: &str) -> Result { + let sql = format!("SELECT COUNT(*) FROM {}", escape_sql_identifier(table)); + let rows = database_client.sql(&sql, schema).await?; + let first_row = rows.as_ref().and_then(|rows| rows.first()).ok_or_else(|| { + InvalidArgumentsSnafu { + msg: format!("empty result for query: {sql}"), + } + .build() + })?; + let first_value = first_row.first().ok_or_else(|| { + InvalidArgumentsSnafu { + msg: format!("no first column for query: {sql}"), + } + .build() + })?; + match first_value { + Value::Number(n) => n.as_u64().ok_or_else(|| { + InvalidArgumentsSnafu { + msg: format!("count is not u64 for query: {sql}"), + } + .build() + }), + _ => InvalidArgumentsSnafu { + msg: format!("unexpected count type for query: {sql}"), + } + .fail(), + } +} + +#[tokio::test] +#[ignore] +async fn export_import_v2_schema_parity_e2e() -> Result<()> { + let addr = env::var("GREPTIME_ADDR").unwrap_or_else(|_| "127.0.0.1:4000".to_string()); + let catalog = env::var("GREPTIME_CATALOG").unwrap_or_else(|_| "greptime".to_string()); + let auth_basic = env::var("GREPTIME_AUTH_BASIC").ok(); + let schema = "test_db_schema_parity"; + + let database_client = DatabaseClient::new( + addr.clone(), + catalog.clone(), + auth_basic.clone(), + Duration::from_secs(60), + None, + false, + ); + + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + database_client + .sql_in_public(&format!("CREATE DATABASE {schema}")) + .await?; + database_client + .sql( + "CREATE TABLE metrics (\ + ts TIMESTAMP TIME INDEX, \ + host STRING PRIMARY KEY, \ + cpu DOUBLE DEFAULT 0.0, \ + region_name STRING \ + ) ENGINE = mito WITH (ttl='7d', 'compaction.type'='twcs')", + schema, + ) + .await?; + database_client + .sql( + "CREATE TABLE logs (\ + ts TIMESTAMP TIME INDEX, \ + app STRING PRIMARY KEY, \ + msg STRING NOT NULL COMMENT 'log message' \ + ) ENGINE = mito", + schema, + ) + .await?; + database_client + .sql( + "CREATE TABLE metrics_physical (\ + ts TIMESTAMP TIME INDEX, \ + host STRING, \ + region_name STRING, \ + cpu DOUBLE DEFAULT 0.0, \ + PRIMARY KEY (host, region_name) \ + ) ENGINE = metric WITH (physical_metric_table='true')", + schema, + ) + .await?; + database_client + .sql( + "CREATE TABLE metrics_logical (\ + ts TIMESTAMP TIME INDEX, \ + host STRING, \ + region_name STRING, \ + cpu DOUBLE DEFAULT 0.0, \ + PRIMARY KEY (host, region_name) \ + ) ENGINE = metric WITH (on_physical_table='metrics_physical')", + schema, + ) + .await?; + database_client + .sql( + "CREATE VIEW metrics_view AS SELECT * FROM metrics WHERE cpu > 0.5", + schema, + ) + .await?; + + let src_dir = tempdir().context(FileIoSnafu)?; + let src_uri = Url::from_directory_path(src_dir.path()) + .map_err(|_| { + InvalidArgumentsSnafu { + msg: "invalid temp dir path".to_string(), + } + .build() + })? + .to_string(); + + let mut export_args = vec![ + "export-v2-create", + "--addr", + &addr, + "--to", + &src_uri, + "--catalog", + &catalog, + "--schemas", + schema, + "--schema-only", + ]; + if let Some(auth) = &auth_basic { + export_args.push("--auth-basic"); + export_args.push(auth); + } + let export_cmd = ExportCreateCommand::parse_from(export_args); + export_cmd + .build() + .await + .context(OtherSnafu)? + .do_work() + .await + .context(OtherSnafu)?; + + database_client + .sql_in_public(&format!("DROP DATABASE {schema}")) + .await?; + + let mut import_args = vec![ + "import-v2", + "--addr", + &addr, + "--from", + &src_uri, + "--catalog", + &catalog, + "--schemas", + schema, + ]; + if let Some(auth) = &auth_basic { + import_args.push("--auth-basic"); + import_args.push(auth); + } + let import_cmd = ImportV2Command::parse_from(import_args); + import_cmd + .build() + .await + .context(OtherSnafu)? + .do_work() + .await + .context(OtherSnafu)?; + + let dst_dir = tempdir().context(FileIoSnafu)?; + let dst_uri = Url::from_directory_path(dst_dir.path()) + .map_err(|_| { + InvalidArgumentsSnafu { + msg: "invalid temp dir path".to_string(), + } + .build() + })? + .to_string(); + + let mut export_args = vec![ + "export-v2-create", + "--addr", + &addr, + "--to", + &dst_uri, + "--catalog", + &catalog, + "--schemas", + schema, + "--schema-only", + ]; + if let Some(auth) = &auth_basic { + export_args.push("--auth-basic"); + export_args.push(auth); + } + let export_cmd = ExportCreateCommand::parse_from(export_args); + export_cmd + .build() + .await + .context(OtherSnafu)? + .do_work() + .await + .context(OtherSnafu)?; + + let storage_config = ObjectStoreConfig::default(); + let src_storage = OpenDalStorage::from_uri(&src_uri, &storage_config) + .map_err(BoxedError::new) + .context(OtherSnafu)?; + let dst_storage = OpenDalStorage::from_uri(&dst_uri, &storage_config) + .map_err(BoxedError::new) + .context(OtherSnafu)?; + + let src_schema_snapshot = src_storage + .read_schema() + .await + .map_err(BoxedError::new) + .context(OtherSnafu)?; + let dst_schema_snapshot = dst_storage + .read_schema() + .await + .map_err(BoxedError::new) + .context(OtherSnafu)?; + assert_eq!(src_schema_snapshot, dst_schema_snapshot); + + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + + Ok(()) +} + +#[tokio::test] +#[ignore] +async fn import_v2_ddl_dry_run_e2e() -> Result<()> { + let addr = env::var("GREPTIME_ADDR").unwrap_or_else(|_| "127.0.0.1:4000".to_string()); + let catalog = env::var("GREPTIME_CATALOG").unwrap_or_else(|_| "greptime".to_string()); + let auth_basic = env::var("GREPTIME_AUTH_BASIC").ok(); + let schema = "test_db_ddl_dry_run"; + + let database_client = DatabaseClient::new( + addr.clone(), + catalog.clone(), + auth_basic.clone(), + Duration::from_secs(60), + None, + false, + ); + + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + database_client + .sql_in_public(&format!("CREATE DATABASE {schema}")) + .await?; + database_client + .sql( + "CREATE TABLE metrics (\ + ts TIMESTAMP TIME INDEX, \ + host STRING PRIMARY KEY, \ + cpu DOUBLE DEFAULT 0.0, \ + region_name STRING \ + ) ENGINE = mito WITH (ttl='7d', 'compaction.type'='twcs')", + schema, + ) + .await?; + database_client + .sql( + "CREATE TABLE logs (\ + ts TIMESTAMP TIME INDEX, \ + app STRING PRIMARY KEY, \ + msg STRING NOT NULL COMMENT 'log message' \ + ) ENGINE = mito", + schema, + ) + .await?; + + let src_dir = tempdir().context(FileIoSnafu)?; + let src_uri = Url::from_directory_path(src_dir.path()) + .map_err(|_| { + InvalidArgumentsSnafu { + msg: "invalid temp dir path".to_string(), + } + .build() + })? + .to_string(); + + let mut export_args = vec![ + "export-v2-create", + "--addr", + &addr, + "--to", + &src_uri, + "--catalog", + &catalog, + "--schemas", + schema, + "--schema-only", + ]; + if let Some(auth) = &auth_basic { + export_args.push("--auth-basic"); + export_args.push(auth); + } + let export_cmd = ExportCreateCommand::parse_from(export_args); + export_cmd + .build() + .await + .context(OtherSnafu)? + .do_work() + .await + .context(OtherSnafu)?; + + let mut import_args = vec![ + "import-v2", + "--addr", + &addr, + "--from", + &src_uri, + "--catalog", + &catalog, + "--schemas", + schema, + "--dry-run", + ]; + if let Some(auth) = &auth_basic { + import_args.push("--auth-basic"); + import_args.push(auth); + } + let import_cmd = ImportV2Command::parse_from(import_args); + import_cmd + .build() + .await + .context(OtherSnafu)? + .do_work() + .await + .context(OtherSnafu)?; + + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + + Ok(()) +} + +#[tokio::test] +#[ignore] +async fn export_import_v2_data_roundtrip_e2e() -> Result<()> { + let addr = env::var("GREPTIME_ADDR").unwrap_or_else(|_| "127.0.0.1:4000".to_string()); + let catalog = env::var("GREPTIME_CATALOG").unwrap_or_else(|_| "greptime".to_string()); + let auth_basic = env::var("GREPTIME_AUTH_BASIC").ok(); + let schema = "test_db_data_roundtrip"; + + let database_client = DatabaseClient::new( + addr.clone(), + catalog.clone(), + auth_basic.clone(), + Duration::from_secs(60), + None, + false, + ); + + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + database_client + .sql_in_public(&format!("CREATE DATABASE {schema}")) + .await?; + database_client + .sql( + "CREATE TABLE metrics (\ + ts TIMESTAMP TIME INDEX, \ + host STRING PRIMARY KEY, \ + cpu DOUBLE \ + ) ENGINE=mito", + schema, + ) + .await?; + database_client + .sql( + "INSERT INTO metrics (ts, host, cpu) VALUES \ + ('2025-01-01T00:00:00Z', 'h1', 1.0), \ + ('2025-01-01T01:00:00Z', 'h2', 2.0)", + schema, + ) + .await?; + + let expected_rows = query_count(&database_client, schema, "metrics").await?; + + let src_dir = tempdir().context(FileIoSnafu)?; + let src_uri = Url::from_directory_path(src_dir.path()) + .map_err(|_| { + InvalidArgumentsSnafu { + msg: "invalid temp dir path".to_string(), + } + .build() + })? + .to_string(); + + let mut export_args = vec![ + "export-v2-create", + "--addr", + &addr, + "--to", + &src_uri, + "--catalog", + &catalog, + "--schemas", + schema, + ]; + if let Some(auth) = &auth_basic { + export_args.push("--auth-basic"); + export_args.push(auth); + } + let export_cmd = ExportCreateCommand::parse_from(export_args); + export_cmd + .build() + .await + .context(OtherSnafu)? + .do_work() + .await + .context(OtherSnafu)?; + + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + + let mut import_args = vec![ + "import-v2", + "--addr", + &addr, + "--from", + &src_uri, + "--catalog", + &catalog, + "--schemas", + schema, + ]; + if let Some(auth) = &auth_basic { + import_args.push("--auth-basic"); + import_args.push(auth); + } + let import_cmd = ImportV2Command::parse_from(import_args); + import_cmd + .build() + .await + .context(OtherSnafu)? + .do_work() + .await + .context(OtherSnafu)?; + + let actual_rows = query_count(&database_client, schema, "metrics").await?; + assert_eq!(actual_rows, expected_rows); + + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + + Ok(()) +} + +#[tokio::test] +#[ignore] +async fn import_v2_fails_on_incomplete_snapshot_e2e() -> Result<()> { + let addr = env::var("GREPTIME_ADDR").unwrap_or_else(|_| "127.0.0.1:4000".to_string()); + let catalog = env::var("GREPTIME_CATALOG").unwrap_or_else(|_| "greptime".to_string()); + let auth_basic = env::var("GREPTIME_AUTH_BASIC").ok(); + let schema = "test_db_incomplete_snapshot"; + + let database_client = DatabaseClient::new( + addr.clone(), + catalog.clone(), + auth_basic.clone(), + Duration::from_secs(60), + None, + false, + ); + + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + database_client + .sql_in_public(&format!("CREATE DATABASE {schema}")) + .await?; + database_client + .sql( + "CREATE TABLE metrics (\ + ts TIMESTAMP TIME INDEX, \ + host STRING PRIMARY KEY, \ + cpu DOUBLE \ + ) ENGINE=mito", + schema, + ) + .await?; + database_client + .sql( + "INSERT INTO metrics (ts, host, cpu) VALUES ('2025-01-01T00:00:00Z', 'h1', 1.0)", + schema, + ) + .await?; + + let src_dir = tempdir().context(FileIoSnafu)?; + let src_uri = Url::from_directory_path(src_dir.path()) + .map_err(|_| { + InvalidArgumentsSnafu { + msg: "invalid temp dir path".to_string(), + } + .build() + })? + .to_string(); + + let mut export_args = vec![ + "export-v2-create", + "--addr", + &addr, + "--to", + &src_uri, + "--catalog", + &catalog, + "--schemas", + schema, + ]; + if let Some(auth) = &auth_basic { + export_args.push("--auth-basic"); + export_args.push(auth); + } + let export_cmd = ExportCreateCommand::parse_from(export_args); + export_cmd + .build() + .await + .context(OtherSnafu)? + .do_work() + .await + .context(OtherSnafu)?; + + let storage_config = ObjectStoreConfig::default(); + let storage = OpenDalStorage::from_uri(&src_uri, &storage_config) + .map_err(BoxedError::new) + .context(OtherSnafu)?; + let mut manifest = storage + .read_manifest() + .await + .map_err(BoxedError::new) + .context(OtherSnafu)?; + if let Some(first_chunk) = manifest.chunks.first_mut() { + first_chunk.status = ChunkStatus::Failed; + } + storage + .write_manifest(&manifest) + .await + .map_err(BoxedError::new) + .context(OtherSnafu)?; + + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + + let mut import_args = vec![ + "import-v2", + "--addr", + &addr, + "--from", + &src_uri, + "--catalog", + &catalog, + "--schemas", + schema, + ]; + if let Some(auth) = &auth_basic { + import_args.push("--auth-basic"); + import_args.push(auth); + } + let import_cmd = ImportV2Command::parse_from(import_args); + let err = import_cmd + .build() + .await + .context(OtherSnafu)? + .do_work() + .await + .expect_err("import should fail on incomplete snapshot"); + assert!(err.to_string().contains("Incomplete snapshot")); + + Ok(()) +} + +#[tokio::test] +#[ignore] +async fn import_v2_schema_filter_data_e2e() -> Result<()> { + let addr = env::var("GREPTIME_ADDR").unwrap_or_else(|_| "127.0.0.1:4000".to_string()); + let catalog = env::var("GREPTIME_CATALOG").unwrap_or_else(|_| "greptime".to_string()); + let auth_basic = env::var("GREPTIME_AUTH_BASIC").ok(); + let schema_a = "test_db_filter_a"; + let schema_b = "test_db_filter_b"; + + let database_client = DatabaseClient::new( + addr.clone(), + catalog.clone(), + auth_basic.clone(), + Duration::from_secs(60), + None, + false, + ); + + for schema in [schema_a, schema_b] { + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + database_client + .sql_in_public(&format!("CREATE DATABASE {schema}")) + .await?; + database_client + .sql( + "CREATE TABLE metrics (\ + ts TIMESTAMP TIME INDEX, \ + host STRING PRIMARY KEY, \ + cpu DOUBLE \ + ) ENGINE=mito", + schema, + ) + .await?; + } + database_client + .sql( + "INSERT INTO metrics (ts, host, cpu) VALUES ('2025-01-01T00:00:00Z', 'a1', 1.0)", + schema_a, + ) + .await?; + database_client + .sql( + "INSERT INTO metrics (ts, host, cpu) VALUES ('2025-01-01T00:00:00Z', 'b1', 2.0)", + schema_b, + ) + .await?; + + let expected_rows_a = query_count(&database_client, schema_a, "metrics").await?; + + let src_dir = tempdir().context(FileIoSnafu)?; + let src_uri = Url::from_directory_path(src_dir.path()) + .map_err(|_| { + InvalidArgumentsSnafu { + msg: "invalid temp dir path".to_string(), + } + .build() + })? + .to_string(); + + let mut export_args = vec![ + "export-v2-create", + "--addr", + &addr, + "--to", + &src_uri, + "--catalog", + &catalog, + "--schemas", + schema_a, + "--schemas", + schema_b, + ]; + if let Some(auth) = &auth_basic { + export_args.push("--auth-basic"); + export_args.push(auth); + } + let export_cmd = ExportCreateCommand::parse_from(export_args); + export_cmd + .build() + .await + .context(OtherSnafu)? + .do_work() + .await + .context(OtherSnafu)?; + + for schema in [schema_a, schema_b] { + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + } + + let mut import_args = vec![ + "import-v2", + "--addr", + &addr, + "--from", + &src_uri, + "--catalog", + &catalog, + "--schemas", + schema_a, + ]; + if let Some(auth) = &auth_basic { + import_args.push("--auth-basic"); + import_args.push(auth); + } + let import_cmd = ImportV2Command::parse_from(import_args); + import_cmd + .build() + .await + .context(OtherSnafu)? + .do_work() + .await + .context(OtherSnafu)?; + + let actual_rows_a = query_count(&database_client, schema_a, "metrics").await?; + assert_eq!(actual_rows_a, expected_rows_a); + + let schema_b_query = database_client + .sql("SELECT COUNT(*) FROM metrics", schema_b) + .await; + assert!(schema_b_query.is_err(), "schema_b should not be imported"); + + for schema in [schema_a, schema_b] { + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + } + + Ok(()) +} + +#[tokio::test] +#[ignore] +async fn export_import_v2_skipped_chunk_e2e() -> Result<()> { + let addr = env::var("GREPTIME_ADDR").unwrap_or_else(|_| "127.0.0.1:4000".to_string()); + let catalog = env::var("GREPTIME_CATALOG").unwrap_or_else(|_| "greptime".to_string()); + let auth_basic = env::var("GREPTIME_AUTH_BASIC").ok(); + let schema = "test_db_skipped_chunk"; + + let database_client = DatabaseClient::new( + addr.clone(), + catalog.clone(), + auth_basic.clone(), + Duration::from_secs(60), + None, + false, + ); + + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + database_client + .sql_in_public(&format!("CREATE DATABASE {schema}")) + .await?; + database_client + .sql( + "CREATE TABLE metrics (\ + ts TIMESTAMP TIME INDEX, \ + host STRING PRIMARY KEY, \ + cpu DOUBLE \ + ) ENGINE=mito", + schema, + ) + .await?; + database_client + .sql( + "INSERT INTO metrics (ts, host, cpu) VALUES \ + ('2025-01-01T00:00:00Z', 'h1', 1.0), \ + ('2025-01-01T01:00:00Z', 'h2', 2.0)", + schema, + ) + .await?; + + let src_dir = tempdir().context(FileIoSnafu)?; + let src_uri = Url::from_directory_path(src_dir.path()) + .map_err(|_| { + InvalidArgumentsSnafu { + msg: "invalid temp dir path".to_string(), + } + .build() + })? + .to_string(); + + let mut export_args = vec![ + "export-v2-create", + "--addr", + &addr, + "--to", + &src_uri, + "--catalog", + &catalog, + "--schemas", + schema, + "--start-time", + "2025-01-01T00:00:00Z", + "--end-time", + "2025-01-01T02:00:00Z", + "--chunk-time-window", + "1h", + ]; + if let Some(auth) = &auth_basic { + export_args.push("--auth-basic"); + export_args.push(auth); + } + let export_cmd = ExportCreateCommand::parse_from(export_args); + export_cmd + .build() + .await + .context(OtherSnafu)? + .do_work() + .await + .context(OtherSnafu)?; + + let storage_config = ObjectStoreConfig::default(); + let storage = OpenDalStorage::from_uri(&src_uri, &storage_config) + .map_err(BoxedError::new) + .context(OtherSnafu)?; + let mut manifest = storage + .read_manifest() + .await + .map_err(BoxedError::new) + .context(OtherSnafu)?; + assert_eq!(manifest.chunks.len(), 2); + manifest.chunks[0].status = ChunkStatus::Skipped; + manifest.chunks[0].files.clear(); + storage + .write_manifest(&manifest) + .await + .map_err(BoxedError::new) + .context(OtherSnafu)?; + + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + + let mut import_args = vec![ + "import-v2", + "--addr", + &addr, + "--from", + &src_uri, + "--catalog", + &catalog, + "--schemas", + schema, + ]; + if let Some(auth) = &auth_basic { + import_args.push("--auth-basic"); + import_args.push(auth); + } + let import_cmd = ImportV2Command::parse_from(import_args); + import_cmd + .build() + .await + .context(OtherSnafu)? + .do_work() + .await + .context(OtherSnafu)?; + + let actual_rows = query_count(&database_client, schema, "metrics").await?; + assert_eq!(actual_rows, 1); + + database_client + .sql_in_public(&format!("DROP DATABASE IF EXISTS {schema}")) + .await?; + + Ok(()) +} diff --git a/src/cli/src/data/import.rs b/src/cli/src/data/import.rs index ffe8b62c7e..f5c234f1a7 100644 --- a/src/cli/src/data/import.rs +++ b/src/cli/src/data/import.rs @@ -81,13 +81,16 @@ pub struct ImportCommand { #[clap(long, value_parser = humantime::parse_duration)] timeout: Option, - /// The proxy server address to connect, if set, will override the system proxy. + /// The proxy server address to connect. /// - /// The default behavior will use the system proxy if neither `proxy` nor `no_proxy` is set. + /// If set, it overrides the system proxy unless `--no-proxy` is specified. + /// If neither `--proxy` nor `--no-proxy` is set, system proxy (env) may be used. #[clap(long)] proxy: Option, - /// Disable proxy server, if set, will not use any proxy. + /// Disable all proxy usage (ignores `--proxy` and system proxy). + /// + /// When set and `--proxy` is not provided, this explicitly disables system proxy. #[clap(long, default_value = "false")] no_proxy: bool, } @@ -104,6 +107,7 @@ impl ImportCommand { // Treats `None` as `0s` to disable server-side default timeout. self.timeout.unwrap_or_default(), proxy, + self.no_proxy, ); Ok(Box::new(Import { @@ -314,6 +318,7 @@ mod tests { None, Duration::from_secs(0), None, + false, ), input_dir: input_dir.to_string(), parallelism: 1, diff --git a/src/cli/src/data/import_v2.rs b/src/cli/src/data/import_v2.rs new file mode 100644 index 0000000000..b197f59b3b --- /dev/null +++ b/src/cli/src/data/import_v2.rs @@ -0,0 +1,43 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Import V2 module. +//! +//! This module provides the V2 implementation of database import functionality, +//! featuring: +//! - DDL-based schema import +//! - Dry-run mode for verification +//! +//! # Example +//! +//! ```bash +//! # Dry-run import (verify without executing) +//! greptime cli data import-v2 \ +//! --addr 127.0.0.1:4000 \ +//! --from file:///tmp/snapshot \ +//! --dry-run +//! +//! # Actual import +//! greptime cli data import-v2 \ +//! --addr 127.0.0.1:4000 \ +//! --from s3://bucket/snapshots/prod-20250101 +//! ``` + +mod command; +pub(crate) mod coordinator; +pub mod error; +pub mod executor; +pub(crate) mod state; + +pub use command::ImportV2Command; diff --git a/src/cli/src/data/import_v2/command.rs b/src/cli/src/data/import_v2/command.rs new file mode 100644 index 0000000000..9ee60252a0 --- /dev/null +++ b/src/cli/src/data/import_v2/command.rs @@ -0,0 +1,974 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Import V2 CLI command. + +use std::collections::HashSet; +use std::time::Duration; + +use async_trait::async_trait; +use clap::Parser; +use common_error::ext::BoxedError; +use common_telemetry::info; +use snafu::{OptionExt, ResultExt}; + +use crate::Tool; +use crate::common::ObjectStoreConfig; +use crate::data::export_v2::data::{build_copy_source, execute_copy_database_from}; +use crate::data::export_v2::manifest::{ChunkMeta, ChunkStatus, DataFormat, MANIFEST_VERSION}; +use crate::data::import_v2::coordinator::{ + ImportResumeConfig, ImportTaskExecutor, build_import_tasks, chunk_has_schema_files, + import_with_resume_session, prepare_import_resume, +}; +use crate::data::import_v2::error::{ + ChunkImportFailedSnafu, EmptyChunkManifestSnafu, ImportStatePathUnavailableSnafu, + IncompleteSnapshotSnafu, ManifestVersionMismatchSnafu, MissingChunkDataSnafu, Result, + SchemaNotInSnapshotSnafu, SnapshotStorageSnafu, +}; +use crate::data::import_v2::executor::{DdlExecutor, DdlStatement}; +use crate::data::import_v2::state::{ImportTaskKey, default_state_path}; +use crate::data::path::{data_dir_for_schema_chunk, ddl_path_for_schema}; +use crate::data::snapshot_storage::{OpenDalStorage, SnapshotStorage, validate_uri}; +use crate::database::{DatabaseClient, parse_proxy_opts}; + +/// Import from a snapshot. +#[derive(Debug, Parser)] +pub struct ImportV2Command { + /// Server address to connect (e.g., 127.0.0.1:4000). + #[clap(long)] + addr: String, + + /// Source snapshot location (e.g., s3://bucket/path, file:///tmp/backup). + #[clap(long)] + from: String, + + /// Target catalog name. + #[clap(long, default_value = "greptime")] + catalog: String, + + /// Schema list to import (default: all in snapshot). + /// Can be specified multiple times or comma-separated. + #[clap(long, value_delimiter = ',')] + schemas: Vec, + + /// Verify without importing (dry-run). + #[clap(long)] + dry_run: bool, + + /// Basic authentication (user:password). + #[clap(long)] + auth_basic: Option, + + /// Request timeout. + #[clap(long, value_parser = humantime::parse_duration)] + timeout: Option, + + /// Proxy server address. + /// + /// If set, it overrides the system proxy unless `--no-proxy` is specified. + /// If neither `--proxy` nor `--no-proxy` is set, system proxy (env) may be used. + #[clap(long)] + proxy: Option, + + /// Disable all proxy usage (ignores `--proxy` and system proxy). + /// + /// When set and `--proxy` is not provided, this explicitly disables system proxy. + #[clap(long)] + no_proxy: bool, + + /// Object store configuration for remote storage backends. + #[clap(flatten)] + storage: ObjectStoreConfig, +} + +impl ImportV2Command { + pub async fn build(&self) -> std::result::Result, BoxedError> { + // Validate URI format + validate_uri(&self.from) + .context(SnapshotStorageSnafu) + .map_err(BoxedError::new)?; + + // Parse schemas (empty vec means all schemas) + let schemas = if self.schemas.is_empty() { + None + } else { + Some(self.schemas.clone()) + }; + + // Build storage + let storage = OpenDalStorage::from_uri(&self.from, &self.storage) + .context(SnapshotStorageSnafu) + .map_err(BoxedError::new)?; + + // Build database client + let proxy = parse_proxy_opts(self.proxy.clone(), self.no_proxy)?; + let database_client = DatabaseClient::new( + self.addr.clone(), + self.catalog.clone(), + self.auth_basic.clone(), + self.timeout.unwrap_or(Duration::from_secs(60)), + proxy, + self.no_proxy, + ); + + Ok(Box::new(Import { + catalog: self.catalog.clone(), + schemas, + dry_run: self.dry_run, + snapshot_uri: self.from.clone(), + storage_config: self.storage.clone(), + storage: Box::new(storage), + database_client, + })) + } +} + +/// Import tool implementation. +pub struct Import { + catalog: String, + schemas: Option>, + dry_run: bool, + snapshot_uri: String, + storage_config: ObjectStoreConfig, + storage: Box, + database_client: DatabaseClient, +} + +#[async_trait] +impl Tool for Import { + async fn do_work(&self) -> std::result::Result<(), BoxedError> { + self.run().await.map_err(BoxedError::new) + } +} + +impl Import { + async fn run(&self) -> Result<()> { + // 1. Read manifest + let manifest = self + .storage + .read_manifest() + .await + .context(SnapshotStorageSnafu)?; + + info!( + "Loading snapshot: {} (version: {}, schema_only: {})", + manifest.snapshot_id, manifest.version, manifest.schema_only + ); + + // Check version compatibility + if manifest.version != MANIFEST_VERSION { + return ManifestVersionMismatchSnafu { + expected: MANIFEST_VERSION, + found: manifest.version, + } + .fail(); + } + + info!("Snapshot contains {} schema(s)", manifest.schemas.len()); + + // 2. Determine schemas to import + let schemas_to_import = match &self.schemas { + Some(filter) => canonicalize_schema_filter(filter, &manifest.schemas)?, + None => manifest.schemas.clone(), + }; + + info!("Importing schemas: {:?}", schemas_to_import); + + // 3. Read DDL statements + let ddl_statements = self.read_ddl_statements(&schemas_to_import).await?; + + info!("Generated {} DDL statements", ddl_statements.len()); + + let data_tasks = if !manifest.schema_only && !manifest.chunks.is_empty() { + validate_data_snapshot(self.storage.as_ref(), &manifest.chunks, &schemas_to_import) + .await?; + build_import_tasks(&manifest.chunks, &schemas_to_import) + } else { + Vec::new() + }; + + // 4. Dry-run mode: print DDL and exit + if self.dry_run { + info!("Dry-run mode - DDL statements to execute:"); + println!(); + for (i, stmt) in ddl_statements.iter().enumerate() { + println!("-- Statement {}", i + 1); + println!("{};", stmt.sql); + println!(); + } + if !manifest.schema_only && !manifest.chunks.is_empty() { + for line in format_data_import_plan(&manifest.chunks, &schemas_to_import) { + println!("{line}"); + } + println!(); + } + return Ok(()); + } + + let mut resume_session = if !data_tasks.is_empty() { + let state_path = default_state_path( + &manifest.snapshot_id.to_string(), + self.database_client.addr(), + &self.catalog, + &schemas_to_import, + ) + .context(ImportStatePathUnavailableSnafu { + snapshot_id: manifest.snapshot_id.to_string(), + })?; + Some( + prepare_import_resume(ImportResumeConfig { + snapshot_id: manifest.snapshot_id.to_string(), + target_addr: self.database_client.addr().to_string(), + catalog: self.catalog.clone(), + schemas: schemas_to_import.clone(), + state_path, + tasks: data_tasks, + }) + .await?, + ) + } else { + None + }; + + let skip_ddl = resume_session + .as_ref() + .map(|session| session.should_skip_ddl()) + .unwrap_or(false); + + // 5. Execute DDL unless a previous run already completed it. + let ddl_executed = if skip_ddl { + info!( + "Existing import state has DDL marked completed; skipping DDL execution and resuming data import" + ); + false + } else { + let executor = DdlExecutor::new(&self.database_client); + executor.execute_strict(&ddl_statements).await?; + if let Some(session) = resume_session.as_mut() { + session.mark_ddl_completed().await?; + } + true + }; + + if let Some(resume_session) = resume_session { + let executor = CopyDatabaseImportTaskExecutor { + import: self, + format: manifest.format, + }; + import_with_resume_session(resume_session, &executor).await?; + } + + if ddl_executed { + info!( + "Import completed: {} DDL statements executed", + ddl_statements.len() + ); + } else { + info!("Import completed: DDL execution skipped"); + } + + Ok(()) + } + + async fn read_ddl_statements(&self, schemas: &[String]) -> Result> { + let mut statements = Vec::new(); + for schema in schemas { + let path = ddl_path_for_schema(schema); + let content = self + .storage + .read_text(&path) + .await + .context(SnapshotStorageSnafu)?; + statements.extend( + parse_ddl_statements(&content) + .into_iter() + .map(|sql| ddl_statement_for_schema(schema, sql)), + ); + } + + Ok(statements) + } +} + +struct CopyDatabaseImportTaskExecutor<'a> { + import: &'a Import, + format: DataFormat, +} + +#[async_trait] +impl ImportTaskExecutor for CopyDatabaseImportTaskExecutor<'_> { + async fn import_task(&self, task: &ImportTaskKey) -> Result<()> { + let source = build_copy_source( + &self.import.snapshot_uri, + &self.import.storage_config, + &task.schema, + task.chunk_id, + ) + .context(ChunkImportFailedSnafu { + chunk_id: task.chunk_id, + schema: task.schema.clone(), + })?; + + execute_copy_database_from( + &self.import.database_client, + &self.import.catalog, + &task.schema, + &source, + self.format, + ) + .await + .context(ChunkImportFailedSnafu { + chunk_id: task.chunk_id, + schema: task.schema.clone(), + }) + } +} + +fn parse_ddl_statements(content: &str) -> Vec { + let mut statements = Vec::new(); + let mut current = String::new(); + let mut chars = content.chars().peekable(); + let mut in_single_quote = false; + let mut in_double_quote = false; + let mut in_line_comment = false; + let mut in_block_comment = false; + + while let Some(ch) = chars.next() { + if in_line_comment { + if ch == '\n' { + in_line_comment = false; + current.push('\n'); + } + continue; + } + + if in_block_comment { + if ch == '*' && chars.peek() == Some(&'/') { + chars.next(); + in_block_comment = false; + } + continue; + } + + if in_single_quote { + current.push(ch); + if ch == '\'' { + if chars.peek() == Some(&'\'') { + current.push(chars.next().expect("peeked quote must exist")); + } else { + in_single_quote = false; + } + } + continue; + } + + if in_double_quote { + current.push(ch); + if ch == '"' { + if chars.peek() == Some(&'"') { + current.push(chars.next().expect("peeked quote must exist")); + } else { + in_double_quote = false; + } + } + continue; + } + + match ch { + '-' if chars.peek() == Some(&'-') => { + chars.next(); + in_line_comment = true; + } + '/' if chars.peek() == Some(&'*') => { + chars.next(); + in_block_comment = true; + } + '\'' => { + in_single_quote = true; + current.push(ch); + } + '"' => { + in_double_quote = true; + current.push(ch); + } + ';' => { + let statement = current.trim(); + if !statement.is_empty() { + statements.push(statement.to_string()); + } + current.clear(); + } + _ => current.push(ch), + } + } + + let statement = current.trim(); + if !statement.is_empty() { + statements.push(statement.to_string()); + } + + statements +} + +fn ddl_statement_for_schema(schema: &str, sql: String) -> DdlStatement { + if is_schema_scoped_statement(&sql) { + DdlStatement::with_execution_schema(sql, schema.to_string()) + } else { + DdlStatement::new(sql) + } +} + +fn is_schema_scoped_statement(sql: &str) -> bool { + let trimmed = sql.trim_start(); + if !starts_with_keyword(trimmed, "CREATE") { + return false; + } + + let Some(rest) = trimmed.get("CREATE".len()..) else { + return false; + }; + let mut rest = rest.trim_start(); + if starts_with_keyword(rest, "OR") { + let Some(next) = rest.get("OR".len()..) else { + return false; + }; + rest = next.trim_start(); + if !starts_with_keyword(rest, "REPLACE") { + return false; + } + let Some(next) = rest.get("REPLACE".len()..) else { + return false; + }; + rest = next.trim_start(); + } + + if starts_with_keyword(rest, "EXTERNAL") { + let Some(next) = rest.get("EXTERNAL".len()..) else { + return false; + }; + rest = next.trim_start(); + } + + starts_with_keyword(rest, "TABLE") || starts_with_keyword(rest, "VIEW") +} + +fn starts_with_keyword(input: &str, keyword: &str) -> bool { + input + .get(0..keyword.len()) + .map(|s| s.eq_ignore_ascii_case(keyword)) + .unwrap_or(false) + && input + .as_bytes() + .get(keyword.len()) + .map(|b| !b.is_ascii_alphanumeric() && *b != b'_') + .unwrap_or(true) +} + +fn canonicalize_schema_filter( + filter: &[String], + manifest_schemas: &[String], +) -> Result> { + let mut canonicalized = Vec::new(); + let mut seen = HashSet::new(); + + for schema in filter { + let canonical = manifest_schemas + .iter() + .find(|candidate| candidate.eq_ignore_ascii_case(schema)) + .cloned() + .ok_or_else(|| { + SchemaNotInSnapshotSnafu { + schema: schema.clone(), + } + .build() + })?; + + if seen.insert(canonical.to_ascii_lowercase()) { + canonicalized.push(canonical); + } + } + + Ok(canonicalized) +} + +fn validate_chunk_statuses(chunks: &[ChunkMeta]) -> Result<()> { + let invalid_chunk = chunks + .iter() + .find(|chunk| !matches!(chunk.status, ChunkStatus::Completed | ChunkStatus::Skipped)); + + if let Some(chunk) = invalid_chunk { + return IncompleteSnapshotSnafu { + chunk_id: chunk.id, + status: chunk.status, + } + .fail(); + } + + Ok(()) +} + +fn format_data_import_plan(chunks: &[ChunkMeta], schemas: &[String]) -> Vec { + let mut lines = vec!["-- Data import plan:".to_string()]; + for chunk in chunks { + lines.push(format!("-- Chunk {}: {:?}", chunk.id, chunk.status)); + for schema in schemas { + if chunk_has_schema_files(chunk, schema) { + lines.push(format!("-- {} -> COPY DATABASE FROM", schema)); + } + } + } + lines +} + +async fn validate_data_snapshot( + storage: &dyn SnapshotStorage, + chunks: &[ChunkMeta], + schemas: &[String], +) -> Result<()> { + validate_chunk_statuses(chunks)?; + let actual_prefixes = collect_chunk_data_prefixes(storage).await?; + + for chunk in chunks { + if chunk.status == ChunkStatus::Skipped { + continue; + } + if chunk.files.is_empty() { + return EmptyChunkManifestSnafu { chunk_id: chunk.id }.fail(); + } + for schema in schemas { + validate_chunk_schema_files(chunk, schema, &actual_prefixes)?; + } + } + + Ok(()) +} + +async fn collect_chunk_data_prefixes(storage: &dyn SnapshotStorage) -> Result> { + let files = storage + .list_files_recursive("data/") + .await + .context(SnapshotStorageSnafu)?; + let mut prefixes = HashSet::new(); + + for path in files { + let normalized = path.trim_start_matches('/'); + let mut parts = normalized.splitn(4, '/'); + let Some(root) = parts.next() else { + continue; + }; + let Some(schema) = parts.next() else { + continue; + }; + let Some(chunk_id) = parts.next() else { + continue; + }; + if root != "data" { + continue; + } + prefixes.insert(format!("data/{schema}/{chunk_id}/")); + } + + Ok(prefixes) +} + +fn validate_chunk_schema_files( + chunk: &ChunkMeta, + schema: &str, + actual_prefixes: &HashSet, +) -> Result { + if !chunk_has_schema_files(chunk, schema) { + return Ok(false); + } + + let prefix = data_dir_for_schema_chunk(schema, chunk.id); + if !actual_prefixes.contains(&prefix) { + return MissingChunkDataSnafu { + chunk_id: chunk.id, + schema: schema.to_string(), + path: prefix, + } + .fail(); + } + + Ok(true) +} + +#[cfg(test)] +mod tests { + use std::collections::{HashMap, HashSet}; + + use async_trait::async_trait; + + use super::*; + use crate::data::export_v2::manifest::{ChunkMeta, ChunkStatus, Manifest, TimeRange}; + use crate::data::export_v2::schema::SchemaSnapshot; + use crate::data::snapshot_storage::SnapshotStorage; + + struct StubStorage { + manifest: Manifest, + files_by_prefix: HashMap>, + } + + #[async_trait] + impl SnapshotStorage for StubStorage { + async fn exists(&self) -> crate::data::export_v2::error::Result { + Ok(true) + } + + async fn read_manifest(&self) -> crate::data::export_v2::error::Result { + Ok(self.manifest.clone()) + } + + async fn write_manifest( + &self, + _manifest: &Manifest, + ) -> crate::data::export_v2::error::Result<()> { + unimplemented!("not needed in import_v2::command tests") + } + + async fn read_text(&self, _path: &str) -> crate::data::export_v2::error::Result { + unimplemented!("not needed in import_v2::command tests") + } + + async fn write_text( + &self, + _path: &str, + _content: &str, + ) -> crate::data::export_v2::error::Result<()> { + unimplemented!("not needed in import_v2::command tests") + } + + async fn write_schema( + &self, + _snapshot: &SchemaSnapshot, + ) -> crate::data::export_v2::error::Result<()> { + unimplemented!("not needed in import_v2::command tests") + } + + async fn create_dir_all(&self, _path: &str) -> crate::data::export_v2::error::Result<()> { + unimplemented!("not needed in import_v2::command tests") + } + + async fn list_files_recursive( + &self, + prefix: &str, + ) -> crate::data::export_v2::error::Result> { + Ok(self + .files_by_prefix + .iter() + .filter(|(candidate, _)| candidate.starts_with(prefix)) + .flat_map(|(_, files)| files.clone()) + .collect()) + } + + async fn delete_snapshot(&self) -> crate::data::export_v2::error::Result<()> { + unimplemented!("not needed in import_v2::command tests") + } + } + + #[test] + fn test_parse_ddl_statements() { + let content = r#" +-- Schema: public +CREATE DATABASE public; +CREATE TABLE t (ts TIMESTAMP TIME INDEX, host STRING, PRIMARY KEY (host)) ENGINE=mito; + +-- comment +CREATE VIEW v AS SELECT * FROM t; +"#; + let statements = parse_ddl_statements(content); + assert_eq!(statements.len(), 3); + assert!(statements[0].starts_with("CREATE DATABASE public")); + assert!(statements[1].starts_with("CREATE TABLE t")); + assert!(statements[2].starts_with("CREATE VIEW v")); + } + + #[test] + fn test_parse_ddl_statements_preserves_semicolons_in_string_literals() { + let content = r#" +CREATE TABLE t ( + host STRING DEFAULT 'a;b' +); +CREATE VIEW v AS SELECT ';' AS marker; +"#; + + let statements = parse_ddl_statements(content); + + assert_eq!(statements.len(), 2); + assert!(statements[0].contains("'a;b'")); + assert!(statements[1].contains("';' AS marker")); + } + + #[test] + fn test_parse_ddl_statements_handles_comments_without_splitting() { + let content = r#" +-- leading comment +CREATE TABLE t (ts TIMESTAMP TIME INDEX); /* block; comment */ +CREATE VIEW v AS SELECT 1; +"#; + + let statements = parse_ddl_statements(content); + + assert_eq!(statements.len(), 2); + assert!(statements[0].starts_with("CREATE TABLE t")); + assert!(statements[1].starts_with("CREATE VIEW v")); + } + + #[test] + fn test_canonicalize_schema_filter_uses_manifest_casing() { + let filter = vec!["TEST_DB".to_string(), "PUBLIC".to_string()]; + let manifest_schemas = vec!["test_db".to_string(), "public".to_string()]; + + let canonicalized = canonicalize_schema_filter(&filter, &manifest_schemas).unwrap(); + + assert_eq!(canonicalized, vec!["test_db", "public"]); + } + + #[test] + fn test_canonicalize_schema_filter_dedupes_case_insensitive_matches() { + let filter = vec![ + "TEST_DB".to_string(), + "test_db".to_string(), + "PUBLIC".to_string(), + "public".to_string(), + ]; + let manifest_schemas = vec!["test_db".to_string(), "public".to_string()]; + + let canonicalized = canonicalize_schema_filter(&filter, &manifest_schemas).unwrap(); + + assert_eq!(canonicalized, vec!["test_db", "public"]); + } + + #[test] + fn test_canonicalize_schema_filter_rejects_missing_schema() { + let filter = vec!["missing".to_string()]; + let manifest_schemas = vec!["test_db".to_string()]; + + let error = canonicalize_schema_filter(&filter, &manifest_schemas) + .expect_err("missing schema should fail") + .to_string(); + + assert!(error.contains("missing")); + } + + #[test] + fn test_ddl_statement_for_schema_create_table_uses_execution_schema() { + let stmt = ddl_statement_for_schema( + "test_db", + "CREATE TABLE metrics (ts TIMESTAMP TIME INDEX) ENGINE=mito".to_string(), + ); + assert_eq!(stmt.execution_schema.as_deref(), Some("test_db")); + } + + #[test] + fn test_ddl_statement_for_schema_create_view_uses_execution_schema() { + let stmt = ddl_statement_for_schema( + "test_db", + "CREATE VIEW metrics_view AS SELECT * FROM metrics".to_string(), + ); + assert_eq!(stmt.execution_schema.as_deref(), Some("test_db")); + } + + #[test] + fn test_ddl_statement_for_schema_create_or_replace_view_uses_execution_schema() { + let stmt = ddl_statement_for_schema( + "test_db", + "CREATE OR REPLACE VIEW metrics_view AS SELECT * FROM metrics".to_string(), + ); + assert_eq!(stmt.execution_schema.as_deref(), Some("test_db")); + } + + #[test] + fn test_ddl_statement_for_schema_create_external_table_uses_execution_schema() { + let stmt = ddl_statement_for_schema( + "test_db", + "CREATE EXTERNAL TABLE IF NOT EXISTS ext_metrics (ts TIMESTAMP TIME INDEX) ENGINE=file" + .to_string(), + ); + assert_eq!(stmt.execution_schema.as_deref(), Some("test_db")); + } + + #[test] + fn test_ddl_statement_for_schema_create_database_uses_public_context() { + let stmt = ddl_statement_for_schema("test_db", "CREATE DATABASE test_db".to_string()); + assert_eq!(stmt.execution_schema, None); + } + + #[test] + fn test_starts_with_keyword_requires_word_boundary() { + assert!(starts_with_keyword("CREATE TABLE t", "CREATE")); + assert!(!starts_with_keyword("CREATED TABLE t", "CREATE")); + assert!(!starts_with_keyword("TABLESPACE foo", "TABLE")); + } + + #[test] + fn test_validate_chunk_statuses_rejects_failed_chunk() { + let mut failed = ChunkMeta::new(3, TimeRange::unbounded()); + failed.status = ChunkStatus::Failed; + + let error = validate_chunk_statuses(&[failed]).expect_err("failed chunk should error"); + assert!(error.to_string().contains("Incomplete snapshot")); + } + + #[test] + fn test_validate_chunk_statuses_accepts_completed_and_skipped_chunks() { + let mut completed = ChunkMeta::new(1, TimeRange::unbounded()); + completed.status = ChunkStatus::Completed; + let skipped = ChunkMeta::skipped(2, TimeRange::unbounded()); + + assert!(validate_chunk_statuses(&[completed, skipped]).is_ok()); + } + + #[test] + fn test_chunk_has_schema_files_matches_encoded_schema_prefix() { + let mut chunk = ChunkMeta::new(7, TimeRange::unbounded()); + chunk.files = vec![ + "data/public/7/a.parquet".to_string(), + "data/%E6%B5%8B%E8%AF%95/7/b.parquet".to_string(), + ]; + + assert!(chunk_has_schema_files(&chunk, "public")); + assert!(chunk_has_schema_files(&chunk, "测试")); + assert!(!chunk_has_schema_files(&chunk, "metrics")); + } + + #[test] + fn test_format_data_import_plan_includes_matching_schemas_only() { + let mut completed = ChunkMeta::new(1, TimeRange::unbounded()); + completed.status = ChunkStatus::Completed; + completed.files = vec![ + "data/public/1/a.parquet".to_string(), + "data/%E6%B5%8B%E8%AF%95/1/b.parquet".to_string(), + ]; + let skipped = ChunkMeta::skipped(2, TimeRange::unbounded()); + + let lines = format_data_import_plan( + &[completed, skipped], + &[ + "public".to_string(), + "测试".to_string(), + "metrics".to_string(), + ], + ); + + assert_eq!(lines[0], "-- Data import plan:"); + assert!(lines.contains(&"-- Chunk 1: Completed".to_string())); + assert!(lines.contains(&"-- public -> COPY DATABASE FROM".to_string())); + assert!(lines.contains(&"-- 测试 -> COPY DATABASE FROM".to_string())); + assert!(!lines.contains(&"-- metrics -> COPY DATABASE FROM".to_string())); + assert!(lines.contains(&"-- Chunk 2: Skipped".to_string())); + } + + #[tokio::test] + async fn test_collect_chunk_data_prefixes_indexes_present_prefixes() { + let storage = StubStorage { + manifest: Manifest::new_schema_only("greptime".to_string(), vec!["public".to_string()]), + files_by_prefix: HashMap::from([ + ( + "data/public/7/".to_string(), + vec!["data/public/7/a.parquet".to_string()], + ), + ( + "data/%E6%B5%8B%E8%AF%95/9/".to_string(), + vec!["data/%E6%B5%8B%E8%AF%95/9/b.parquet".to_string()], + ), + ]), + }; + + let prefixes = collect_chunk_data_prefixes(&storage).await.unwrap(); + + assert!(prefixes.contains("data/public/7/")); + assert!(prefixes.contains("data/%E6%B5%8B%E8%AF%95/9/")); + } + + #[test] + fn test_validate_chunk_schema_files_accepts_present_prefix() { + let mut chunk = ChunkMeta::new(7, TimeRange::unbounded()); + chunk.files = vec!["data/public/7/a.parquet".to_string()]; + let actual_prefixes = HashSet::from(["data/public/7/".to_string()]); + + assert!(validate_chunk_schema_files(&chunk, "public", &actual_prefixes).unwrap()); + } + + #[test] + fn test_validate_chunk_schema_files_rejects_missing_prefix() { + let mut chunk = ChunkMeta::new(7, TimeRange::unbounded()); + chunk.files = vec!["data/public/7/a.parquet".to_string()]; + + let error = validate_chunk_schema_files(&chunk, "public", &HashSet::new()) + .expect_err("missing chunk prefix should fail") + .to_string(); + assert!(error.contains("marked completed but no files were found")); + } + + #[test] + fn test_validate_chunk_schema_files_skips_absent_schema() { + let mut chunk = ChunkMeta::new(7, TimeRange::unbounded()); + chunk.files = vec!["data/public/7/a.parquet".to_string()]; + + assert!(!validate_chunk_schema_files(&chunk, "metrics", &HashSet::new()).unwrap()); + } + + #[tokio::test] + async fn test_validate_data_snapshot_rejects_failed_chunk_before_dry_run() { + let mut failed = ChunkMeta::new(3, TimeRange::unbounded()); + failed.status = ChunkStatus::Failed; + + let storage = StubStorage { + manifest: Manifest::new_schema_only("greptime".to_string(), vec!["public".to_string()]), + files_by_prefix: HashMap::new(), + }; + + let error = validate_data_snapshot(&storage, &[failed], &["public".to_string()]) + .await + .expect_err("failed chunk should reject dry-run validation") + .to_string(); + assert!(error.contains("Incomplete snapshot")); + } + + #[tokio::test] + async fn test_validate_data_snapshot_rejects_missing_chunk_prefix_before_dry_run() { + let mut completed = ChunkMeta::new(7, TimeRange::unbounded()); + completed.status = ChunkStatus::Completed; + completed.files = vec!["data/public/7/a.parquet".to_string()]; + + let storage = StubStorage { + manifest: Manifest::new_schema_only("greptime".to_string(), vec!["public".to_string()]), + files_by_prefix: HashMap::new(), + }; + + let error = validate_data_snapshot(&storage, &[completed], &["public".to_string()]) + .await + .expect_err("missing chunk prefix should reject dry-run validation") + .to_string(); + assert!(error.contains("marked completed but no files were found")); + } + + #[tokio::test] + async fn test_validate_data_snapshot_rejects_completed_chunk_with_empty_manifest() { + let mut completed = ChunkMeta::new(7, TimeRange::unbounded()); + completed.status = ChunkStatus::Completed; + + let storage = StubStorage { + manifest: Manifest::new_schema_only("greptime".to_string(), vec!["public".to_string()]), + files_by_prefix: HashMap::new(), + }; + + let error = validate_data_snapshot(&storage, &[completed], &["public".to_string()]) + .await + .expect_err("empty completed chunk should reject validation") + .to_string(); + assert!(error.contains("file manifest is empty")); + } +} diff --git a/src/cli/src/data/import_v2/coordinator.rs b/src/cli/src/data/import_v2/coordinator.rs new file mode 100644 index 0000000000..fe2d016464 --- /dev/null +++ b/src/cli/src/data/import_v2/coordinator.rs @@ -0,0 +1,695 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::BTreeSet; +use std::path::{Path, PathBuf}; +use std::time::Instant; + +use async_trait::async_trait; +use common_telemetry::{info, warn}; + +use crate::data::export_v2::manifest::{ChunkMeta, ChunkStatus}; +use crate::data::import_v2::error::{ + ImportStateDdlIncompleteSnafu, ImportStateMismatchSnafu, Result, +}; +use crate::data::import_v2::state::{ + ImportState, ImportStateLockGuard, ImportTaskKey, ImportTaskStatus, canonical_schema_selection, + delete_import_state, load_import_state, save_import_state, try_acquire_import_state_lock, +}; +use crate::data::path::data_dir_for_schema_chunk; + +#[async_trait] +pub(crate) trait ImportTaskExecutor { + async fn import_task(&self, task: &ImportTaskKey) -> Result<()>; +} + +pub(crate) struct ImportResumeConfig { + pub(crate) snapshot_id: String, + pub(crate) target_addr: String, + pub(crate) catalog: String, + pub(crate) schemas: Vec, + pub(crate) state_path: PathBuf, + pub(crate) tasks: Vec, +} + +pub(crate) struct ImportResumeSession { + config: ImportResumeConfig, + state: ImportState, + lock: ImportStateLockGuard, +} + +impl ImportResumeSession { + pub(crate) fn should_skip_ddl(&self) -> bool { + self.state.ddl_completed + } + + /// Marks DDL as completed and persists the state. Must be called after a + /// successful DDL run on a fresh session, so that crashes after this point + /// resume into the data-import phase instead of replaying DDL. + pub(crate) async fn mark_ddl_completed(&mut self) -> Result<()> { + self.state.mark_ddl_completed(); + save_import_state(&self.config.state_path, &self.state).await + } +} + +pub(crate) fn chunk_has_schema_files(chunk: &ChunkMeta, schema: &str) -> bool { + let prefix = data_dir_for_schema_chunk(schema, chunk.id); + chunk.files.iter().any(|path| { + let normalized = path.trim_start_matches('/'); + normalized.starts_with(&prefix) + }) +} + +pub(crate) fn build_import_tasks(chunks: &[ChunkMeta], schemas: &[String]) -> Vec { + let mut tasks = Vec::new(); + for chunk in chunks { + if chunk.status == ChunkStatus::Skipped { + continue; + } + // TODO: build a per-chunk schema index if chunk file manifests become large. + for schema in schemas { + if chunk_has_schema_files(chunk, schema) { + tasks.push(ImportTaskKey::new(chunk.id, schema.clone())); + } + } + } + tasks +} + +pub(crate) async fn prepare_import_resume( + config: ImportResumeConfig, +) -> Result { + // Validate the request before touching the state file or acquiring the + // lock. Duplicate task keys would corrupt the resume bookkeeping because + // status lookups use linear `find()` and only ever see the first match. + validate_config_tasks(&config)?; + + let lock = try_acquire_import_state_lock(&config.state_path)?; + let state = match load_import_state(&config.state_path).await? { + Some(loaded) => { + validate_state_matches(&loaded, &config)?; + loaded + } + None => { + // Persist a fresh state immediately so that any crash after this + // point is recoverable as a resume. `ddl_completed=false` on a + // loaded state therefore means a previous run reached this point + // but did not confirm DDL completion - DDL must be (re-)run before + // data import is allowed. + let fresh = ImportState::new( + &config.snapshot_id, + &config.target_addr, + &config.catalog, + &config.schemas, + config.tasks.clone(), + ); + save_import_state(&config.state_path, &fresh).await?; + fresh + } + }; + + Ok(ImportResumeSession { + config, + state, + lock, + }) +} + +pub(crate) async fn import_with_resume_session( + session: ImportResumeSession, + executor: &E, +) -> Result<()> +where + E: ImportTaskExecutor + Sync, +{ + let ImportResumeSession { + config, + mut state, + lock, + } = session; + + // The state machine requires DDL to be explicitly marked completed before + // data import; otherwise a caller could import data and leave a state that + // replays DDL on the next resume. Surface the misuse instead of silently + // importing. + if !state.ddl_completed { + return ImportStateDdlIncompleteSnafu { + path: config.state_path.display().to_string(), + } + .fail(); + } + + let completed = state + .tasks + .iter() + .filter(|task| task.status == ImportTaskStatus::Completed) + .count(); + info!( + "Import resume state: {} completed, {} pending, path: {}", + completed, + state.tasks.len().saturating_sub(completed), + config.state_path.display() + ); + + let import_start = Instant::now(); + for (idx, task) in config.tasks.iter().enumerate() { + if state.task_status(task.chunk_id, &task.schema) == Some(ImportTaskStatus::Completed) { + info!( + "[{}/{}] Chunk {} schema {}: already completed, skipped", + idx + 1, + config.tasks.len(), + task.chunk_id, + task.schema + ); + continue; + } + + info!( + "[{}/{}] Chunk {} schema {}: importing...", + idx + 1, + config.tasks.len(), + task.chunk_id, + task.schema + ); + state.set_task_status( + task.chunk_id, + &task.schema, + ImportTaskStatus::InProgress, + None, + )?; + save_import_state(&config.state_path, &state).await?; + + let task_start = Instant::now(); + let result = executor.import_task(task).await; + + match result { + Ok(()) => { + // The task itself succeeded. If we cannot persist the + // Completed marker, the next resume will replay it (potentially + // duplicating data depending on engine semantics), but we must + // not pretend the import as a whole failed - return the persist + // error so the operator notices, after logging the success. + update_status_and_save( + &config, + &mut state, + task, + ImportTaskStatus::Completed, + None, + ) + .await?; + info!( + "[{}/{}] Chunk {} schema {}: done in {:?}", + idx + 1, + config.tasks.len(), + task.chunk_id, + task.schema, + task_start.elapsed() + ); + } + Err(task_error) => { + // Persist Failed best-effort, but always surface the original + // task error to the caller. State persistence problems are + // logged so they are not silently lost. + if let Err(persist_error) = update_status_and_save( + &config, + &mut state, + task, + ImportTaskStatus::Failed, + Some(task_error.to_string()), + ) + .await + { + warn!( + "Failed to persist Failed status for chunk {} schema {} after task error ({}); state file may be out of date: {}", + task.chunk_id, task.schema, task_error, persist_error + ); + } + return Err(task_error); + } + } + } + + delete_import_state(&config.state_path).await?; + info!("Data import finished in {:?}", import_start.elapsed()); + drop(lock); + Ok(()) +} + +async fn update_status_and_save( + config: &ImportResumeConfig, + state: &mut ImportState, + task: &ImportTaskKey, + status: ImportTaskStatus, + error_message: Option, +) -> Result<()> { + // set_task_status only fails if the task isn't in the state; that would + // indicate a logic bug since `task` came from the same config. Surface it + // instead of swallowing. + state.set_task_status(task.chunk_id, &task.schema, status, error_message)?; + save_import_state(&config.state_path, state).await +} + +fn validate_state_matches(state: &ImportState, config: &ImportResumeConfig) -> Result<()> { + if state.snapshot_id != config.snapshot_id { + return state_mismatch( + config, + format!( + "snapshot_id differs (state: {}, requested: {})", + state.snapshot_id, config.snapshot_id + ), + ); + } + // Target addresses are compared literally; hostname normalization is left to the caller. + if state.target_addr != config.target_addr { + return state_mismatch( + config, + format!( + "target_addr differs (state: {}, requested: {})", + state.target_addr, config.target_addr + ), + ); + } + if state.catalog != config.catalog { + return state_mismatch( + config, + format!( + "catalog differs (state: {}, requested: {})", + state.catalog, config.catalog + ), + ); + } + + let requested_schemas = canonical_schema_selection(&config.schemas); + if state.schemas != requested_schemas { + return state_mismatch( + config, + format!( + "schemas differ (state: {:?}, requested: {:?})", + state.schemas, requested_schemas + ), + ); + } + + if task_set_from_state(state, &config.state_path)? != task_set_from_config(config)? { + return state_mismatch(config, "task set differs".to_string()); + } + + Ok(()) +} + +fn state_mismatch(config: &ImportResumeConfig, reason: String) -> Result<()> { + ImportStateMismatchSnafu { + path: config.state_path.display().to_string(), + reason, + } + .fail() +} + +fn task_set_from_state<'a>( + state: &'a ImportState, + state_path: &Path, +) -> Result> { + let mut tasks = BTreeSet::new(); + for task in &state.tasks { + if !tasks.insert((task.chunk_id, task.schema.as_str())) { + return ImportStateMismatchSnafu { + path: state_path.display().to_string(), + reason: format!( + "duplicate task key in state (chunk_id: {}, schema: {})", + task.chunk_id, task.schema + ), + } + .fail(); + } + } + Ok(tasks) +} + +fn task_set_from_config(config: &ImportResumeConfig) -> Result> { + let mut tasks = BTreeSet::new(); + for task in &config.tasks { + if !tasks.insert((task.chunk_id, task.schema.as_str())) { + return ImportStateMismatchSnafu { + path: config.state_path.display().to_string(), + reason: format!( + "duplicate task key in request (chunk_id: {}, schema: {})", + task.chunk_id, task.schema + ), + } + .fail(); + } + } + Ok(tasks) +} + +fn validate_config_tasks(config: &ImportResumeConfig) -> Result<()> { + task_set_from_config(config).map(|_| ()) +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + + use super::*; + use crate::data::export_v2::manifest::{ChunkMeta, TimeRange}; + use crate::data::import_v2::error::TestTaskFailedSnafu; + + #[derive(Debug, Clone, Copy)] + enum FailureMode { + Fatal, + RetryableThenSuccess { failures: usize }, + } + + struct RecordingExecutor { + imported: Arc>>, + fail_task: Option, + failure_mode: Option, + attempts: Arc, + } + + #[async_trait] + impl ImportTaskExecutor for RecordingExecutor { + async fn import_task(&self, task: &ImportTaskKey) -> Result<()> { + let attempt = self.attempts.fetch_add(1, Ordering::SeqCst); + if self.fail_task.as_ref() == Some(task) { + match self.failure_mode { + Some(FailureMode::Fatal) => { + return TestTaskFailedSnafu { + message: "fatal failure".to_string(), + retryable: false, + } + .fail(); + } + Some(FailureMode::RetryableThenSuccess { failures }) if attempt < failures => { + return TestTaskFailedSnafu { + message: "retryable failure".to_string(), + retryable: true, + } + .fail(); + } + _ => {} + } + } + self.imported.lock().unwrap().push(task.clone()); + Ok(()) + } + } + + fn recording_executor(imported: Arc>>) -> RecordingExecutor { + RecordingExecutor { + imported, + fail_task: None, + failure_mode: None, + attempts: Arc::new(AtomicUsize::new(0)), + } + } + + fn config(path: PathBuf, tasks: Vec) -> ImportResumeConfig { + ImportResumeConfig { + snapshot_id: "snapshot-1".to_string(), + target_addr: "127.0.0.1:4000".to_string(), + catalog: "greptime".to_string(), + schemas: vec!["public".to_string(), "analytics".to_string()], + state_path: path, + tasks, + } + } + + async fn run_import_with_resume(config: ImportResumeConfig, executor: &E) -> Result<()> + where + E: ImportTaskExecutor + Sync, + { + // Mirror the production caller: mark DDL completed for fresh sessions + // so the data-import guard is satisfied. Tests that want to exercise + // the unsafe path drive prepare/import directly. + let mut session = prepare_import_resume(config).await?; + if !session.should_skip_ddl() { + session.mark_ddl_completed().await?; + } + import_with_resume_session(session, executor).await + } + + #[test] + fn test_build_import_tasks_skips_skipped_chunks_and_missing_schema_files() { + let mut completed = ChunkMeta::new(1, TimeRange::unbounded()); + completed.status = ChunkStatus::Completed; + completed.files = vec!["data/public/1/file.parquet".to_string()]; + let mut skipped = ChunkMeta::new(2, TimeRange::unbounded()); + skipped.status = ChunkStatus::Skipped; + skipped.files = vec!["data/public/2/file.parquet".to_string()]; + + let tasks = build_import_tasks( + &[completed, skipped], + &["public".to_string(), "analytics".to_string()], + ); + + assert_eq!(tasks, vec![ImportTaskKey::new(1, "public")]); + } + + #[tokio::test] + async fn test_import_with_resume_skips_completed_tasks() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("import_state.json"); + let tasks = vec![ + ImportTaskKey::new(1, "public"), + ImportTaskKey::new(2, "analytics"), + ]; + let mut state = ImportState::new( + "snapshot-1", + "127.0.0.1:4000", + "greptime", + &["public".to_string(), "analytics".to_string()], + tasks.clone(), + ); + state.mark_ddl_completed(); + state + .set_task_status(1, "public", ImportTaskStatus::Completed, None) + .unwrap(); + save_import_state(&path, &state).await.unwrap(); + + let imported = Arc::new(Mutex::new(Vec::new())); + let executor = recording_executor(imported.clone()); + + run_import_with_resume(config(path.clone(), tasks), &executor) + .await + .unwrap(); + + assert_eq!( + imported.lock().unwrap().clone(), + vec![ImportTaskKey::new(2, "analytics")] + ); + assert!(load_import_state(&path).await.unwrap().is_none()); + } + + #[tokio::test] + async fn test_import_with_resume_persists_failed_task() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("import_state.json"); + let failed_task = ImportTaskKey::new(1, "public"); + let tasks = vec![failed_task.clone()]; + let imported = Arc::new(Mutex::new(Vec::new())); + let executor = RecordingExecutor { + imported, + fail_task: Some(failed_task.clone()), + failure_mode: Some(FailureMode::Fatal), + attempts: Arc::new(AtomicUsize::new(0)), + }; + + let error = run_import_with_resume(config(path.clone(), tasks), &executor) + .await + .unwrap_err(); + assert!(matches!( + error, + crate::data::import_v2::error::Error::TestTaskFailed { + retryable: false, + .. + } + )); + + let state = load_import_state(&path).await.unwrap().unwrap(); + assert_eq!( + state.task_status(failed_task.chunk_id, &failed_task.schema), + Some(ImportTaskStatus::Failed) + ); + } + + #[tokio::test] + async fn test_import_with_resume_rejects_mismatched_state_identity() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("import_state.json"); + let tasks = vec![ImportTaskKey::new(1, "public")]; + let state = ImportState::new( + "snapshot-1", + "127.0.0.1:4001", + "greptime", + &["public".to_string(), "analytics".to_string()], + tasks.clone(), + ); + save_import_state(&path, &state).await.unwrap(); + + let imported = Arc::new(Mutex::new(Vec::new())); + let executor = recording_executor(imported); + + let error = run_import_with_resume(config(path, tasks), &executor) + .await + .unwrap_err(); + + assert!(matches!( + error, + crate::data::import_v2::error::Error::ImportStateMismatch { .. } + )); + } + + #[tokio::test] + async fn test_prepare_import_resume_reports_existing_state_before_ddl() { + let dir = tempfile::tempdir().unwrap(); + let tasks = vec![ImportTaskKey::new(1, "public")]; + + let fresh_session = + prepare_import_resume(config(dir.path().join("fresh_state.json"), tasks.clone())) + .await + .unwrap(); + assert!(!fresh_session.should_skip_ddl()); + drop(fresh_session); + + let existing_path = dir.path().join("existing_state.json"); + let mut state = ImportState::new( + "snapshot-1", + "127.0.0.1:4000", + "greptime", + &["public".to_string(), "analytics".to_string()], + tasks.clone(), + ); + state.mark_ddl_completed(); + save_import_state(&existing_path, &state).await.unwrap(); + + let resume_session = prepare_import_resume(config(existing_path, tasks)) + .await + .unwrap(); + assert!(resume_session.should_skip_ddl()); + } + + #[tokio::test] + async fn test_import_with_resume_rejects_duplicate_state_tasks() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("import_state.json"); + let tasks = vec![ImportTaskKey::new(1, "public")]; + let mut state = ImportState::new( + "snapshot-1", + "127.0.0.1:4000", + "greptime", + &["public".to_string(), "analytics".to_string()], + tasks.clone(), + ); + state.tasks.push(state.tasks[0].clone()); + save_import_state(&path, &state).await.unwrap(); + + let imported = Arc::new(Mutex::new(Vec::new())); + let executor = recording_executor(imported); + + let error = run_import_with_resume(config(path, tasks), &executor) + .await + .unwrap_err(); + + assert!(matches!( + error, + crate::data::import_v2::error::Error::ImportStateMismatch { .. } + )); + } + + #[tokio::test] + async fn test_import_with_resume_rejects_data_import_when_ddl_incomplete() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("import_state.json"); + let tasks = vec![ImportTaskKey::new(1, "public")]; + + // prepare creates fresh state with ddl_completed=false; calling + // import_with_resume_session directly (without mark_ddl_completed) + // must be rejected. + let session = prepare_import_resume(config(path, tasks)).await.unwrap(); + let imported = Arc::new(Mutex::new(Vec::new())); + let executor = recording_executor(imported.clone()); + + let error = import_with_resume_session(session, &executor) + .await + .unwrap_err(); + + assert!(matches!( + error, + crate::data::import_v2::error::Error::ImportStateDdlIncomplete { .. } + )); + assert!(imported.lock().unwrap().is_empty()); + } + + #[tokio::test] + async fn test_prepare_import_resume_rejects_duplicate_request_tasks_on_fresh_state() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("import_state.json"); + let task = ImportTaskKey::new(1, "public"); + // No state file yet - duplicate detection must run before the fresh + // state is persisted, otherwise corrupted bookkeeping would be + // written to disk and observed only on a later resume. + let error = + match prepare_import_resume(config(path.clone(), vec![task.clone(), task])).await { + Ok(_) => panic!("duplicate request tasks should be rejected"), + Err(error) => error, + }; + + assert!(matches!( + error, + crate::data::import_v2::error::Error::ImportStateMismatch { .. } + )); + assert!(load_import_state(&path).await.unwrap().is_none()); + } + + #[tokio::test] + async fn test_import_with_resume_does_not_retry_retryable_task_error() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("import_state.json"); + let failed_task = ImportTaskKey::new(1, "public"); + let tasks = vec![failed_task.clone()]; + let imported = Arc::new(Mutex::new(Vec::new())); + let attempts = Arc::new(AtomicUsize::new(0)); + let executor = RecordingExecutor { + imported: imported.clone(), + fail_task: Some(failed_task.clone()), + // If task import were retried, the second attempt would succeed. + // COPY DATABASE FROM failures are ambiguous, so retryable errors + // must still stop immediately to avoid duplicate rows. + failure_mode: Some(FailureMode::RetryableThenSuccess { failures: 1 }), + attempts: attempts.clone(), + }; + + let error = run_import_with_resume(config(path.clone(), tasks), &executor) + .await + .unwrap_err(); + + assert!(matches!( + error, + crate::data::import_v2::error::Error::TestTaskFailed { + retryable: true, + .. + } + )); + assert_eq!(attempts.load(Ordering::SeqCst), 1); + assert!(imported.lock().unwrap().is_empty()); + + let state = load_import_state(&path).await.unwrap().unwrap(); + assert_eq!( + state.task_status(failed_task.chunk_id, &failed_task.schema), + Some(ImportTaskStatus::Failed) + ); + } +} diff --git a/src/cli/src/data/import_v2/error.rs b/src/cli/src/data/import_v2/error.rs new file mode 100644 index 0000000000..165f1c0118 --- /dev/null +++ b/src/cli/src/data/import_v2/error.rs @@ -0,0 +1,222 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::any::Any; + +use common_error::ext::ErrorExt; +use common_error::status_code::StatusCode; +use common_macro::stack_trace_debug; +use snafu::{Location, Snafu}; + +use crate::data::export_v2::manifest::ChunkStatus; + +#[derive(Snafu)] +#[snafu(visibility(pub))] +#[stack_trace_debug] +pub enum Error { + #[snafu(display("Snapshot not found at '{}'", uri))] + SnapshotNotFound { + uri: String, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Manifest version mismatch: expected {}, found {}", expected, found))] + ManifestVersionMismatch { + expected: u32, + found: u32, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Schema '{}' not found in snapshot", schema))] + SchemaNotInSnapshot { + schema: String, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Incomplete snapshot: chunk {} has status {:?}", chunk_id, status))] + IncompleteSnapshot { + chunk_id: u32, + status: ChunkStatus, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display( + "Snapshot is inconsistent: chunk {} is marked completed but its file manifest is empty", + chunk_id + ))] + EmptyChunkManifest { + chunk_id: u32, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display( + "Snapshot is inconsistent: chunk {} for schema '{}' is marked completed but no files were found under '{}'", + chunk_id, + schema, + path + ))] + MissingChunkData { + chunk_id: u32, + schema: String, + path: String, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Chunk {} import failed for schema '{}'", chunk_id, schema))] + ChunkImportFailed { + chunk_id: u32, + schema: String, + #[snafu(source)] + error: crate::data::export_v2::error::Error, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Snapshot storage error"))] + SnapshotStorage { + #[snafu(source)] + error: crate::data::export_v2::error::Error, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Database error"))] + Database { + #[snafu(source)] + error: crate::error::Error, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Failed to parse import state file"))] + ImportStateParse { + #[snafu(source)] + error: serde_json::Error, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Import state I/O failed at '{}': {}", path, error))] + ImportStateIo { + path: String, + #[snafu(source)] + error: std::io::Error, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display("Import state is already locked at '{}'", path))] + ImportStateLocked { + path: String, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display( + "Failed to determine import state path for snapshot '{}'. Set HOME, USERPROFILE, or run from a valid current directory.", + snapshot_id + ))] + ImportStatePathUnavailable { + snapshot_id: String, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display( + "Import state at '{}' does not match current import: {}. Either rerun with matching import arguments, or delete the state file to start over (DDL will be re-executed).", + path, + reason + ))] + ImportStateMismatch { + path: String, + reason: String, + #[snafu(implicit)] + location: Location, + }, + + #[cfg(test)] + #[snafu(display("Test task failed: {}", message))] + TestTaskFailed { + message: String, + retryable: bool, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display( + "Import state references unknown task: chunk {}, schema '{}'", + chunk_id, + schema + ))] + ImportStateUnknownTask { + chunk_id: u32, + schema: String, + #[snafu(implicit)] + location: Location, + }, + + #[snafu(display( + "Import state at '{}' is not ready for data import: DDL has not been marked completed", + path + ))] + ImportStateDdlIncomplete { + path: String, + #[snafu(implicit)] + location: Location, + }, +} + +pub type Result = std::result::Result; + +impl ErrorExt for Error { + fn status_code(&self) -> StatusCode { + match self { + Error::SnapshotNotFound { .. } + | Error::SchemaNotInSnapshot { .. } + | Error::ManifestVersionMismatch { .. } + | Error::IncompleteSnapshot { .. } + | Error::EmptyChunkManifest { .. } + | Error::MissingChunkData { .. } => StatusCode::InvalidArguments, + Error::ImportStatePathUnavailable { .. } + | Error::ImportStateUnknownTask { .. } + | Error::ImportStateDdlIncomplete { .. } => StatusCode::Unexpected, + Error::ImportStateMismatch { .. } => StatusCode::InvalidArguments, + #[cfg(test)] + Error::TestTaskFailed { retryable, .. } => { + if *retryable { + StatusCode::StorageUnavailable + } else { + StatusCode::InvalidArguments + } + } + Error::Database { error, .. } => error.status_code(), + Error::SnapshotStorage { error, .. } | Error::ChunkImportFailed { error, .. } => { + error.status_code() + } + Error::ImportStateParse { .. } => StatusCode::Internal, + Error::ImportStateIo { .. } => StatusCode::StorageUnavailable, + Error::ImportStateLocked { .. } => StatusCode::IllegalState, + } + } + + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/src/cli/src/data/import_v2/executor.rs b/src/cli/src/data/import_v2/executor.rs new file mode 100644 index 0000000000..3f2bf66ae6 --- /dev/null +++ b/src/cli/src/data/import_v2/executor.rs @@ -0,0 +1,122 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! DDL execution for import. + +use common_telemetry::info; +use snafu::ResultExt; + +use crate::data::import_v2::error::{DatabaseSnafu, Result}; +use crate::database::DatabaseClient; + +/// A DDL statement with an explicit execution schema context. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DdlStatement { + pub sql: String, + pub execution_schema: Option, +} + +impl DdlStatement { + pub fn new(sql: String) -> Self { + Self { + sql, + execution_schema: None, + } + } + + pub fn with_execution_schema(sql: String, schema: String) -> Self { + Self { + sql, + execution_schema: Some(schema), + } + } +} + +/// Executes DDL statements against the database. +pub struct DdlExecutor<'a> { + client: &'a DatabaseClient, +} + +impl<'a> DdlExecutor<'a> { + /// Creates a new DDL executor. + pub fn new(client: &'a DatabaseClient) -> Self { + Self { client } + } + + /// Executes a list of DDL statements, stopping on first error. + pub async fn execute_strict(&self, statements: &[DdlStatement]) -> Result<()> { + let total = statements.len(); + + for (i, stmt) in statements.iter().enumerate() { + let preview = preview_sql(&stmt.sql); + + info!("Executing DDL ({}/{}): {}", i + 1, total, preview); + + if let Some(schema) = stmt.execution_schema.as_deref() { + self.client + .sql(&stmt.sql, schema) + .await + .context(DatabaseSnafu)?; + } else { + self.client + .sql_in_public(&stmt.sql) + .await + .context(DatabaseSnafu)?; + } + } + + Ok(()) + } +} + +fn preview_sql(sql: &str) -> String { + let mut chars = sql.chars(); + let preview: String = chars.by_ref().take(80).collect(); + if chars.next().is_some() { + format!("{preview}...") + } else { + preview + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_statement_without_execution_schema_uses_public() { + let stmt = DdlStatement::new("CREATE DATABASE IF NOT EXISTS test_db".to_string()); + assert_eq!(stmt.execution_schema, None); + } + + #[test] + fn test_statement_with_execution_schema_preserves_context() { + let stmt = DdlStatement::with_execution_schema( + r#"CREATE TABLE IF NOT EXISTS "my""schema"."metrics" (ts TIMESTAMP TIME INDEX)"# + .to_string(), + r#"my"schema"#.to_string(), + ); + assert_eq!(stmt.execution_schema.as_deref(), Some(r#"my"schema"#)); + } + + #[test] + fn test_preview_sql_truncates_at_char_boundary() { + let sql = format!( + "CREATE TABLE {} (ts TIMESTAMP TIME INDEX)", + "测".repeat(100) + ); + let preview = preview_sql(&sql); + assert!(preview.ends_with("...")); + assert!(preview.is_char_boundary(preview.len())); + } +} diff --git a/src/cli/src/data/import_v2/state.rs b/src/cli/src/data/import_v2/state.rs new file mode 100644 index 0000000000..ba431ac62f --- /dev/null +++ b/src/cli/src/data/import_v2/state.rs @@ -0,0 +1,804 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; + +use chrono::{DateTime, Utc}; +use fs2::FileExt; +use serde::{Deserialize, Serialize}; +use snafu::{IntoError, OptionExt, ResultExt}; +use tokio::io::AsyncWriteExt; + +use crate::data::import_v2::error::{ + ImportStateIoSnafu, ImportStateLockedSnafu, ImportStateParseSnafu, ImportStateUnknownTaskSnafu, + Result, +}; +use crate::data::path::encode_path_segment; + +const IMPORT_STATE_ROOT: &str = ".greptime"; +const IMPORT_STATE_DIR: &str = "import_state"; +static IMPORT_STATE_TMP_ID: AtomicU64 = AtomicU64::new(0); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum ImportTaskStatus { + Pending, + InProgress, + Completed, + Failed, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct ImportTaskKey { + pub(crate) chunk_id: u32, + pub(crate) schema: String, +} + +impl ImportTaskKey { + pub(crate) fn new(chunk_id: u32, schema: impl Into) -> Self { + Self { + chunk_id, + schema: schema.into(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct ImportTaskState { + pub(crate) chunk_id: u32, + pub(crate) schema: String, + pub(crate) status: ImportTaskStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) error: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct ImportState { + pub(crate) snapshot_id: String, + pub(crate) target_addr: String, + pub(crate) catalog: String, + pub(crate) schemas: Vec, + #[serde(default)] + pub(crate) ddl_completed: bool, + pub(crate) updated_at: DateTime, + // Tasks are (chunk-schema) tuples and can reach the tens of thousands; + // linear scans here are accepted because per-task work is dominated by + // network I/O and an fsync, but if the bound grows further this should be + // backed by a HashMap<(chunk_id, schema), index> rebuilt after load. + pub(crate) tasks: Vec, +} + +impl ImportState { + pub(crate) fn new( + snapshot_id: impl Into, + target_addr: impl Into, + catalog: impl Into, + schemas: &[String], + tasks: I, + ) -> Self + where + I: IntoIterator, + { + Self { + snapshot_id: snapshot_id.into(), + target_addr: target_addr.into(), + catalog: catalog.into(), + schemas: canonical_schema_selection(schemas), + ddl_completed: false, + updated_at: Utc::now(), + tasks: tasks + .into_iter() + .map(|task| ImportTaskState { + chunk_id: task.chunk_id, + schema: task.schema, + status: ImportTaskStatus::Pending, + error: None, + }) + .collect(), + } + } + + pub(crate) fn mark_ddl_completed(&mut self) { + self.ddl_completed = true; + self.updated_at = Utc::now(); + } + + pub(crate) fn task_status(&self, chunk_id: u32, schema: &str) -> Option { + self.tasks + .iter() + .find(|task| task.chunk_id == chunk_id && task.schema == schema) + .map(|task| task.status) + } + + pub(crate) fn set_task_status( + &mut self, + chunk_id: u32, + schema: &str, + status: ImportTaskStatus, + error: Option, + ) -> Result<()> { + let task = self + .tasks + .iter_mut() + .find(|task| task.chunk_id == chunk_id && task.schema == schema) + .context(ImportStateUnknownTaskSnafu { + chunk_id, + schema: schema.to_string(), + })?; + task.status = status; + task.error = error; + self.updated_at = Utc::now(); + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct ImportStateLockGuard { + file: std::fs::File, +} + +impl Drop for ImportStateLockGuard { + fn drop(&mut self) { + let _ = self.file.unlock(); + } +} + +pub(crate) fn default_state_path( + snapshot_id: &str, + target_addr: &str, + catalog: &str, + schemas: &[String], +) -> Option { + let home = default_home_dir_with(|key| std::env::var_os(key)); + let cwd = std::env::current_dir().ok(); + default_state_path_with( + home.as_deref(), + cwd.as_deref(), + snapshot_id, + target_addr, + catalog, + schemas, + ) +} + +fn default_home_dir_with(get: F) -> Option +where + F: Fn(&str) -> Option, +{ + get("HOME") + .or_else(|| get("USERPROFILE")) + .map(PathBuf::from) + .or_else(|| { + let drive = get("HOMEDRIVE")?; + let path = get("HOMEPATH")?; + Some(PathBuf::from(drive).join(path)) + }) +} + +fn default_state_path_with( + home: Option<&Path>, + cwd: Option<&Path>, + snapshot_id: &str, + target_addr: &str, + catalog: &str, + schemas: &[String], +) -> Option { + let file_name = import_state_file_name(snapshot_id, target_addr, catalog, schemas); + match (home, cwd) { + (Some(home), _) => Some( + home.join(IMPORT_STATE_ROOT) + .join(IMPORT_STATE_DIR) + .join(file_name), + ), + (None, Some(cwd)) => Some(cwd.join(file_name)), + (None, None) => None, + } +} + +fn import_state_file_name( + snapshot_id: &str, + target_addr: &str, + catalog: &str, + schemas: &[String], +) -> String { + format!( + ".import_state_{}_{}_{}.json", + encode_path_segment(snapshot_id), + encode_path_segment(target_addr), + import_identity_hash(catalog, schemas) + ) +} + +pub(crate) fn canonical_schema_selection(schemas: &[String]) -> Vec { + let mut canonicalized = schemas + .iter() + .map(|schema| schema.to_ascii_lowercase()) + .collect::>(); + canonicalized.sort(); + canonicalized.dedup(); + canonicalized +} + +/// FNV-1a over `(catalog, schemas)`. The output is part of the persisted state +/// filename, so we cannot use `std::collections::hash_map::DefaultHasher` - +/// Rust does not guarantee its algorithm across releases, which would make a +/// state file written by one toolchain undiscoverable by another. +fn import_identity_hash(catalog: &str, schemas: &[String]) -> String { + const FNV_OFFSET: u64 = 0xcbf29ce484222325; + const FNV_PRIME: u64 = 0x100000001b3; + + fn hash_bytes(mut hash: u64, bytes: &[u8]) -> u64 { + for byte in bytes { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(FNV_PRIME); + } + hash + } + + let mut hash = FNV_OFFSET; + hash = hash_bytes(hash, catalog.as_bytes()); + // 0xff cannot appear in valid UTF-8, so it works as an unambiguous + // field separator between adjacent identifiers. + hash = hash_bytes(hash, &[0xff]); + for schema in canonical_schema_selection(schemas) { + hash = hash_bytes(hash, schema.as_bytes()); + hash = hash_bytes(hash, &[0xff]); + } + format!("{hash:016x}") +} + +pub(crate) async fn load_import_state(path: &Path) -> Result> { + match tokio::fs::read(path).await { + Ok(bytes) => { + let mut state: ImportState = + serde_json::from_slice(&bytes).context(ImportStateParseSnafu)?; + normalize_import_state_for_resume(&mut state); + Ok(Some(state)) + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(source) => Err(source).context(ImportStateIoSnafu { + path: path.display().to_string(), + }), + } +} + +/// Caller must hold the lock acquired via `try_acquire_import_state_lock`. +pub(crate) async fn save_import_state(path: &Path, state: &ImportState) -> Result<()> { + if let Some(parent) = path.parent() { + tokio::fs::create_dir_all(parent) + .await + .context(ImportStateIoSnafu { + path: parent.display().to_string(), + })?; + } + + let bytes = + serde_json::to_vec_pretty(state).expect("ImportState should always be serializable"); + let tmp_path = unique_tmp_path(path); + let mut file = tokio::fs::File::create(&tmp_path) + .await + .context(ImportStateIoSnafu { + path: tmp_path.display().to_string(), + })?; + file.write_all(&bytes).await.context(ImportStateIoSnafu { + path: tmp_path.display().to_string(), + })?; + file.sync_all().await.context(ImportStateIoSnafu { + path: tmp_path.display().to_string(), + })?; + // Close before rename; Windows forbids renaming an open file. + drop(file); + + tokio::fs::rename(&tmp_path, path) + .await + .context(ImportStateIoSnafu { + path: path.display().to_string(), + })?; + sync_parent_dir(path).await?; + Ok(()) +} + +pub(crate) fn try_acquire_import_state_lock(path: &Path) -> Result { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).context(ImportStateIoSnafu { + path: parent.display().to_string(), + })?; + } + + let lock_path = import_state_lock_path(path); + let file = std::fs::OpenOptions::new() + .create(true) + .read(true) + .write(true) + .truncate(false) + .open(&lock_path) + .context(ImportStateIoSnafu { + path: lock_path.display().to_string(), + })?; + file.try_lock_exclusive().map_err(|error| { + if is_lock_contention(&error) { + ImportStateLockedSnafu { + path: lock_path.display().to_string(), + } + .build() + } else { + ImportStateIoSnafu { + path: lock_path.display().to_string(), + } + .into_error(error) + } + })?; + + Ok(ImportStateLockGuard { file }) +} + +fn is_lock_contention(error: &std::io::Error) -> bool { + error.kind() == std::io::ErrorKind::WouldBlock + || error.raw_os_error() == fs2::lock_contended_error().raw_os_error() +} + +fn unique_tmp_path(path: &Path) -> PathBuf { + let pid = std::process::id(); + let seq = IMPORT_STATE_TMP_ID.fetch_add(1, Ordering::Relaxed); + let file_name = path.file_name().unwrap_or_default().to_string_lossy(); + path.with_file_name(format!("{file_name}.{pid}.{seq}.tmp")) +} + +fn import_state_lock_path(path: &Path) -> PathBuf { + let file_name = path.file_name().unwrap_or_default().to_string_lossy(); + path.with_file_name(format!("{file_name}.lock")) +} + +fn normalize_import_state_for_resume(state: &mut ImportState) { + for task in &mut state.tasks { + if task.status == ImportTaskStatus::InProgress { + task.status = ImportTaskStatus::Pending; + task.error = None; + } + } +} + +pub(crate) async fn delete_import_state(path: &Path) -> Result<()> { + match tokio::fs::remove_file(path).await { + Ok(()) => { + sync_parent_dir(path).await?; + Ok(()) + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(source) => Err(source).context(ImportStateIoSnafu { + path: path.display().to_string(), + }), + } +} + +#[cfg(unix)] +async fn sync_parent_dir(path: &Path) -> Result<()> { + let Some(parent) = path.parent() else { + return Ok(()); + }; + + let dir = tokio::fs::File::open(parent) + .await + .context(ImportStateIoSnafu { + path: parent.display().to_string(), + })?; + dir.sync_all().await.context(ImportStateIoSnafu { + path: parent.display().to_string(), + })?; + Ok(()) +} + +#[cfg(not(unix))] +async fn sync_parent_dir(_path: &Path) -> Result<()> { + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::process::Command; + + use chrono::Utc; + use tempfile::tempdir; + + use super::*; + + const CHILD_LOCK_PATH_ENV: &str = "GREPTIME_IMPORT_STATE_LOCK_PATH"; + const CHILD_LOCK_TEST: &str = + "data::import_v2::state::tests::test_try_acquire_import_state_lock_child_process"; + + fn schemas() -> Vec { + vec!["public".to_string(), "analytics".to_string()] + } + + fn tasks() -> Vec { + vec![ + ImportTaskKey::new(1, "public"), + ImportTaskKey::new(2, "analytics"), + ] + } + + #[test] + fn test_import_state_new_initializes_pending_tasks() { + let state = ImportState::new( + "snapshot-1", + "127.0.0.1:4000", + "greptime", + &schemas(), + tasks(), + ); + + assert_eq!(state.snapshot_id, "snapshot-1"); + assert_eq!(state.target_addr, "127.0.0.1:4000"); + assert_eq!(state.catalog, "greptime"); + assert_eq!(state.schemas, vec!["analytics", "public"]); + assert_eq!(state.tasks.len(), 2); + assert_eq!(state.tasks[0].status, ImportTaskStatus::Pending); + assert_eq!(state.tasks[1].status, ImportTaskStatus::Pending); + } + + #[test] + fn test_set_task_status_updates_timestamp_and_error() { + let mut state = ImportState::new( + "snapshot-1", + "127.0.0.1:4000", + "greptime", + &schemas(), + tasks(), + ); + let before = state.updated_at; + state.updated_at = Utc::now() - chrono::Duration::seconds(10); + + state + .set_task_status( + 1, + "public", + ImportTaskStatus::Failed, + Some("timeout".to_string()), + ) + .unwrap(); + assert_eq!( + state.task_status(1, "public"), + Some(ImportTaskStatus::Failed) + ); + assert_eq!(state.tasks[0].error.as_deref(), Some("timeout")); + assert!(state.updated_at > before); + } + + #[test] + fn test_set_task_status_rejects_unknown_task() { + let mut state = ImportState::new( + "snapshot-1", + "127.0.0.1:4000", + "greptime", + &schemas(), + tasks(), + ); + + let error = state + .set_task_status(99, "public", ImportTaskStatus::Completed, None) + .unwrap_err(); + + assert!(matches!( + error, + crate::data::import_v2::error::Error::ImportStateUnknownTask { chunk_id, schema, .. } + if chunk_id == 99 && schema == "public" + )); + } + + #[tokio::test] + async fn test_save_and_load_import_state_round_trip() { + let dir = tempdir().unwrap(); + let path = dir.path().join("import_state.json"); + let mut state = ImportState::new( + "snapshot-1", + "127.0.0.1:4000", + "greptime", + &schemas(), + tasks(), + ); + state + .set_task_status(2, "analytics", ImportTaskStatus::Completed, None) + .unwrap(); + + save_import_state(&path, &state).await.unwrap(); + let loaded = load_import_state(&path).await.unwrap().unwrap(); + + assert_eq!(loaded.snapshot_id, state.snapshot_id); + assert_eq!(loaded.target_addr, state.target_addr); + assert_eq!(loaded.catalog, state.catalog); + assert_eq!(loaded.schemas, state.schemas); + assert_eq!(loaded.tasks, state.tasks); + } + + #[tokio::test] + async fn test_save_import_state_overwrites_existing_file() { + let dir = tempdir().unwrap(); + let path = dir.path().join("import_state.json"); + let mut state = ImportState::new( + "snapshot-1", + "127.0.0.1:4000", + "greptime", + &schemas(), + tasks(), + ); + save_import_state(&path, &state).await.unwrap(); + + state + .set_task_status(1, "public", ImportTaskStatus::Completed, None) + .unwrap(); + save_import_state(&path, &state).await.unwrap(); + + let loaded = load_import_state(&path).await.unwrap().unwrap(); + assert_eq!( + loaded.task_status(1, "public"), + Some(ImportTaskStatus::Completed) + ); + } + + #[test] + fn test_load_import_state_resets_in_progress_to_pending() { + let mut state = ImportState::new( + "snapshot-1", + "127.0.0.1:4000", + "greptime", + &schemas(), + tasks(), + ); + state + .set_task_status( + 2, + "analytics", + ImportTaskStatus::InProgress, + Some("running".to_string()), + ) + .unwrap(); + + normalize_import_state_for_resume(&mut state); + + assert_eq!( + state.task_status(1, "public"), + Some(ImportTaskStatus::Pending) + ); + assert_eq!( + state.task_status(2, "analytics"), + Some(ImportTaskStatus::Pending) + ); + assert_eq!(state.tasks[1].error, None); + } + + #[test] + fn test_unique_tmp_path_generates_distinct_paths() { + let dir = tempdir().unwrap(); + let path = dir.path().join("import_state.json"); + + let first = unique_tmp_path(&path); + let second = unique_tmp_path(&path); + + assert_ne!(first, second); + assert!(first.starts_with(dir.path())); + assert!(second.starts_with(dir.path())); + assert!( + first + .file_name() + .unwrap() + .to_string_lossy() + .ends_with(".tmp") + ); + assert!( + second + .file_name() + .unwrap() + .to_string_lossy() + .ends_with(".tmp") + ); + } + + #[test] + fn test_lock_contention_detection_accepts_platform_error() { + let error = fs2::lock_contended_error(); + + assert!(is_lock_contention(&error)); + } + + #[test] + fn test_try_acquire_import_state_lock_rejects_second_holder() { + let dir = tempdir().unwrap(); + let path = dir.path().join("import_state.json"); + + let _first = try_acquire_import_state_lock(&path).unwrap(); + // Import state locking guards concurrent CLI processes, so validate cross-process exclusion. + let output = Command::new(std::env::current_exe().unwrap()) + .arg(CHILD_LOCK_TEST) + .arg("--ignored") + .arg("--exact") + .env(CHILD_LOCK_PATH_ENV, &path) + .output() + .unwrap(); + + assert!( + output.status.success(), + "child lock test failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("1 passed"), + "child lock test did not run the expected ignored test\nstdout:\n{stdout}" + ); + } + + #[test] + #[ignore = "spawned by test_try_acquire_import_state_lock_rejects_second_holder"] + fn test_try_acquire_import_state_lock_child_process() { + let path = std::env::var_os(CHILD_LOCK_PATH_ENV) + .expect("child lock path must be set by the parent test"); + let path = PathBuf::from(path); + let error = try_acquire_import_state_lock(&path).unwrap_err(); + + assert!(matches!( + error, + crate::data::import_v2::error::Error::ImportStateLocked { .. } + )); + } + + #[tokio::test] + async fn test_delete_import_state_ignores_missing_file() { + let dir = tempdir().unwrap(); + let path = dir.path().join("missing.json"); + + delete_import_state(&path).await.unwrap(); + } + + #[test] + fn test_default_state_path_prefers_home_and_encodes_snapshot_id() { + let home = tempdir().unwrap(); + let cwd = tempdir().unwrap(); + + let path = default_state_path_with( + Some(home.path()), + Some(cwd.path()), + "../snapshot", + "127.0.0.1:4000", + "greptime", + &schemas(), + ) + .unwrap(); + + assert_eq!( + path.parent().unwrap(), + home.path().join(IMPORT_STATE_ROOT).join(IMPORT_STATE_DIR) + ); + let file_name = path.file_name().unwrap().to_string_lossy(); + assert!(file_name.starts_with(".import_state_%2E%2E%2Fsnapshot_127%2E0%2E0%2E1%3A4000_")); + assert!(file_name.ends_with(".json")); + } + + #[test] + fn test_default_state_path_falls_back_to_cwd_when_home_missing() { + let cwd = tempdir().unwrap(); + + let path = default_state_path_with( + None, + Some(cwd.path()), + "snapshot-1", + "target-a", + "greptime", + &schemas(), + ) + .unwrap(); + + assert_eq!(path.parent().unwrap(), cwd.path()); + let file_name = path.file_name().unwrap().to_string_lossy(); + assert!(file_name.starts_with(".import_state_snapshot-1_target-a_")); + assert!(file_name.ends_with(".json")); + } + + #[test] + fn test_default_state_path_isolated_by_target_addr() { + let cwd = tempdir().unwrap(); + + let first = default_state_path_with( + None, + Some(cwd.path()), + "snapshot-1", + "127.0.0.1:4000", + "greptime", + &schemas(), + ) + .unwrap(); + let second = default_state_path_with( + None, + Some(cwd.path()), + "snapshot-1", + "127.0.0.1:4001", + "greptime", + &schemas(), + ) + .unwrap(); + + assert_ne!(first, second); + } + + #[test] + fn test_default_state_path_isolated_by_catalog_and_schemas() { + let cwd = tempdir().unwrap(); + let public_only = vec!["public".to_string()]; + let analytics_only = vec!["analytics".to_string()]; + + let first = default_state_path_with( + None, + Some(cwd.path()), + "snapshot-1", + "127.0.0.1:4000", + "greptime", + &public_only, + ) + .unwrap(); + let second = default_state_path_with( + None, + Some(cwd.path()), + "snapshot-1", + "127.0.0.1:4000", + "other", + &public_only, + ) + .unwrap(); + let third = default_state_path_with( + None, + Some(cwd.path()), + "snapshot-1", + "127.0.0.1:4000", + "greptime", + &analytics_only, + ) + .unwrap(); + + assert_ne!(first, second); + assert_ne!(first, third); + } + + #[test] + fn test_default_home_dir_prefers_home() { + let detected = default_home_dir_with(|key| match key { + "HOME" => Some(std::ffi::OsString::from("/tmp/home")), + "USERPROFILE" => Some(std::ffi::OsString::from("/tmp/userprofile")), + _ => None, + }); + + assert_eq!(detected, Some(PathBuf::from("/tmp/home"))); + } + + #[test] + fn test_default_home_dir_falls_back_to_userprofile() { + let detected = default_home_dir_with(|key| match key { + "USERPROFILE" => Some(std::ffi::OsString::from("/tmp/userprofile")), + _ => None, + }); + + assert_eq!(detected, Some(PathBuf::from("/tmp/userprofile"))); + } + + #[test] + fn test_default_home_dir_falls_back_to_home_drive_and_path() { + let detected = default_home_dir_with(|key| match key { + "HOMEDRIVE" => Some(std::ffi::OsString::from("/tmp")), + "HOMEPATH" => Some(std::ffi::OsString::from("windows-home")), + _ => None, + }); + + assert_eq!(detected, Some(PathBuf::from("/tmp").join("windows-home"))); + } +} diff --git a/src/cli/src/data/path.rs b/src/cli/src/data/path.rs new file mode 100644 index 0000000000..2df81f62c8 --- /dev/null +++ b/src/cli/src/data/path.rs @@ -0,0 +1,89 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Shared path helpers for export/import data files. + +use crate::data::export_v2::schema::{DDL_DIR, SCHEMA_DIR}; + +pub(crate) fn ddl_path_for_schema(schema: &str) -> String { + format!( + "{}/{}/{}.sql", + SCHEMA_DIR, + DDL_DIR, + encode_path_segment(schema) + ) +} + +pub(crate) fn data_dir_for_schema_chunk(schema: &str, chunk_id: u32) -> String { + format!("data/{}/{}/", encode_path_segment(schema), chunk_id) +} + +pub(crate) fn encode_path_segment(value: &str) -> String { + let mut encoded = String::with_capacity(value.len()); + for byte in value.bytes() { + match byte { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' => { + encoded.push(byte as char); + } + _ => { + encoded.push('%'); + encoded.push(hex_char(byte >> 4)); + encoded.push(hex_char(byte & 0x0F)); + } + } + } + encoded +} + +fn hex_char(nibble: u8) -> char { + match nibble { + 0..=9 => (b'0' + nibble) as char, + 10..=15 => (b'A' + (nibble - 10)) as char, + _ => unreachable!("nibble must be in 0..=15"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encode_path_segment_preserves_safe_ascii() { + assert_eq!(encode_path_segment("test_db"), "test_db"); + } + + #[test] + fn test_encode_path_segment_escapes_path_traversal_chars() { + assert_eq!(encode_path_segment("../evil"), "%2E%2E%2Fevil"); + assert_eq!(encode_path_segment(r"..\\evil"), "%2E%2E%5C%5Cevil"); + } + + #[test] + fn test_ddl_path_for_schema_encodes_schema_segment() { + assert_eq!(ddl_path_for_schema("public"), "schema/ddl/public.sql"); + assert_eq!( + ddl_path_for_schema("../evil"), + "schema/ddl/%2E%2E%2Fevil.sql" + ); + } + + #[test] + fn test_data_dir_for_schema_chunk_encodes_schema_segment() { + assert_eq!(data_dir_for_schema_chunk("public", 1), "data/public/1/"); + assert_eq!( + data_dir_for_schema_chunk("../evil", 7), + "data/%2E%2E%2Fevil/7/" + ); + } +} diff --git a/src/cli/src/data/snapshot_storage.rs b/src/cli/src/data/snapshot_storage.rs new file mode 100644 index 0000000000..2ee07b5586 --- /dev/null +++ b/src/cli/src/data/snapshot_storage.rs @@ -0,0 +1,863 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Storage abstraction for Export/Import V2. +//! +//! This module provides a unified interface for reading and writing snapshot data +//! to various storage backends (S3, OSS, GCS, Azure Blob, local filesystem). + +use std::collections::BTreeSet; + +use async_trait::async_trait; +use futures::TryStreamExt; +use object_store::services::{Azblob, Fs, Gcs, Oss, S3}; +use object_store::util::{with_instrument_layers, with_retry_layers}; +use object_store::{ + AzblobConnection, ErrorKind, GcsConnection, ObjectStore, OssConnection, S3Connection, +}; +use snafu::ResultExt; +use url::Url; + +use crate::common::ObjectStoreConfig; +use crate::data::export_v2::error::{ + BuildObjectStoreSnafu, InvalidUriSnafu, ManifestParseSnafu, ManifestSerializeSnafu, Result, + SnapshotNotFoundSnafu, StorageOperationSnafu, TextDecodeSnafu, UnsupportedSchemeSnafu, + UrlParseSnafu, +}; +use crate::data::export_v2::manifest::{MANIFEST_FILE, Manifest}; +#[cfg(test)] +use crate::data::export_v2::schema::SchemaDefinition; +use crate::data::export_v2::schema::{SCHEMA_DIR, SCHEMAS_FILE, SchemaSnapshot}; + +struct RemoteLocation { + bucket_or_container: String, + root: String, +} + +/// URI schemes supported for snapshot storage. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StorageScheme { + /// Amazon S3. + S3, + /// Alibaba Cloud OSS. + Oss, + /// Google Cloud Storage. + Gcs, + /// Azure Blob Storage. + Azblob, + /// Local filesystem (file://). + File, +} + +impl StorageScheme { + /// Parses storage scheme from URI. + pub fn from_uri(uri: &str) -> Result { + let url = Url::parse(uri).context(UrlParseSnafu)?; + + match url.scheme() { + "s3" => Ok(Self::S3), + "oss" => Ok(Self::Oss), + "gs" | "gcs" => Ok(Self::Gcs), + "azblob" => Ok(Self::Azblob), + "file" => Ok(Self::File), + scheme => UnsupportedSchemeSnafu { scheme }.fail(), + } + } +} + +/// Extracts bucket/container and root path from a URI. +fn extract_remote_location_with_root_policy( + uri: &str, + allow_empty_root: bool, +) -> Result { + let url = Url::parse(uri).context(UrlParseSnafu)?; + let bucket_or_container = url.host_str().unwrap_or("").to_string(); + if bucket_or_container.is_empty() { + return InvalidUriSnafu { + uri, + reason: "URI must include bucket/container in host", + } + .fail(); + } + + let root = url.path().trim_start_matches('/').to_string(); + if root.is_empty() && !allow_empty_root { + return InvalidUriSnafu { + uri, + reason: "snapshot URI must include a non-empty path after the bucket/container", + } + .fail(); + } + + Ok(RemoteLocation { + bucket_or_container, + root, + }) +} + +/// Validates that a URI has a proper scheme. +/// +/// Rejects bare paths (e.g., `/tmp/backup`, `./backup`) because: +/// - Schema export (CLI) and data export (server) run in different processes +/// - Using bare paths would split the snapshot across machines +/// +/// Supported URI schemes: +/// - `s3://bucket/path` - Amazon S3 +/// - `oss://bucket/path` - Alibaba Cloud OSS +/// - `gs://bucket/path` - Google Cloud Storage +/// - `azblob://container/path` - Azure Blob Storage +/// - `file:///absolute/path` - Local filesystem +pub fn validate_uri(uri: &str) -> Result { + // Must have a scheme + if !uri.contains("://") { + return InvalidUriSnafu { + uri, + reason: "URI must have a scheme (e.g., s3://, file://). Bare paths are not supported.", + } + .fail(); + } + + StorageScheme::from_uri(uri) +} + +fn schema_index_path() -> String { + format!("{}/{}", SCHEMA_DIR, SCHEMAS_FILE) +} + +/// Extracts the absolute filesystem path from a file:// URI. +fn extract_file_path_from_uri(uri: &str) -> Result { + let url = Url::parse(uri).context(UrlParseSnafu)?; + + match url.host_str() { + Some(host) if !host.is_empty() && host != "localhost" => InvalidUriSnafu { + uri, + reason: "file:// URI must use an absolute path like file:///tmp/backup", + } + .fail(), + _ => url + .to_file_path() + .map_err(|_| { + InvalidUriSnafu { + uri, + reason: "file:// URI must use an absolute path like file:///tmp/backup", + } + .build() + }) + .map(|path| path.to_string_lossy().into_owned()), + } +} + +async fn ensure_snapshot_exists(storage: &OpenDalStorage) -> Result<()> { + if storage.exists().await? { + Ok(()) + } else { + SnapshotNotFoundSnafu { + uri: storage.target_uri.as_str(), + } + .fail() + } +} + +/// Snapshot storage abstraction. +/// +/// Provides operations for reading and writing snapshot data to various storage backends. +#[async_trait] +pub trait SnapshotStorage: Send + Sync { + /// Checks if a snapshot exists at this location (manifest.json exists). + async fn exists(&self) -> Result; + + /// Reads the manifest file. + async fn read_manifest(&self) -> Result; + + /// Writes the manifest file. + async fn write_manifest(&self, manifest: &Manifest) -> Result<()>; + + /// Writes the schema index to schema/schemas.json. + async fn write_schema(&self, schema: &SchemaSnapshot) -> Result<()>; + + /// Writes a text file to a relative path under the snapshot root. + async fn write_text(&self, path: &str, content: &str) -> Result<()>; + + /// Reads a text file from a relative path under the snapshot root. + async fn read_text(&self, path: &str) -> Result; + + /// Creates a directory-like prefix under the snapshot root when needed by the backend. + async fn create_dir_all(&self, path: &str) -> Result<()>; + + /// Lists files recursively under a relative prefix. + async fn list_files_recursive(&self, prefix: &str) -> Result>; + + /// Deletes the entire snapshot (for --force). + async fn delete_snapshot(&self) -> Result<()>; +} + +/// OpenDAL-based implementation of SnapshotStorage. +pub struct OpenDalStorage { + object_store: ObjectStore, + target_uri: String, +} + +impl OpenDalStorage { + fn new_operator_rooted(object_store: ObjectStore, target_uri: &str) -> Self { + Self { + object_store, + target_uri: target_uri.to_string(), + } + } + + fn finish_local_store(object_store: ObjectStore) -> ObjectStore { + with_instrument_layers(object_store, false) + } + + fn finish_remote_store(object_store: ObjectStore) -> ObjectStore { + with_instrument_layers(with_retry_layers(object_store), false) + } + + fn ensure_backend_enabled(uri: &str, enabled: bool, reason: &'static str) -> Result<()> { + if enabled { + Ok(()) + } else { + InvalidUriSnafu { uri, reason }.fail() + } + } + + fn validate_remote_config( + uri: &str, + backend: &str, + result: std::result::Result<(), E>, + ) -> Result<()> { + result.map_err(|error| { + InvalidUriSnafu { + uri, + reason: format!("invalid {} config: {}", backend, error), + } + .build() + }) + } + + /// Creates a new storage from a file:// URI. + pub fn from_file_uri(uri: &str) -> Result { + let path = extract_file_path_from_uri(uri)?; + + let builder = Fs::default().root(&path); + let object_store = ObjectStore::new(builder) + .context(BuildObjectStoreSnafu)? + .finish(); + Ok(Self::new_operator_rooted( + Self::finish_local_store(object_store), + uri, + )) + } + + fn from_file_uri_with_config(uri: &str, storage: &ObjectStoreConfig) -> Result { + if storage.enable_s3 || storage.enable_oss || storage.enable_gcs || storage.enable_azblob { + return InvalidUriSnafu { + uri, + reason: "file:// cannot be used with remote storage flags", + } + .fail(); + } + + Self::from_file_uri(uri) + } + + fn from_s3_uri(uri: &str, storage: &ObjectStoreConfig) -> Result { + Self::from_s3_uri_with_root_policy(uri, storage, false) + } + + fn from_s3_uri_with_root_policy( + uri: &str, + storage: &ObjectStoreConfig, + allow_empty_root: bool, + ) -> Result { + Self::ensure_backend_enabled( + uri, + storage.enable_s3, + "s3:// requires --s3 and related options", + )?; + + let location = extract_remote_location_with_root_policy(uri, allow_empty_root)?; + let mut config = storage.s3.clone(); + config.s3_bucket = location.bucket_or_container; + config.s3_root = location.root; + Self::validate_remote_config(uri, "s3", config.validate())?; + + let conn: S3Connection = config.into(); + let object_store = ObjectStore::new(S3::from(&conn)) + .context(BuildObjectStoreSnafu)? + .finish(); + Ok(Self::new_operator_rooted( + Self::finish_remote_store(object_store), + uri, + )) + } + + fn from_oss_uri(uri: &str, storage: &ObjectStoreConfig) -> Result { + Self::from_oss_uri_with_root_policy(uri, storage, false) + } + + fn from_oss_uri_with_root_policy( + uri: &str, + storage: &ObjectStoreConfig, + allow_empty_root: bool, + ) -> Result { + Self::ensure_backend_enabled( + uri, + storage.enable_oss, + "oss:// requires --oss and related options", + )?; + + let location = extract_remote_location_with_root_policy(uri, allow_empty_root)?; + let mut config = storage.oss.clone(); + config.oss_bucket = location.bucket_or_container; + config.oss_root = location.root; + Self::validate_remote_config(uri, "oss", config.validate())?; + + let conn: OssConnection = config.into(); + let object_store = ObjectStore::new(Oss::from(&conn)) + .context(BuildObjectStoreSnafu)? + .finish(); + Ok(Self::new_operator_rooted( + Self::finish_remote_store(object_store), + uri, + )) + } + + fn from_gcs_uri(uri: &str, storage: &ObjectStoreConfig) -> Result { + Self::from_gcs_uri_with_root_policy(uri, storage, false) + } + + fn from_gcs_uri_with_root_policy( + uri: &str, + storage: &ObjectStoreConfig, + allow_empty_root: bool, + ) -> Result { + Self::ensure_backend_enabled( + uri, + storage.enable_gcs, + "gs:// or gcs:// requires --gcs and related options", + )?; + + let location = extract_remote_location_with_root_policy(uri, allow_empty_root)?; + let mut config = storage.gcs.clone(); + config.gcs_bucket = location.bucket_or_container; + config.gcs_root = location.root; + // GCS validate() rejects empty root, unlike S3/OSS/Azblob. + if allow_empty_root && config.gcs_root.is_empty() { + Self::validate_gcs_parent_config(uri, &config)?; + } else { + Self::validate_remote_config(uri, "gcs", config.validate())?; + } + + let conn: GcsConnection = config.into(); + let object_store = ObjectStore::new(Gcs::from(&conn)) + .context(BuildObjectStoreSnafu)? + .finish(); + Ok(Self::new_operator_rooted( + Self::finish_remote_store(object_store), + uri, + )) + } + + fn validate_gcs_parent_config( + uri: &str, + config: &crate::common::PrefixedGcsConnection, + ) -> Result<()> { + if config.gcs_bucket.is_empty() { + return InvalidUriSnafu { + uri, + reason: "invalid gcs config: GCS bucket must be set when --gcs is enabled.", + } + .fail(); + } + if config.gcs_scope.is_empty() { + return InvalidUriSnafu { + uri, + reason: "invalid gcs config: GCS scope must be set when --gcs is enabled.", + } + .fail(); + } + Ok(()) + } + + fn from_azblob_uri(uri: &str, storage: &ObjectStoreConfig) -> Result { + Self::from_azblob_uri_with_root_policy(uri, storage, false) + } + + fn from_azblob_uri_with_root_policy( + uri: &str, + storage: &ObjectStoreConfig, + allow_empty_root: bool, + ) -> Result { + Self::ensure_backend_enabled( + uri, + storage.enable_azblob, + "azblob:// requires --azblob and related options", + )?; + + let location = extract_remote_location_with_root_policy(uri, allow_empty_root)?; + let mut config = storage.azblob.clone(); + config.azblob_container = location.bucket_or_container; + config.azblob_root = location.root; + Self::validate_remote_config(uri, "azblob", config.validate())?; + + let conn: AzblobConnection = config.into(); + let object_store = ObjectStore::new(Azblob::from(&conn)) + .context(BuildObjectStoreSnafu)? + .finish(); + Ok(Self::new_operator_rooted( + Self::finish_remote_store(object_store), + uri, + )) + } + + /// Creates a new storage from a URI and object store config. + pub fn from_uri(uri: &str, storage: &ObjectStoreConfig) -> Result { + match StorageScheme::from_uri(uri)? { + StorageScheme::File => Self::from_file_uri_with_config(uri, storage), + StorageScheme::S3 => Self::from_s3_uri(uri, storage), + StorageScheme::Oss => Self::from_oss_uri(uri, storage), + StorageScheme::Gcs => Self::from_gcs_uri(uri, storage), + StorageScheme::Azblob => Self::from_azblob_uri(uri, storage), + } + } + + /// Creates storage rooted at a snapshot parent URI. + /// + /// Parent-oriented commands such as `export-v2 list` may scan bucket/container + /// roots. Snapshot-oriented commands must keep using `from_uri`, which rejects + /// empty remote roots to avoid unsafe snapshot operations at bucket scope. + pub fn from_parent_uri(uri: &str, storage: &ObjectStoreConfig) -> Result { + match StorageScheme::from_uri(uri)? { + StorageScheme::File => Self::from_file_uri_with_config(uri, storage), + StorageScheme::S3 => Self::from_s3_uri_with_root_policy(uri, storage, true), + StorageScheme::Oss => Self::from_oss_uri_with_root_policy(uri, storage, true), + StorageScheme::Gcs => Self::from_gcs_uri_with_root_policy(uri, storage, true), + StorageScheme::Azblob => Self::from_azblob_uri_with_root_policy(uri, storage, true), + } + } + + /// Reads a file as bytes. + async fn read_file(&self, path: &str) -> Result> { + let data = self + .object_store + .read(path) + .await + .context(StorageOperationSnafu { + operation: format!("read {}", path), + })?; + Ok(data.to_vec()) + } + + /// Reads a file as bytes if it exists. + pub(crate) async fn read_file_if_exists(&self, path: &str) -> Result>> { + match self.object_store.read(path).await { + Ok(data) => Ok(Some(data.to_vec())), + Err(error) if error.kind() == ErrorKind::NotFound => Ok(None), + Err(error) => Err(error).context(StorageOperationSnafu { + operation: format!("read {}", path), + }), + } + } + + /// Writes bytes to a file. + async fn write_file(&self, path: &str, data: Vec) -> Result<()> { + self.object_store + .write(path, data) + .await + .map(|_| ()) + .context(StorageOperationSnafu { + operation: format!("write {}", path), + }) + } + + /// Checks if a file exists using stat. + async fn file_exists(&self, path: &str) -> Result { + match self.object_store.stat(path).await { + Ok(_) => Ok(true), + Err(e) if e.kind() == object_store::ErrorKind::NotFound => Ok(false), + Err(e) => Err(e).context(StorageOperationSnafu { + operation: format!("check exists {}", path), + }), + } + } + + /// Lists direct child directory names under the storage root. + pub(crate) async fn list_direct_child_dirs(&self) -> Result> { + let mut lister = match self.object_store.lister_with("/").recursive(false).await { + Ok(lister) => lister, + Err(error) if error.kind() == ErrorKind::NotFound => return Ok(Vec::new()), + Err(error) => { + return Err(error).context(StorageOperationSnafu { + operation: "list /", + }); + } + }; + + let mut dirs = BTreeSet::new(); + while let Some(entry) = lister.try_next().await.context(StorageOperationSnafu { + operation: "list /", + })? { + let path = entry.path().trim_matches('/'); + if path.is_empty() { + continue; + } + + if entry.metadata().is_dir() + && let Some(name) = path.split('/').next() + { + dirs.insert(name.to_string()); + } + } + + Ok(dirs.into_iter().collect()) + } + + #[cfg(test)] + pub async fn read_schema(&self) -> Result { + let schemas_path = schema_index_path(); + let schemas: Vec = if self.file_exists(&schemas_path).await? { + let data = self.read_file(&schemas_path).await?; + serde_json::from_slice(&data).context(ManifestParseSnafu)? + } else { + vec![] + }; + + Ok(SchemaSnapshot { schemas }) + } +} + +#[async_trait] +impl SnapshotStorage for OpenDalStorage { + async fn exists(&self) -> Result { + self.file_exists(MANIFEST_FILE).await + } + + async fn read_manifest(&self) -> Result { + ensure_snapshot_exists(self).await?; + + let data = self.read_file(MANIFEST_FILE).await?; + serde_json::from_slice(&data).context(ManifestParseSnafu) + } + + async fn write_manifest(&self, manifest: &Manifest) -> Result<()> { + let data = serde_json::to_vec_pretty(manifest).context(ManifestSerializeSnafu)?; + self.write_file(MANIFEST_FILE, data).await + } + + async fn write_schema(&self, schema: &SchemaSnapshot) -> Result<()> { + let schemas_path = schema_index_path(); + let schemas_data = + serde_json::to_vec_pretty(&schema.schemas).context(ManifestSerializeSnafu)?; + self.write_file(&schemas_path, schemas_data).await + } + + async fn write_text(&self, path: &str, content: &str) -> Result<()> { + self.write_file(path, content.as_bytes().to_vec()).await + } + + async fn read_text(&self, path: &str) -> Result { + let data = self.read_file(path).await?; + String::from_utf8(data).context(TextDecodeSnafu) + } + + async fn create_dir_all(&self, path: &str) -> Result<()> { + self.object_store + .create_dir(path) + .await + .context(StorageOperationSnafu { + operation: format!("create dir {}", path), + }) + } + + async fn list_files_recursive(&self, prefix: &str) -> Result> { + let mut lister = match self.object_store.lister_with(prefix).recursive(true).await { + Ok(lister) => lister, + Err(error) if error.kind() == ErrorKind::NotFound => return Ok(Vec::new()), + Err(error) => { + return Err(error).context(StorageOperationSnafu { + operation: format!("list {}", prefix), + }); + } + }; + + let mut files = Vec::new(); + while let Some(entry) = lister.try_next().await.context(StorageOperationSnafu { + operation: format!("list {}", prefix), + })? { + if entry.metadata().is_dir() { + continue; + } + files.push(entry.path().to_string()); + } + Ok(files) + } + + async fn delete_snapshot(&self) -> Result<()> { + self.object_store + .delete_with("/") + .recursive(true) + .await + .context(StorageOperationSnafu { + operation: "delete snapshot", + }) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::path::Path; + + use object_store::ObjectStore; + use object_store::services::Fs; + use tempfile::tempdir; + use url::Url; + + use super::*; + use crate::data::export_v2::manifest::{DataFormat, TimeRange}; + use crate::data::export_v2::schema::SchemaDefinition; + + fn make_storage_with_rooted_fs(dir: &std::path::Path) -> OpenDalStorage { + let object_store = ObjectStore::new(Fs::default().root(dir.to_str().unwrap())) + .unwrap() + .finish(); + OpenDalStorage::new_operator_rooted( + OpenDalStorage::finish_local_store(object_store), + Url::from_directory_path(dir).unwrap().as_ref(), + ) + } + + #[test] + fn test_validate_uri_valid() { + assert_eq!(validate_uri("s3://bucket/path").unwrap(), StorageScheme::S3); + assert_eq!( + validate_uri("oss://bucket/path").unwrap(), + StorageScheme::Oss + ); + assert_eq!( + validate_uri("gs://bucket/path").unwrap(), + StorageScheme::Gcs + ); + assert_eq!( + validate_uri("gcs://bucket/path").unwrap(), + StorageScheme::Gcs + ); + assert_eq!( + validate_uri("azblob://container/path").unwrap(), + StorageScheme::Azblob + ); + assert_eq!( + validate_uri("file:///tmp/backup").unwrap(), + StorageScheme::File + ); + } + + #[test] + fn test_validate_uri_invalid() { + // Bare paths should be rejected + assert!(validate_uri("/tmp/backup").is_err()); + assert!(validate_uri("./backup").is_err()); + assert!(validate_uri("backup").is_err()); + + // Unknown schemes + assert!(validate_uri("ftp://server/path").is_err()); + } + + #[test] + fn test_extract_remote_location_requires_non_empty_root() { + assert!(extract_remote_location_with_root_policy("s3://bucket", false).is_err()); + assert!(extract_remote_location_with_root_policy("s3://bucket/", false).is_err()); + assert!(extract_remote_location_with_root_policy("oss://bucket", false).is_err()); + assert!(extract_remote_location_with_root_policy("gs://bucket", false).is_err()); + assert!(extract_remote_location_with_root_policy("azblob://container", false).is_err()); + } + + #[test] + fn test_extract_remote_location_allows_empty_root_when_permitted() { + let location = extract_remote_location_with_root_policy("s3://bucket", true).unwrap(); + assert_eq!(location.bucket_or_container, "bucket"); + assert_eq!(location.root, ""); + + let location = + extract_remote_location_with_root_policy("azblob://container/", true).unwrap(); + assert_eq!(location.bucket_or_container, "container"); + assert_eq!(location.root, ""); + } + + #[test] + fn test_parent_storage_allows_s3_bucket_root() { + let mut storage = ObjectStoreConfig { + enable_s3: true, + ..Default::default() + }; + storage.s3.s3_region = Some("us-east-1".to_string()); + + assert!(OpenDalStorage::from_uri("s3://bucket", &storage).is_err()); + assert!(OpenDalStorage::from_parent_uri("s3://bucket", &storage).is_ok()); + } + + #[cfg(not(windows))] + #[test] + fn test_extract_path_from_uri_unix_examples() { + assert_eq!( + extract_file_path_from_uri("file:///tmp/backup").unwrap(), + "/tmp/backup" + ); + assert_eq!( + extract_file_path_from_uri("file://localhost/tmp/backup").unwrap(), + "/tmp/backup" + ); + assert_eq!( + extract_file_path_from_uri("file:///tmp/my%20backup").unwrap(), + "/tmp/my backup" + ); + assert_eq!( + extract_file_path_from_uri("file://localhost/tmp/my%20backup").unwrap(), + "/tmp/my backup" + ); + } + + #[test] + fn test_extract_file_path_from_uri_rejects_file_host() { + assert!(extract_file_path_from_uri("file://tmp/backup").is_err()); + } + + #[test] + fn test_extract_file_path_from_uri_round_trips_directory_url() { + let dir = tempdir().unwrap(); + let uri = Url::from_directory_path(dir.path()).unwrap().to_string(); + let path = extract_file_path_from_uri(&uri).unwrap(); + + assert_eq!(Path::new(&path), dir.path()); + } + + #[tokio::test] + async fn test_read_manifest_reports_requested_uri() { + let dir = tempdir().unwrap(); + let uri = Url::from_directory_path(dir.path()).unwrap().to_string(); + let storage = OpenDalStorage::from_file_uri(&uri).unwrap(); + + let error = storage.read_manifest().await.unwrap_err().to_string(); + + assert!(error.contains(uri.as_str())); + } + + #[tokio::test] + async fn test_manifest_round_trip() { + let dir = tempdir().unwrap(); + let storage = make_storage_with_rooted_fs(dir.path()); + + let manifest = Manifest::new_full( + "greptime".to_string(), + vec!["public".to_string()], + TimeRange::unbounded(), + DataFormat::Parquet, + ); + + storage.write_manifest(&manifest).await.unwrap(); + let loaded = storage.read_manifest().await.unwrap(); + + assert_eq!(loaded.catalog, manifest.catalog); + assert_eq!(loaded.schemas, manifest.schemas); + assert_eq!(loaded.schema_only, manifest.schema_only); + assert_eq!(loaded.format, manifest.format); + assert_eq!(loaded.snapshot_id, manifest.snapshot_id); + } + + #[tokio::test] + async fn test_schema_round_trip() { + let dir = tempdir().unwrap(); + let storage = make_storage_with_rooted_fs(dir.path()); + + let mut snapshot = SchemaSnapshot::new(); + snapshot.add_schema(SchemaDefinition { + catalog: "greptime".to_string(), + name: "test_db".to_string(), + options: HashMap::from([("ttl".to_string(), "7d".to_string())]), + }); + + storage.write_schema(&snapshot).await.unwrap(); + let loaded = storage.read_schema().await.unwrap(); + + assert_eq!(loaded, snapshot); + } + + #[tokio::test] + async fn test_text_round_trip() { + let dir = tempdir().unwrap(); + let storage = make_storage_with_rooted_fs(dir.path()); + let content = "CREATE TABLE metrics (ts TIMESTAMP TIME INDEX);"; + + storage + .write_text("schema/ddl/public.sql", content) + .await + .unwrap(); + let loaded = storage.read_text("schema/ddl/public.sql").await.unwrap(); + + assert_eq!(loaded, content); + } + + #[tokio::test] + async fn test_read_text_rejects_invalid_utf8() { + let dir = tempdir().unwrap(); + let storage = make_storage_with_rooted_fs(dir.path()); + + storage + .write_file("schema/ddl/public.sql", vec![0xff, 0xfe, 0xfd]) + .await + .unwrap(); + + let error = storage + .read_text("schema/ddl/public.sql") + .await + .unwrap_err(); + assert!(error.to_string().contains("UTF-8")); + } + + #[tokio::test] + async fn test_exists_follows_manifest_presence() { + let dir = tempdir().unwrap(); + let storage = make_storage_with_rooted_fs(dir.path()); + + assert!(!storage.exists().await.unwrap()); + + storage + .write_manifest(&Manifest::new_schema_only( + "greptime".to_string(), + vec!["public".to_string()], + )) + .await + .unwrap(); + + assert!(storage.exists().await.unwrap()); + } + + #[tokio::test] + async fn test_delete_snapshot_only_removes_rooted_contents() { + let parent = tempdir().unwrap(); + let snapshot_root = parent.path().join("snapshot"); + let sibling = parent.path().join("sibling"); + std::fs::create_dir_all(&snapshot_root).unwrap(); + std::fs::create_dir_all(&sibling).unwrap(); + std::fs::write(snapshot_root.join("manifest.json"), b"{}").unwrap(); + std::fs::write(sibling.join("keep.txt"), b"keep").unwrap(); + + let storage = make_storage_with_rooted_fs(&snapshot_root); + storage.delete_snapshot().await.unwrap(); + + assert!(!snapshot_root.join("manifest.json").exists()); + assert!(sibling.join("keep.txt").exists()); + } +} diff --git a/src/cli/src/data/sql.rs b/src/cli/src/data/sql.rs new file mode 100644 index 0000000000..7de4206b26 --- /dev/null +++ b/src/cli/src/data/sql.rs @@ -0,0 +1,40 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Shared SQL escaping helpers for CLI-generated statements. + +pub(crate) fn escape_sql_literal(value: &str) -> String { + value.replace('\'', "''") +} + +pub(crate) fn escape_sql_identifier(value: &str) -> String { + value.replace('"', "\"\"") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_escape_sql_literal_escapes_single_quotes() { + assert_eq!(escape_sql_literal("test_db"), "test_db"); + assert_eq!(escape_sql_literal("te'st"), "te''st"); + } + + #[test] + fn test_escape_sql_identifier_escapes_double_quotes() { + assert_eq!(escape_sql_identifier("test_db"), "test_db"); + assert_eq!(escape_sql_identifier(r#"te"st"#), r#"te""st"#); + } +} diff --git a/src/cli/src/database.rs b/src/cli/src/database.rs index db98c38e38..cba27129dd 100644 --- a/src/cli/src/database.rs +++ b/src/cli/src/database.rs @@ -36,6 +36,7 @@ pub struct DatabaseClient { auth_header: Option, timeout: Duration, proxy: Option, + no_proxy: bool, } pub fn parse_proxy_opts( @@ -61,6 +62,7 @@ impl DatabaseClient { auth_basic: Option, timeout: Duration, proxy: Option, + no_proxy: bool, ) -> Self { let auth_header = if let Some(basic) = auth_basic { let encoded = general_purpose::STANDARD.encode(basic); @@ -69,7 +71,9 @@ impl DatabaseClient { None }; - if let Some(ref proxy) = proxy { + if no_proxy { + common_telemetry::info!("Proxy disabled"); + } else if let Some(ref proxy) = proxy { common_telemetry::info!("Using proxy: {:?}", proxy); } else { common_telemetry::info!("Using system proxy(if any)"); @@ -81,9 +85,14 @@ impl DatabaseClient { auth_header, timeout, proxy, + no_proxy, } } + pub fn addr(&self) -> &str { + &self.addr + } + pub async fn sql_in_public(&self, sql: &str) -> Result>>> { self.sql(sql, DEFAULT_SCHEMA_NAME).await } @@ -95,12 +104,14 @@ impl DatabaseClient { ("db", format!("{}-{}", self.catalog, schema)), ("sql", sql.to_string()), ]; - let client = self - .proxy - .clone() - .map(|proxy| reqwest::Client::builder().proxy(proxy).build()) - .unwrap_or_else(|| Ok(reqwest::Client::new())) - .context(BuildClientSnafu)?; + let mut builder = reqwest::Client::builder(); + if let Some(proxy) = self.proxy.clone() { + builder = builder.proxy(proxy); + } + if self.no_proxy { + builder = builder.no_proxy(); + } + let client = builder.build().context(BuildClientSnafu)?; let mut request = client .post(&url) .form(¶ms) diff --git a/src/cli/src/lib.rs b/src/cli/src/lib.rs index acf5df4086..4305da9c8f 100644 --- a/src/cli/src/lib.rs +++ b/src/cli/src/lib.rs @@ -29,7 +29,7 @@ pub use database::DatabaseClient; use error::Result; pub use crate::bench::BenchTableMetadataCommand; -pub use crate::data::DataCommand; +pub use crate::data::{DataCommand, export_v2, import_v2}; pub use crate::metadata::MetadataCommand; #[async_trait] diff --git a/src/cli/src/metadata.rs b/src/cli/src/metadata.rs index 5800293cb6..beb62a2deb 100644 --- a/src/cli/src/metadata.rs +++ b/src/cli/src/metadata.rs @@ -21,7 +21,7 @@ use clap::Subcommand; use common_error::ext::BoxedError; use crate::Tool; -use crate::metadata::control::{DelCommand, GetCommand}; +use crate::metadata::control::{DelCommand, GetCommand, PutCommand}; use crate::metadata::repair::RepairCommand; use crate::metadata::snapshot::SnapshotCommand; @@ -37,6 +37,8 @@ pub enum MetadataCommand { #[clap(subcommand)] Del(DelCommand), #[clap(subcommand)] + Put(PutCommand), + #[clap(subcommand)] Repair(RepairCommand), } @@ -47,6 +49,7 @@ impl MetadataCommand { MetadataCommand::Repair(cmd) => cmd.build().await, MetadataCommand::Get(cmd) => cmd.build().await, MetadataCommand::Del(cmd) => cmd.build().await, + MetadataCommand::Put(cmd) => cmd.build().await, } } } diff --git a/src/cli/src/metadata/control.rs b/src/cli/src/metadata/control.rs index 3fba582ce5..eaf5b167b5 100644 --- a/src/cli/src/metadata/control.rs +++ b/src/cli/src/metadata/control.rs @@ -14,9 +14,12 @@ mod del; mod get; +mod put; +mod selector; #[cfg(test)] mod test_utils; mod utils; pub(crate) use del::DelCommand; pub(crate) use get::GetCommand; +pub(crate) use put::PutCommand; diff --git a/src/cli/src/metadata/control/del/table.rs b/src/cli/src/metadata/control/del/table.rs index d1346928fe..c92e573224 100644 --- a/src/cli/src/metadata/control/del/table.rs +++ b/src/cli/src/metadata/control/del/table.rs @@ -14,111 +14,59 @@ use async_trait::async_trait; use clap::Parser; -use client::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME}; -use common_catalog::format_full_table_name; use common_error::ext::BoxedError; use common_meta::ddl::utils::get_region_wal_options; use common_meta::key::TableMetadataManager; -use common_meta::key::table_name::TableNameManager; use common_meta::kv_backend::KvBackendRef; use store_api::storage::TableId; use crate::Tool; use crate::common::StoreConfig; -use crate::error::{InvalidArgumentsSnafu, TableNotFoundSnafu}; +use crate::error::TableNotFoundSnafu; use crate::metadata::control::del::CLI_TOMBSTONE_PREFIX; -use crate::metadata::control::utils::get_table_id_by_name; +use crate::metadata::control::selector::TableSelector; /// Delete table metadata logically from the metadata store. #[derive(Debug, Default, Parser)] pub struct DelTableCommand { - /// The table id to delete from the metadata store. - #[clap(long)] - table_id: Option, - - /// The table name to delete from the metadata store. - #[clap(long)] - table_name: Option, - - /// The schema name of the table. - #[clap(long, default_value = DEFAULT_SCHEMA_NAME)] - schema_name: String, - - /// The catalog name of the table. - #[clap(long, default_value = DEFAULT_CATALOG_NAME)] - catalog_name: String, + #[clap(flatten)] + selector: TableSelector, /// The store config. #[clap(flatten)] store: StoreConfig, } -impl DelTableCommand { - fn validate(&self) -> Result<(), BoxedError> { - if matches!( - (&self.table_id, &self.table_name), - (Some(_), Some(_)) | (None, None) - ) { - return Err(BoxedError::new( - InvalidArgumentsSnafu { - msg: "You must specify either --table-id or --table-name.", - } - .build(), - )); - } - Ok(()) - } -} - impl DelTableCommand { pub async fn build(&self) -> Result, BoxedError> { - self.validate()?; + self.selector.validate()?; let kv_backend = self.store.build().await?; Ok(Box::new(DelTableTool { - table_id: self.table_id, - table_name: self.table_name.clone(), - schema_name: self.schema_name.clone(), - catalog_name: self.catalog_name.clone(), - table_name_manager: TableNameManager::new(kv_backend.clone()), + selector: self.selector.clone(), table_metadata_deleter: TableMetadataDeleter::new(kv_backend), })) } } struct DelTableTool { - table_id: Option, - table_name: Option, - schema_name: String, - catalog_name: String, - table_name_manager: TableNameManager, + selector: TableSelector, table_metadata_deleter: TableMetadataDeleter, } #[async_trait] impl Tool for DelTableTool { async fn do_work(&self) -> Result<(), BoxedError> { - let table_id = if let Some(table_name) = &self.table_name { - let catalog_name = &self.catalog_name; - let schema_name = &self.schema_name; - - let Some(table_id) = get_table_id_by_name( - &self.table_name_manager, - catalog_name, - schema_name, - table_name, + let Some(table_id) = self + .selector + .resolve_table_id( + self.table_metadata_deleter + .table_metadata_manager + .table_name_manager(), ) .await? - else { - println!( - "Table({}) not found", - format_full_table_name(catalog_name, schema_name, table_name) - ); - return Ok(()); - }; - table_id - } else { - // Safety: we have validated that table_id or table_name is not None - self.table_id.unwrap() + else { + println!("Table({}) not found", self.selector.formatted_table_name()); + return Ok(()); }; self.table_metadata_deleter.delete(table_id).await?; println!("Table({}) deleted", table_id); @@ -182,6 +130,7 @@ mod tests { use std::collections::HashMap; use std::sync::Arc; + use clap::Parser; use common_error::ext::ErrorExt; use common_error::status_code::StatusCode; use common_meta::key::TableMetadataManager; @@ -192,9 +141,83 @@ mod tests { use common_meta::rpc::store::RangeRequest; use crate::metadata::control::del::CLI_TOMBSTONE_PREFIX; - use crate::metadata::control::del::table::TableMetadataDeleter; + use crate::metadata::control::del::table::{DelTableCommand, TableMetadataDeleter}; use crate::metadata::control::test_utils::prepare_physical_table_metadata; + #[tokio::test] + async fn test_del_table_selector_requires_single_target() { + let command = DelTableCommand::parse_from([ + "table", + "--backend", + "memory-store", + "--store-addrs", + "memory://", + ]); + + let err = match command.build().await { + Ok(_) => panic!("expected validation failure"), + Err(err) => err, + }; + assert!( + err.output_msg() + .contains("You must specify either --table-id or --table-name.") + ); + } + + #[tokio::test] + async fn test_del_table_selector_rejects_both_targets() { + let command = DelTableCommand::parse_from([ + "table", + "--table-id", + "1024", + "--table-name", + "my_table", + "--backend", + "memory-store", + "--store-addrs", + "memory://", + ]); + + let err = match command.build().await { + Ok(_) => panic!("expected validation failure"), + Err(err) => err, + }; + assert!( + err.output_msg() + .contains("You must specify either --table-id or --table-name.") + ); + } + + #[tokio::test] + async fn test_del_table_command_builds_tool_with_table_id() { + let command = DelTableCommand::parse_from([ + "table", + "--table-id", + "1024", + "--backend", + "memory-store", + "--store-addrs", + "memory://", + ]); + + let _tool = command.build().await.unwrap(); + } + + #[tokio::test] + async fn test_del_table_command_builds_tool_with_table_name() { + let command = DelTableCommand::parse_from([ + "table", + "--table-name", + "my_table", + "--backend", + "memory-store", + "--store-addrs", + "memory://", + ]); + + let _tool = command.build().await.unwrap(); + } + #[tokio::test] async fn test_delete_table_not_found() { let kv_backend = Arc::new(MemoryKvBackend::new()) as KvBackendRef; diff --git a/src/cli/src/metadata/control/get.rs b/src/cli/src/metadata/control/get.rs index 1de2bb370e..8a1de07acf 100644 --- a/src/cli/src/metadata/control/get.rs +++ b/src/cli/src/metadata/control/get.rs @@ -16,8 +16,6 @@ use std::cmp::min; use async_trait::async_trait; use clap::{Parser, Subcommand}; -use client::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME}; -use common_catalog::format_full_table_name; use common_error::ext::BoxedError; use common_meta::key::TableMetadataManager; use common_meta::key::table_info::TableInfoKey; @@ -29,8 +27,8 @@ use futures::TryStreamExt; use crate::Tool; use crate::common::StoreConfig; -use crate::error::InvalidArgumentsSnafu; -use crate::metadata::control::utils::{decode_key_value, get_table_id_by_name, json_formatter}; +use crate::metadata::control::selector::TableSelector; +use crate::metadata::control::utils::{decode_key_value, json_formatter}; /// Getting metadata from metadata store. #[derive(Subcommand)] @@ -120,21 +118,8 @@ impl Tool for GetKeyTool { /// Get table metadata from the metadata store via table id. #[derive(Debug, Default, Parser)] pub struct GetTableCommand { - /// Get table metadata by table id. - #[clap(long)] - table_id: Option, - - /// Get table metadata by table name. - #[clap(long)] - table_name: Option, - - /// The schema name of the table. - #[clap(long, default_value = DEFAULT_SCHEMA_NAME)] - schema_name: String, - - /// The catalog name of the table. - #[clap(long, default_value = DEFAULT_CATALOG_NAME)] - catalog_name: String, + #[clap(flatten)] + selector: TableSelector, /// Pretty print the output. #[clap(long, default_value = "false")] @@ -144,29 +129,9 @@ pub struct GetTableCommand { store: StoreConfig, } -impl GetTableCommand { - pub fn validate(&self) -> Result<(), BoxedError> { - if matches!( - (&self.table_id, &self.table_name), - (Some(_), Some(_)) | (None, None) - ) { - return Err(BoxedError::new( - InvalidArgumentsSnafu { - msg: "You must specify either --table-id or --table-name.", - } - .build(), - )); - } - Ok(()) - } -} - struct GetTableTool { kvbackend: KvBackendRef, - table_id: Option, - table_name: Option, - schema_name: String, - catalog_name: String, + selector: TableSelector, pretty: bool, } @@ -178,24 +143,9 @@ impl Tool for GetTableTool { let table_info_manager = table_metadata_manager.table_info_manager(); let table_route_manager = table_metadata_manager.table_route_manager(); - let table_id = if let Some(table_name) = &self.table_name { - let catalog_name = &self.catalog_name; - let schema_name = &self.schema_name; - - let Some(table_id) = - get_table_id_by_name(table_name_manager, catalog_name, schema_name, table_name) - .await? - else { - println!( - "Table({}) not found", - format_full_table_name(catalog_name, schema_name, table_name) - ); - return Ok(()); - }; - table_id - } else { - // Safety: we have validated that table_id or table_name is not None - self.table_id.unwrap() + let Some(table_id) = self.selector.resolve_table_id(table_name_manager).await? else { + println!("Table({}) not found", self.selector.formatted_table_name()); + return Ok(()); }; let table_info = table_info_manager @@ -233,15 +183,94 @@ impl Tool for GetTableTool { impl GetTableCommand { pub async fn build(&self) -> Result, BoxedError> { - self.validate()?; + self.selector.validate()?; let kvbackend = self.store.build().await?; Ok(Box::new(GetTableTool { kvbackend, - table_id: self.table_id, - table_name: self.table_name.clone(), - schema_name: self.schema_name.clone(), - catalog_name: self.catalog_name.clone(), + selector: self.selector.clone(), pretty: self.pretty, })) } } + +#[cfg(test)] +mod tests { + use clap::Parser; + use common_error::ext::ErrorExt; + + use super::GetTableCommand; + + #[tokio::test] + async fn test_get_table_selector_requires_single_target() { + let command = GetTableCommand::parse_from([ + "table", + "--backend", + "memory-store", + "--store-addrs", + "memory://", + ]); + + let err = match command.build().await { + Ok(_) => panic!("expected validation failure"), + Err(err) => err, + }; + assert!( + err.output_msg() + .contains("You must specify either --table-id or --table-name.") + ); + } + + #[tokio::test] + async fn test_get_table_selector_rejects_both_targets() { + let command = GetTableCommand::parse_from([ + "table", + "--table-id", + "1024", + "--table-name", + "my_table", + "--backend", + "memory-store", + "--store-addrs", + "memory://", + ]); + + let err = match command.build().await { + Ok(_) => panic!("expected validation failure"), + Err(err) => err, + }; + assert!( + err.output_msg() + .contains("You must specify either --table-id or --table-name.") + ); + } + + #[tokio::test] + async fn test_get_table_command_builds_tool_with_table_id() { + let command = GetTableCommand::parse_from([ + "table", + "--table-id", + "1024", + "--backend", + "memory-store", + "--store-addrs", + "memory://", + ]); + + let _tool = command.build().await.unwrap(); + } + + #[tokio::test] + async fn test_get_table_command_builds_tool_with_table_name() { + let command = GetTableCommand::parse_from([ + "table", + "--table-name", + "my_table", + "--backend", + "memory-store", + "--store-addrs", + "memory://", + ]); + + let _tool = command.build().await.unwrap(); + } +} diff --git a/src/cli/src/metadata/control/put.rs b/src/cli/src/metadata/control/put.rs new file mode 100644 index 0000000000..c2c535c57a --- /dev/null +++ b/src/cli/src/metadata/control/put.rs @@ -0,0 +1,56 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod key; +mod table; + +use clap::Subcommand; +use common_error::ext::BoxedError; +use snafu::ResultExt; +use tokio::io::{AsyncRead, AsyncReadExt}; + +use crate::Tool; +use crate::error::FileIoSnafu; +use crate::metadata::control::put::key::PutKeyCommand; +use crate::metadata::control::put::table::PutTableCommand; + +pub(crate) async fn read_value(mut reader: R) -> Result, BoxedError> +where + R: AsyncRead + Unpin, +{ + let mut value = Vec::new(); + reader + .read_to_end(&mut value) + .await + .context(FileIoSnafu) + .map_err(BoxedError::new)?; + Ok(value) +} + +/// Subcommand for putting metadata into the metadata store. +#[derive(Subcommand)] +pub enum PutCommand { + Key(PutKeyCommand), + #[clap(subcommand)] + Table(PutTableCommand), +} + +impl PutCommand { + pub async fn build(&self) -> Result, BoxedError> { + match self { + PutCommand::Key(cmd) => cmd.build().await, + PutCommand::Table(cmd) => cmd.build().await, + } + } +} diff --git a/src/cli/src/metadata/control/put/key.rs b/src/cli/src/metadata/control/put/key.rs new file mode 100644 index 0000000000..7becfd72dc --- /dev/null +++ b/src/cli/src/metadata/control/put/key.rs @@ -0,0 +1,444 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use async_trait::async_trait; +use clap::Parser; +use common_error::ext::BoxedError; +use common_meta::key::catalog_name::{CatalogNameKey, CatalogNameValue}; +use common_meta::key::flow::flow_state::FlowStateValue; +use common_meta::key::flow::{ + flow_info_key_prefix, flow_name_key_prefix, flow_route_key_prefix, flow_state_full_key, + flownode_flow_key_prefix, table_flow_key_prefix, +}; +use common_meta::key::node_address::{NodeAddressKey, NodeAddressValue}; +use common_meta::key::schema_name::{SchemaNameKey, SchemaNameValue}; +use common_meta::key::table_repart::{TableRepartKey, TableRepartValue}; +use common_meta::key::topic_name::{TopicNameKey, TopicNameValue}; +use common_meta::key::topic_region::{TopicRegionKey, TopicRegionValue}; +use common_meta::key::view_info::{ViewInfoKey, ViewInfoValue}; +use common_meta::key::{ + CATALOG_NAME_KEY_PREFIX, DATANODE_TABLE_KEY_PREFIX, KAFKA_TOPIC_KEY_PREFIX, MetadataKey, + MetadataValue, NODE_ADDRESS_PREFIX, SCHEMA_NAME_KEY_PREFIX, TABLE_INFO_KEY_PREFIX, + TABLE_NAME_KEY_PREFIX, TABLE_REPART_PREFIX, TABLE_ROUTE_PREFIX, TOPIC_REGION_PREFIX, + VIEW_INFO_KEY_PREFIX, +}; +use common_meta::kv_backend::KvBackendRef; +use common_meta::rpc::store::PutRequest; + +use crate::Tool; +use crate::common::StoreConfig; +use crate::error::InvalidArgumentsSnafu; +use crate::metadata::control::put::read_value; + +/// Put a key-value pair into the metadata store. +#[derive(Debug, Default, Parser)] +pub struct PutKeyCommand { + /// The key to put into the metadata store. + key: String, + + /// Read the value to put into the metadata store from standard input. + #[clap(long, required = true)] + value_stdin: bool, + + /// Skip metadata validation before writing. + #[clap(long)] + no_validate: bool, + + #[clap(flatten)] + store: StoreConfig, +} + +impl PutKeyCommand { + pub async fn build(&self) -> Result, BoxedError> { + let kv_backend = self.store.build().await?; + self.build_tool(tokio::io::stdin(), kv_backend).await + } + + async fn build_tool( + &self, + reader: R, + kv_backend: KvBackendRef, + ) -> Result, BoxedError> + where + R: tokio::io::AsyncRead + Unpin, + { + Ok(Box::new(PutKeyTool { + kv_backend, + key: self.key.clone(), + value: read_value(reader).await?, + no_validate: self.no_validate, + })) + } +} + +struct PutKeyTool { + kv_backend: KvBackendRef, + key: String, + value: Vec, + no_validate: bool, +} + +#[async_trait] +impl Tool for PutKeyTool { + async fn do_work(&self) -> Result<(), BoxedError> { + if !self.no_validate { + validate_metadata_value(&self.key, &self.value)?; + } + + let request = PutRequest::new() + .with_key(self.key.as_bytes()) + .with_value(self.value.clone()); + self.kv_backend + .put(request) + .await + .map_err(BoxedError::new)?; + + println!("Key({}) updated", self.key); + Ok(()) + } +} + +fn validate_metadata_value(key: &str, value: &[u8]) -> Result<(), BoxedError> { + if let Some(reason) = unsupported_direct_put_reason(key) { + return Err(BoxedError::new( + InvalidArgumentsSnafu { + msg: format!("{reason}, use --no-validate to bypass"), + } + .build(), + )); + } + + if key == flow_state_full_key() { + validate_value(key, value, FlowStateValue::try_from_raw_value)?; + return Ok(()); + } else if matches_key_prefix(key, VIEW_INFO_KEY_PREFIX) { + validate_key(ViewInfoKey::from_bytes(key.as_bytes()), key)?; + validate_value(key, value, ViewInfoValue::try_from_raw_value)?; + return Ok(()); + } else if matches_key_prefix(key, CATALOG_NAME_KEY_PREFIX) { + validate_key(CatalogNameKey::from_bytes(key.as_bytes()), key)?; + validate_value(key, value, CatalogNameValue::try_from_raw_value)?; + return Ok(()); + } else if matches_key_prefix(key, SCHEMA_NAME_KEY_PREFIX) { + validate_key(SchemaNameKey::from_bytes(key.as_bytes()), key)?; + validate_value(key, value, SchemaNameValue::try_from_raw_value)?; + return Ok(()); + } else if matches_key_prefix(key, TABLE_REPART_PREFIX) { + validate_key(TableRepartKey::from_bytes(key.as_bytes()), key)?; + validate_value(key, value, TableRepartValue::try_from_raw_value)?; + return Ok(()); + } else if matches_key_prefix(key, NODE_ADDRESS_PREFIX) { + validate_key(NodeAddressKey::from_bytes(key.as_bytes()), key)?; + validate_value(key, value, NodeAddressValue::try_from_raw_value)?; + return Ok(()); + } else if matches_key_prefix(key, KAFKA_TOPIC_KEY_PREFIX) { + validate_key(TopicNameKey::from_bytes(key.as_bytes()), key)?; + validate_value(key, value, TopicNameValue::try_from_raw_value)?; + return Ok(()); + } else if matches_key_prefix(key, TOPIC_REGION_PREFIX) { + validate_key(TopicRegionKey::from_bytes(key.as_bytes()), key)?; + validate_value(key, value, TopicRegionValue::try_from_raw_value)?; + return Ok(()); + } + + Err(BoxedError::new( + InvalidArgumentsSnafu { + msg: format!( + "Unsupported metadata key for validation: {key}, use --no-validate to bypass" + ), + } + .build(), + )) +} + +/// Returns the rejection reason for keys that should not be updated by `put key`. +/// +/// These keys may be decodable, but they are not safe to update via raw KV writes. +/// `__table_route/*` is the canonical example. +fn unsupported_direct_put_reason(key: &str) -> Option { + let flow_info_prefix = flow_info_key_prefix(); + let flow_name_prefix = flow_name_key_prefix(); + let flow_route_prefix = flow_route_key_prefix(); + let table_flow_prefix = table_flow_key_prefix(); + let flownode_flow_prefix = flownode_flow_key_prefix(); + + let (prefix, target) = [ + (TABLE_ROUTE_PREFIX, "table route metadata"), + (TABLE_INFO_KEY_PREFIX, "table info metadata"), + (TABLE_NAME_KEY_PREFIX, "table name metadata"), + (DATANODE_TABLE_KEY_PREFIX, "datanode table metadata"), + (&flow_info_prefix, "flow info metadata"), + (&flow_name_prefix, "flow name metadata"), + (&flow_route_prefix, "flow route metadata"), + (&table_flow_prefix, "flow source table metadata"), + (&flownode_flow_prefix, "flownode flow metadata"), + ] + .into_iter() + .find(|(prefix, _)| matches_key_prefix(key, prefix))?; + + Some(format!( + "Direct put is not supported for {target} ({prefix}*); use a dedicated metadata update interface instead" + )) +} + +fn matches_key_prefix(key: &str, prefix: &str) -> bool { + key == prefix + || key + .strip_prefix(prefix) + .is_some_and(|rest| rest.starts_with('/')) +} + +fn validate_value(key: &str, value: &[u8], parser: F) -> Result<(), BoxedError> +where + F: FnOnce(&[u8]) -> common_meta::error::Result, +{ + parser(value).map_err(|e| { + BoxedError::new( + InvalidArgumentsSnafu { + msg: format!("Invalid metadata value for key: {key}: {e}"), + } + .build(), + ) + })?; + Ok(()) +} + +fn validate_key(result: common_meta::error::Result, key: &str) -> Result<(), BoxedError> { + result.map_err(|e| { + BoxedError::new( + InvalidArgumentsSnafu { + msg: format!("Invalid metadata key: {key}: {e}"), + } + .build(), + ) + })?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use std::sync::Arc; + + use clap::Parser; + use common_error::ext::{BoxedError, ErrorExt}; + use common_meta::key::flow::flow_state::FlowStateValue; + use common_meta::key::flow::flow_state_full_key; + use common_meta::key::schema_name::SchemaNameValue; + use common_meta::key::topic_name::TopicNameValue; + use common_meta::key::{KAFKA_TOPIC_KEY_PREFIX, MetadataValue, SCHEMA_NAME_KEY_PREFIX}; + use common_meta::kv_backend::KvBackendRef; + use common_meta::kv_backend::memory::MemoryKvBackend; + use tokio::io::BufReader; + + use super::{ + PutKeyCommand, PutKeyTool, TABLE_ROUTE_PREFIX, matches_key_prefix, + unsupported_direct_put_reason, validate_metadata_value, + }; + use crate::Tool; + + impl PutKeyCommand { + async fn build_for_test( + &self, + reader: R, + kv_backend: KvBackendRef, + ) -> Result, BoxedError> + where + R: tokio::io::AsyncRead + Unpin, + { + self.build_tool(reader, kv_backend).await + } + } + + #[test] + fn test_validate_supported_key_success() { + let value = SchemaNameValue::default().try_as_raw_value().unwrap(); + + validate_metadata_value(&format!("{SCHEMA_NAME_KEY_PREFIX}/greptime/public"), &value) + .unwrap(); + } + + #[test] + fn test_validate_supported_key_invalid_value() { + let err = validate_metadata_value( + &format!("{KAFKA_TOPIC_KEY_PREFIX}/test-topic"), + b"not-a-valid-json-value", + ) + .unwrap_err(); + + assert!(err.output_msg().contains("Invalid metadata value for key")); + } + + #[test] + fn test_validate_complex_key_fails() { + let value = serde_json::to_vec(&BTreeMap::::new()).unwrap(); + let err = + validate_metadata_value(&format!("{TABLE_ROUTE_PREFIX}/1024"), &value).unwrap_err(); + + assert!( + err.output_msg() + .contains("Direct put is not supported for table route metadata") + ); + } + + #[test] + fn test_validate_unknown_key_fails() { + let err = validate_metadata_value("__unknown/foo", b"{}").unwrap_err(); + + assert!( + err.output_msg() + .contains("Unsupported metadata key for validation") + ); + } + + #[test] + fn test_validate_invalid_supported_key_fails() { + let value = SchemaNameValue::default().try_as_raw_value().unwrap(); + let err = validate_metadata_value("__schema_name/greptime", &value).unwrap_err(); + + assert!( + err.output_msg() + .contains("Invalid metadata key: __schema_name/greptime") + ); + } + + #[test] + fn test_unsupported_direct_put_reason_covers_complex_keys() { + let cases = [ + "__table_route/1024", + "__table_info/1024", + "__table_name/greptime/public/demo", + "__dn_table/1/1024", + "__flow/route/1/1", + ]; + + for key in cases { + assert!(unsupported_direct_put_reason(key).is_some(), "key: {key}"); + } + } + + #[test] + fn test_matches_key_prefix() { + assert!(matches_key_prefix("__table_route", "__table_route")); + assert!(matches_key_prefix("__table_route/1024", "__table_route")); + assert!(!matches_key_prefix( + "__table_route_extra/1024", + "__table_route" + )); + assert!(!matches_key_prefix("__table_routex", "__table_route")); + assert!(!matches_key_prefix( + "__topic_name/kafka_backup/foo", + "__topic_name/kafka" + )); + } + + #[test] + fn test_validate_exact_flow_state_key() { + let value = FlowStateValue::new(BTreeMap::new(), BTreeMap::new()) + .try_as_raw_value() + .unwrap(); + + validate_metadata_value(&flow_state_full_key(), &value).unwrap(); + } + + #[tokio::test] + async fn test_put_key_tool_writes_supported_key() { + let kv_backend = Arc::new(MemoryKvBackend::new()) as KvBackendRef; + let value = TopicNameValue::new(42).try_as_raw_value().unwrap(); + let key = format!("{KAFKA_TOPIC_KEY_PREFIX}/test-topic"); + let tool = PutKeyTool { + kv_backend: kv_backend.clone(), + key: key.clone(), + value: value.clone(), + no_validate: false, + }; + + tool.do_work().await.unwrap(); + + let stored = kv_backend.get(key.as_bytes()).await.unwrap().unwrap(); + assert_eq!(stored.value, value); + } + + #[tokio::test] + async fn test_put_key_tool_bypasses_validation() { + let kv_backend = Arc::new(MemoryKvBackend::new()) as KvBackendRef; + let key = format!("{TABLE_ROUTE_PREFIX}/1024"); + let value = b"not-json".to_vec(); + let tool = PutKeyTool { + kv_backend: kv_backend.clone(), + key: key.clone(), + value: value.clone(), + no_validate: true, + }; + + tool.do_work().await.unwrap(); + + let stored = kv_backend.get(key.as_bytes()).await.unwrap().unwrap(); + assert_eq!(stored.value, value); + } + + #[test] + fn test_put_key_command_requires_value_stdin() { + let err = PutKeyCommand::try_parse_from([ + "key", + "__topic_name/kafka/test-cli-topic", + "--backend", + "memory-store", + "--store-addrs", + "memory://", + ]) + .unwrap_err(); + + assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument); + } + + #[tokio::test] + async fn test_put_key_command_builds_tool_with_stdin() { + let value = TopicNameValue::new(7).try_as_raw_value().unwrap(); + let command = PutKeyCommand::parse_from([ + "key", + "__topic_name/kafka/test-cli-topic", + "--value-stdin", + "--backend", + "memory-store", + "--store-addrs", + "memory://", + ]); + + let tool = command + .build_for_test( + BufReader::new(value.as_slice()), + Arc::new(MemoryKvBackend::new()) as KvBackendRef, + ) + .await + .unwrap(); + tool.do_work().await.unwrap(); + } + + #[tokio::test] + async fn test_put_key_command_validate_failure() { + let tool = PutKeyTool { + kv_backend: Arc::new(MemoryKvBackend::new()) as KvBackendRef, + key: "__table_route/1024".to_string(), + value: b"{}".to_vec(), + no_validate: false, + }; + let err = tool.do_work().await.unwrap_err(); + + assert!( + err.output_msg() + .contains("Direct put is not supported for table route metadata") + ); + } +} diff --git a/src/cli/src/metadata/control/put/table.rs b/src/cli/src/metadata/control/put/table.rs new file mode 100644 index 0000000000..29e5c546d5 --- /dev/null +++ b/src/cli/src/metadata/control/put/table.rs @@ -0,0 +1,687 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashSet; + +use async_trait::async_trait; +use clap::{Parser, Subcommand}; +use common_error::ext::BoxedError; +use common_meta::key::datanode_table::{DatanodeTableKey, RegionInfo}; +use common_meta::key::table_info::TableInfoValue; +use common_meta::key::table_route::TableRouteValue; +use common_meta::key::{ + DeserializedValueWithBytes, MetadataValue, RegionDistribution, TableMetadataManager, +}; +use common_meta::kv_backend::KvBackendRef; +use common_meta::rpc::router::{RegionRoute, region_distribution}; +use snafu::{OptionExt, ensure}; +use store_api::storage::TableId; +use table::metadata::TableInfo; + +use crate::Tool; +use crate::common::StoreConfig; +use crate::error::{Error, InvalidArgumentsSnafu, TableNotFoundSnafu, UnexpectedSnafu}; +use crate::metadata::control::put::read_value; +use crate::metadata::control::selector::TableSelector; + +/// Put table metadata into the metadata store. +#[derive(Subcommand)] +pub enum PutTableCommand { + Info(PutTableInfoCommand), + Route(PutTableRouteCommand), +} + +impl PutTableCommand { + pub async fn build(&self) -> Result, BoxedError> { + match self { + PutTableCommand::Info(cmd) => cmd.build().await, + PutTableCommand::Route(cmd) => cmd.build().await, + } + } +} + +/// Put table info into the metadata store. +#[derive(Debug, Parser)] +pub struct PutTableInfoCommand { + #[clap(flatten)] + selector: TableSelector, + + /// Read the JSON-encoded [`TableInfoValue`] from standard input. + #[clap(long, required = true)] + value_stdin: bool, + + #[clap(flatten)] + store: StoreConfig, +} + +impl PutTableInfoCommand { + pub async fn build(&self) -> Result, BoxedError> { + let kv_backend = self.store.build().await?; + self.build_tool(tokio::io::stdin(), kv_backend).await + } + + async fn build_tool( + &self, + reader: R, + kv_backend: KvBackendRef, + ) -> Result, BoxedError> + where + R: tokio::io::AsyncRead + Unpin, + { + self.selector.validate()?; + Ok(Box::new(PutTableInfoTool { + kv_backend, + selector: self.selector.clone(), + value: read_value(reader).await?, + })) + } +} + +struct PutTableInfoTool { + kv_backend: KvBackendRef, + selector: TableSelector, + value: Vec, +} + +#[async_trait] +impl Tool for PutTableInfoTool { + async fn do_work(&self) -> Result<(), BoxedError> { + let table_metadata_manager = TableMetadataManager::new(self.kv_backend.clone()); + let Some(table_id) = self + .selector + .resolve_table_id(table_metadata_manager.table_name_manager()) + .await? + else { + return Err(BoxedError::new( + UnexpectedSnafu { + msg: format!("Table({}) not found", self.selector.formatted_table_name()), + } + .build(), + )); + }; + + let (current_table_info, current_table_route) = + load_table_metadata(&table_metadata_manager, table_id).await?; + let new_table_info = TableInfoValue::try_from_raw_value(&self.value) + .map_err(|e| { + BoxedError::new( + InvalidArgumentsSnafu { + msg: format!("Invalid table info JSON: {e}"), + } + .build(), + ) + })? + .table_info; + validate_table_info(table_id, ¤t_table_info.table_info, &new_table_info) + .map_err(BoxedError::new)?; + + let region_distribution = + physical_region_distribution(current_table_route.get_inner_ref())?; + + if current_table_info.table_info != new_table_info { + table_metadata_manager + .update_table_info(¤t_table_info, region_distribution, new_table_info) + .await + .map_err(BoxedError::new)?; + println!("Table({table_id}) info updated"); + } + + Ok(()) + } +} + +/// Put table route into the metadata store. +#[derive(Debug, Parser)] +pub struct PutTableRouteCommand { + #[clap(flatten)] + selector: TableSelector, + + /// Read the JSON-encoded [`TableRouteValue`] from standard input. + #[clap(long, required = true)] + value_stdin: bool, + + #[clap(flatten)] + store: StoreConfig, +} + +impl PutTableRouteCommand { + pub async fn build(&self) -> Result, BoxedError> { + let kv_backend = self.store.build().await?; + self.build_tool(tokio::io::stdin(), kv_backend).await + } + + async fn build_tool( + &self, + reader: R, + kv_backend: KvBackendRef, + ) -> Result, BoxedError> + where + R: tokio::io::AsyncRead + Unpin, + { + self.selector.validate()?; + Ok(Box::new(PutTableRouteTool { + kv_backend, + selector: self.selector.clone(), + value: read_value(reader).await?, + })) + } +} + +struct PutTableRouteTool { + kv_backend: KvBackendRef, + selector: TableSelector, + value: Vec, +} + +#[async_trait] +impl Tool for PutTableRouteTool { + async fn do_work(&self) -> Result<(), BoxedError> { + let table_metadata_manager = TableMetadataManager::new(self.kv_backend.clone()); + let Some(table_id) = self + .selector + .resolve_table_id(table_metadata_manager.table_name_manager()) + .await? + else { + return Err(BoxedError::new( + UnexpectedSnafu { + msg: format!("Table({}) not found", self.selector.formatted_table_name()), + } + .build(), + )); + }; + + let (current_table_info, current_table_route) = + load_table_metadata(&table_metadata_manager, table_id).await?; + let current_region_routes = current_table_route + .region_routes() + .map_err(BoxedError::new)?; + let new_table_route = TableRouteValue::try_from_raw_value(&self.value).map_err(|e| { + BoxedError::new( + InvalidArgumentsSnafu { + msg: format!("Invalid table route JSON: {e}"), + } + .build(), + ) + })?; + let new_region_routes = new_table_route.region_routes().map_err(BoxedError::new)?; + validate_table_route(table_id, new_region_routes, current_region_routes) + .map_err(BoxedError::new)?; + let region_info = + load_region_info(&table_metadata_manager, table_id, current_region_routes).await?; + let new_region_options = current_table_info.table_info.to_region_options(); + let new_region_wal_options = region_info.region_wal_options.clone(); + + if current_table_route.get_inner_ref() != &new_table_route { + table_metadata_manager + .update_table_route( + table_id, + region_info, + ¤t_table_route, + new_region_routes.clone(), + &new_region_options, + &new_region_wal_options, + ) + .await + .map_err(BoxedError::new)?; + println!("Table({table_id}) route updated"); + } + + Ok(()) + } +} + +fn validate_table_route( + table_id: TableId, + new_region_routes: &[RegionRoute], + current_region_route: &[RegionRoute], +) -> Result<(), Error> { + let current_region_ids = current_region_route + .iter() + .map(|r| r.region.id) + .collect::>(); + for route in new_region_routes { + ensure!( + route.region.id.table_id() == table_id, + InvalidArgumentsSnafu { + msg: format!( + "Invalid table route: all region routes must have table id {table_id}, but got {}", + route.region.id.table_id() + ), + } + ); + // Ensure the region in new route exists in current route + current_region_ids + .contains(&route.region.id) + .then_some(()) + .context(InvalidArgumentsSnafu { + msg: format!( + "Invalid table route: region {} does not exist in current routes", + route.region.id + ), + })?; + } + + Ok(()) +} + +fn validate_table_info( + table_id: TableId, + current_table_info: &TableInfo, + new_table_info: &TableInfo, +) -> Result<(), Error> { + ensure!( + new_table_info.ident.table_id == table_id, + InvalidArgumentsSnafu { + msg: format!( + "Invalid table info: expected table id {table_id}, got {}", + new_table_info.ident.table_id + ), + } + ); + + ensure!( + current_table_info.catalog_name == new_table_info.catalog_name, + InvalidArgumentsSnafu { + msg: format!( + "Invalid table info: catalog name is immutable, expected {}, got {}", + current_table_info.catalog_name, new_table_info.catalog_name + ), + } + ); + + ensure!( + current_table_info.schema_name == new_table_info.schema_name, + InvalidArgumentsSnafu { + msg: format!( + "Invalid table info: schema name is immutable, expected {}, got {}", + current_table_info.schema_name, new_table_info.schema_name + ), + } + ); + + ensure!( + current_table_info.name == new_table_info.name, + InvalidArgumentsSnafu { + msg: format!( + "Invalid table info: table name is immutable, expected {}, got {}", + current_table_info.name, new_table_info.name + ), + } + ); + + Ok(()) +} + +async fn load_region_info( + table_metadata_manager: &TableMetadataManager, + table_id: TableId, + region_routes: &[RegionRoute], +) -> Result { + let datanode_id = region_distribution(region_routes) + .into_keys() + .next() + .ok_or_else(|| { + BoxedError::new( + UnexpectedSnafu { + msg: format!( + "Missing datanode assignment for physical table route: {table_id}" + ), + } + .build(), + ) + })?; + + table_metadata_manager + .datanode_table_manager() + .get(&DatanodeTableKey::new(datanode_id, table_id)) + .await + .map_err(BoxedError::new)? + .map(|value| value.region_info) + .ok_or_else(|| { + BoxedError::new( + UnexpectedSnafu { + msg: format!( + "Missing datanode table metadata for physical table route: {table_id}" + ), + } + .build(), + ) + }) +} + +async fn load_table_metadata( + table_metadata_manager: &TableMetadataManager, + table_id: TableId, +) -> Result< + ( + DeserializedValueWithBytes, + DeserializedValueWithBytes, + ), + BoxedError, +> { + let (table_info, table_route) = table_metadata_manager + .get_full_table_info(table_id) + .await + .map_err(BoxedError::new)?; + let table_info = + table_info.ok_or_else(|| BoxedError::new(TableNotFoundSnafu { table_id }.build()))?; + let table_route = + table_route.ok_or_else(|| BoxedError::new(TableNotFoundSnafu { table_id }.build()))?; + Ok((table_info, table_route)) +} + +fn physical_region_distribution( + table_route: &TableRouteValue, +) -> Result, BoxedError> { + if !table_route.is_physical() { + return Ok(None); + } + + table_route + .region_routes() + .map(|routes| Some(region_distribution(routes))) + .map_err(BoxedError::new) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::sync::Arc; + + use clap::Parser; + use common_error::ext::{BoxedError, ErrorExt}; + use common_meta::key::TableMetadataManager; + use common_meta::key::datanode_table::{DatanodeTableKey, DatanodeTableManager}; + use common_meta::key::table_info::TableInfoValue; + use common_meta::key::table_route::TableRouteValue; + use common_meta::kv_backend::KvBackendRef; + use common_meta::kv_backend::memory::MemoryKvBackend; + use common_meta::peer::Peer; + use common_meta::rpc::router::RegionRoute; + use store_api::storage::RegionId; + use tokio::io::BufReader; + + use super::{ + PutTableInfoCommand, PutTableInfoTool, PutTableRouteCommand, PutTableRouteTool, + validate_table_route, + }; + use crate::Tool; + use crate::metadata::control::selector::TableSelector; + use crate::metadata::control::test_utils::prepare_physical_table_metadata; + + impl PutTableInfoCommand { + async fn build_for_test( + &self, + reader: R, + kv_backend: KvBackendRef, + ) -> Result, BoxedError> + where + R: tokio::io::AsyncRead + Unpin, + { + self.build_tool(reader, kv_backend).await + } + } + + impl PutTableRouteCommand { + async fn build_for_test( + &self, + reader: R, + kv_backend: KvBackendRef, + ) -> Result, BoxedError> + where + R: tokio::io::AsyncRead + Unpin, + { + self.build_tool(reader, kv_backend).await + } + } + + #[tokio::test] + async fn test_put_table_selector_validation() { + let command = PutTableInfoCommand::parse_from([ + "info", + "--value-stdin", + "--backend", + "memory-store", + "--store-addrs", + "memory://", + ]); + + let err = match command.build().await { + Ok(_) => panic!("expected validation failure"), + Err(err) => err, + }; + assert!( + err.output_msg() + .contains("You must specify either --table-id or --table-name.") + ); + } + + #[tokio::test] + async fn test_put_table_command_builds_tool_with_table_name() { + let command = PutTableInfoCommand::parse_from([ + "info", + "--table-name", + "my_table", + "--value-stdin", + "--backend", + "memory-store", + "--store-addrs", + "memory://", + ]); + + let _tool = command + .build_for_test( + BufReader::new(&b"{}"[..]), + Arc::new(MemoryKvBackend::new()) as KvBackendRef, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_put_table_info_rejects_table_name_change() { + let kv_backend = Arc::new(MemoryKvBackend::new()) as KvBackendRef; + let table_metadata_manager = TableMetadataManager::new(kv_backend.clone()); + let table_id = 1024; + let (table_info, table_route) = + prepare_physical_table_metadata("old_table", table_id).await; + table_metadata_manager + .create_table_metadata( + table_info.clone(), + TableRouteValue::Physical(table_route), + HashMap::new(), + ) + .await + .unwrap(); + + let mut new_table_info = table_info; + new_table_info.name = "new_table".to_string(); + let tool = PutTableInfoTool { + kv_backend: kv_backend.clone(), + selector: TableSelector::with_table_id(table_id), + value: serde_json::to_vec(&TableInfoValue::new(new_table_info)).unwrap(), + }; + + let err = tool.do_work().await.unwrap_err(); + assert!( + err.output_msg() + .contains("Invalid table info: table name is immutable") + ); + } + + #[tokio::test] + async fn test_put_table_info_rejects_schema_change() { + let kv_backend = Arc::new(MemoryKvBackend::new()) as KvBackendRef; + let table_metadata_manager = TableMetadataManager::new(kv_backend.clone()); + let table_id = 1024; + let (table_info, table_route) = + prepare_physical_table_metadata("old_table", table_id).await; + table_metadata_manager + .create_table_metadata( + table_info.clone(), + TableRouteValue::Physical(table_route), + HashMap::new(), + ) + .await + .unwrap(); + + let mut new_table_info = table_info; + new_table_info.schema_name = "another_schema".to_string(); + let tool = PutTableInfoTool { + kv_backend, + selector: TableSelector::with_table_id(table_id), + value: serde_json::to_vec(&TableInfoValue::new(new_table_info)).unwrap(), + }; + + let err = tool.do_work().await.unwrap_err(); + assert!( + err.output_msg() + .contains("Invalid table info: schema name is immutable") + ); + } + + #[tokio::test] + async fn test_put_table_route_updates_route_and_datanode_table() { + let kv_backend = Arc::new(MemoryKvBackend::new()) as KvBackendRef; + let table_metadata_manager = TableMetadataManager::new(kv_backend.clone()); + let table_id = 1024; + let (table_info, table_route) = prepare_physical_table_metadata("my_table", table_id).await; + table_metadata_manager + .create_table_metadata( + table_info, + TableRouteValue::Physical(table_route.clone()), + HashMap::new(), + ) + .await + .unwrap(); + + let mut region_routes = table_route.region_routes.clone(); + region_routes[0].leader_peer = Some(Peer::empty(2)); + let new_table_route = TableRouteValue::physical(region_routes); + let tool = PutTableRouteTool { + kv_backend: kv_backend.clone(), + selector: TableSelector::with_table_id(table_id), + value: serde_json::to_vec(&new_table_route).unwrap(), + }; + + tool.do_work().await.unwrap(); + + let (_, current_route) = table_metadata_manager + .get_full_table_info(table_id) + .await + .unwrap(); + let current_route = current_route.unwrap().into_inner(); + assert_eq!( + current_route.region_routes().unwrap(), + new_table_route.region_routes().unwrap() + ); + + let datanode_table_manager = DatanodeTableManager::new(kv_backend); + let updated = datanode_table_manager + .get(&DatanodeTableKey::new(2, table_id)) + .await + .unwrap(); + assert!(updated.is_some()); + } + + #[tokio::test] + async fn test_put_table_route_rejects_logical_route() { + let kv_backend = Arc::new(MemoryKvBackend::new()) as KvBackendRef; + let table_metadata_manager = TableMetadataManager::new(kv_backend.clone()); + let table_id = 1024; + let (table_info, table_route) = prepare_physical_table_metadata("my_table", table_id).await; + table_metadata_manager + .create_table_metadata( + table_info, + TableRouteValue::Physical(table_route), + HashMap::new(), + ) + .await + .unwrap(); + + let tool = PutTableRouteTool { + kv_backend, + selector: TableSelector::with_table_id(table_id), + value: serde_json::to_vec(&TableRouteValue::logical(table_id + 1)).unwrap(), + }; + + let err = tool.do_work().await.unwrap_err(); + assert!(err.output_msg().contains("non-physical TableRouteValue.")); + } + + #[test] + fn test_validate_table_route_rejects_new_region_not_in_current_route() { + let table_id = 1024; + let current_region_routes = vec![ + RegionRoute { + region: common_meta::rpc::router::Region { + id: RegionId::new(table_id, 1), + ..Default::default() + }, + ..Default::default() + }, + RegionRoute { + region: common_meta::rpc::router::Region { + id: RegionId::new(table_id, 2), + ..Default::default() + }, + ..Default::default() + }, + ]; + let new_region_routes = vec![ + RegionRoute { + region: common_meta::rpc::router::Region { + id: RegionId::new(table_id, 1), + ..Default::default() + }, + ..Default::default() + }, + RegionRoute { + region: common_meta::rpc::router::Region { + id: RegionId::new(table_id, 3), + ..Default::default() + }, + ..Default::default() + }, + ]; + + let err = + validate_table_route(table_id, ¤t_region_routes, &new_region_routes).unwrap_err(); + + assert!(err.to_string().contains("does not exist in current routes")); + } + + #[tokio::test] + async fn test_put_table_command_builds_tool() { + let value = serde_json::to_vec(&TableRouteValue::logical(1025)).unwrap(); + let command = PutTableRouteCommand::parse_from([ + "route", + "--table-id", + "1024", + "--value-stdin", + "--backend", + "memory-store", + "--store-addrs", + "memory://", + ]); + + let _tool = command + .build_for_test( + BufReader::new(value.as_slice()), + Arc::new(MemoryKvBackend::new()) as KvBackendRef, + ) + .await + .unwrap(); + } +} diff --git a/src/cli/src/metadata/control/selector.rs b/src/cli/src/metadata/control/selector.rs new file mode 100644 index 0000000000..6d3c47fa9c --- /dev/null +++ b/src/cli/src/metadata/control/selector.rs @@ -0,0 +1,100 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::Args; +use client::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME}; +use common_catalog::format_full_table_name; +use common_error::ext::BoxedError; +use common_meta::key::table_name::TableNameManager; +use store_api::storage::TableId; + +use crate::error::InvalidArgumentsSnafu; +use crate::metadata::control::utils::get_table_id_by_name; + +/// Selects a table by id or by fully qualified name. +#[derive(Debug, Clone, Default, Args)] +pub(crate) struct TableSelector { + /// The table id to select from the metadata store. + #[clap(long)] + table_id: Option, + + /// The table name to select from the metadata store. + #[clap(long)] + table_name: Option, + + /// The schema name of the table. + #[clap(long, default_value = DEFAULT_SCHEMA_NAME)] + schema_name: String, + + /// The catalog name of the table. + #[clap(long, default_value = DEFAULT_CATALOG_NAME)] + catalog_name: String, +} + +impl TableSelector { + pub(crate) fn validate(&self) -> Result<(), BoxedError> { + if matches!( + (&self.table_id, &self.table_name), + (Some(_), Some(_)) | (None, None) + ) { + return Err(BoxedError::new( + InvalidArgumentsSnafu { + msg: "You must specify either --table-id or --table-name.", + } + .build(), + )); + } + + Ok(()) + } + + pub(crate) async fn resolve_table_id( + &self, + table_name_manager: &TableNameManager, + ) -> Result, BoxedError> { + if let Some(table_id) = self.table_id { + return Ok(Some(table_id)); + } + + get_table_id_by_name( + table_name_manager, + &self.catalog_name, + &self.schema_name, + self.table_name + .as_deref() + .expect("validated table selector"), + ) + .await + } + + pub(crate) fn formatted_table_name(&self) -> String { + format_full_table_name( + &self.catalog_name, + &self.schema_name, + self.table_name.as_deref().unwrap_or_default(), + ) + } +} + +#[cfg(test)] +impl TableSelector { + pub(crate) fn with_table_id(table_id: u32) -> Self { + Self { + table_id: Some(table_id), + table_name: None, + schema_name: DEFAULT_SCHEMA_NAME.to_string(), + catalog_name: DEFAULT_CATALOG_NAME.to_string(), + } + } +} diff --git a/src/cli/src/metadata/snapshot.rs b/src/cli/src/metadata/snapshot.rs index 648d3a687d..59b2599ad9 100644 --- a/src/cli/src/metadata/snapshot.rs +++ b/src/cli/src/metadata/snapshot.rs @@ -16,7 +16,7 @@ use async_trait::async_trait; use clap::{Parser, Subcommand}; use common_error::ext::BoxedError; use common_meta::snapshot::MetadataSnapshotManager; -use object_store::{ObjectStore, Scheme}; +use object_store::{ObjectStore, services}; use crate::Tool; use crate::common::{ObjectStoreConfig, StoreConfig, new_fs_object_store}; @@ -276,7 +276,7 @@ fn build_object_store_and_resolve_file_path( None => new_fs_object_store(fs_root)?, }; - let file_path = if matches!(object_store.info().scheme(), Scheme::Fs) { + let file_path = if object_store.info().scheme() == services::FS_SCHEME { resolve_relative_path_with_current_dir(file_path).map_err(BoxedError::new)? } else { file_path.to_string() diff --git a/src/client/src/database.rs b/src/client/src/database.rs index 6a7ac62fc3..13ff9fc651 100644 --- a/src/client/src/database.rs +++ b/src/client/src/database.rs @@ -14,7 +14,9 @@ use std::pin::Pin; use std::str::FromStr; -use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, RwLock}; +use std::task::{Context, Poll}; use api::v1::auth_header::AuthScheme; use api::v1::ddl_request::Expr as DdlExpr; @@ -25,6 +27,7 @@ use api::v1::{ AlterTableExpr, AuthHeader, Basic, CreateTableExpr, DdlRequest, GreptimeRequest, InsertRequests, QueryRequest, RequestHeader, RowInsertRequests, }; +use arc_swap::ArcSwapOption; use arrow_flight::{FlightData, Ticket}; use async_stream::stream; use base64::Engine; @@ -33,17 +36,18 @@ use common_catalog::build_db_string; use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME}; use common_error::ext::BoxedError; use common_grpc::flight::do_put::DoPutResponse; -use common_grpc::flight::{FlightDecoder, FlightMessage}; +use common_grpc::flight::{FLOW_EXTENSIONS_METADATA_KEY, FlightDecoder, FlightMessage}; use common_query::Output; +use common_recordbatch::adapter::RecordBatchMetrics; use common_recordbatch::error::ExternalSnafu; -use common_recordbatch::{RecordBatch, RecordBatchStreamWrapper}; +use common_recordbatch::{OrderOption, RecordBatch, RecordBatchStream, RecordBatchStreamWrapper}; use common_telemetry::tracing::Span; use common_telemetry::tracing_context::W3cTrace; use common_telemetry::{error, warn}; use futures::future; use futures_util::{Stream, StreamExt, TryStreamExt}; use prost::Message; -use snafu::{OptionExt, ResultExt, ensure}; +use snafu::{OptionExt, ResultExt}; use tonic::metadata::{AsciiMetadataKey, AsciiMetadataValue, MetadataMap, MetadataValue}; use tonic::transport::Channel; @@ -57,6 +61,313 @@ type FlightDataStream = Pin + Send>>; type DoPutResponseStream = Pin>>>; +/// Terminal metrics associated with a query output. +/// +/// For streaming outputs, metrics are only final after the stream is fully +/// drained and [`Self::is_ready`] returns `true`. +#[derive(Debug, Clone, Default)] +pub struct OutputMetrics { + inner: Arc, +} + +#[derive(Debug, Default)] +struct OutputMetricsInner { + metrics: RwLock>, + ready: AtomicBool, +} + +impl OutputMetrics { + fn new() -> Self { + Self::default() + } + + /// Replaces the current terminal metrics snapshot. + pub fn update(&self, metrics: Option) { + *self.inner.metrics.write().unwrap() = metrics; + } + + /// Marks the terminal metrics as final for this output. + pub fn mark_ready(&self) { + let _ = self + .inner + .ready + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire); + } + + /// Returns whether terminal metrics are final. + /// + /// Streaming outputs become ready only after the stream reaches EOF. + pub fn is_ready(&self) -> bool { + self.inner.ready.load(Ordering::Acquire) + } + + /// Returns the latest terminal metrics snapshot, if any. + pub fn get(&self) -> Option { + self.inner.metrics.read().unwrap().clone() + } + + /// Returns proved per-region watermarks. + /// + /// Entries whose watermark is `None` are intentionally omitted because they + /// represent participating regions whose terminal sequence bound was not + /// provable. + pub fn region_watermark_map(&self) -> Option> { + Some( + self.get()? + .region_watermarks + .into_iter() + .filter_map(|entry| entry.watermark.map(|seq| (entry.region_id, seq))) + .collect::>(), + ) + } + + /// Returns all regions that participated in terminal metric collection, + /// including entries whose watermark is `None`. + pub fn participating_regions(&self) -> Option> { + Some( + self.get()? + .region_watermarks + .into_iter() + .map(|entry| entry.region_id) + .collect::>(), + ) + } +} + +/// Query output together with a handle for its terminal metrics. +/// +/// The contained [`OutputMetrics`] lets callers read stream terminal metrics +/// after consuming `output`. For non-stream outputs, metrics are ready +/// immediately. +#[derive(Debug)] +pub struct OutputWithMetrics { + pub output: Output, + pub metrics: OutputMetrics, +} + +impl OutputWithMetrics { + /// Wraps an output with a terminal metrics handle. + /// + /// Stream outputs update the handle as the stream is consumed. Non-stream + /// outputs are marked ready immediately. + pub fn from_output(output: Output) -> Self { + let terminal_metrics = OutputMetrics::new(); + let output = attach_terminal_metrics(output, &terminal_metrics); + Self { + output, + metrics: terminal_metrics, + } + } + + /// Returns proved per-region watermarks from the terminal metrics. + pub fn region_watermark_map(&self) -> Option> { + self.metrics.region_watermark_map() + } + + /// Returns all regions participating in terminal metric collection. + pub fn participating_regions(&self) -> Option> { + self.metrics.participating_regions() + } + + /// Drops the terminal metrics handle and returns the original output. + pub fn into_output(self) -> Output { + self.output + } +} + +fn parse_terminal_metrics(metrics_json: &str) -> Result { + serde_json::from_str(metrics_json).map_err(|e| { + IllegalFlightMessagesSnafu { + reason: format!("Invalid terminal metrics message: {e}"), + } + .build() + }) +} + +struct StreamWithMetrics { + stream: common_recordbatch::SendableRecordBatchStream, + metrics: OutputMetrics, +} + +impl StreamWithMetrics { + fn new(stream: common_recordbatch::SendableRecordBatchStream, metrics: OutputMetrics) -> Self { + Self { stream, metrics } + } + + fn sync_terminal_metrics(&self) { + self.metrics.update(self.stream.metrics()); + } +} + +impl RecordBatchStream for StreamWithMetrics { + fn name(&self) -> &str { + self.stream.name() + } + + fn schema(&self) -> datatypes::schema::SchemaRef { + self.stream.schema() + } + + fn output_ordering(&self) -> Option<&[OrderOption]> { + self.stream.output_ordering() + } + + fn metrics(&self) -> Option { + self.sync_terminal_metrics(); + self.metrics.get() + } +} + +impl Stream for StreamWithMetrics { + type Item = common_recordbatch::error::Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let polled = Pin::new(&mut self.stream).poll_next(cx); + if let Poll::Ready(None) = &polled { + self.sync_terminal_metrics(); + self.metrics.mark_ready(); + } + polled + } + + fn size_hint(&self) -> (usize, Option) { + self.stream.size_hint() + } +} + +fn attach_terminal_metrics(output: Output, terminal_metrics: &OutputMetrics) -> Output { + let Output { data, meta } = output; + let data = match data { + common_query::OutputData::Stream(stream) => { + terminal_metrics.update(stream.metrics()); + common_query::OutputData::Stream(Box::pin(StreamWithMetrics::new( + stream, + terminal_metrics.clone(), + ))) + } + other => { + terminal_metrics.mark_ready(); + other + } + }; + Output::new(data, meta) +} + +async fn output_from_flight_message_stream( + mut flight_message_stream: S, +) -> Result +where + S: Stream> + Send + Unpin + 'static, +{ + let Some(first_flight_message) = flight_message_stream.next().await else { + return IllegalFlightMessagesSnafu { + reason: "Expect the response not to be empty", + } + .fail(); + }; + + let first_flight_message = first_flight_message?; + + match first_flight_message { + FlightMessage::AffectedRows { rows, metrics } => { + let terminal_metrics = OutputMetrics::new(); + if let Some(metrics) = metrics { + terminal_metrics.update(Some(parse_terminal_metrics(&metrics)?)); + } + let next_message = flight_message_stream.next().await.transpose()?; + match next_message { + None => terminal_metrics.mark_ready(), + Some(FlightMessage::Metrics(s)) if terminal_metrics.get().is_none() => { + terminal_metrics.update(Some(parse_terminal_metrics(&s)?)); + terminal_metrics.mark_ready(); + } + Some(FlightMessage::Metrics(_)) => { + return IllegalFlightMessagesSnafu { + reason: "'AffectedRows' Flight metadata already carries Metrics and cannot be followed by another Metrics message", + } + .fail(); + } + Some(other) => { + return IllegalFlightMessagesSnafu { + reason: format!( + "'AffectedRows' Flight message can only be followed by a Metrics message, got {other:?}" + ), + } + .fail(); + } + } + Ok(OutputWithMetrics { + output: Output::new_with_affected_rows(rows), + metrics: terminal_metrics, + }) + } + FlightMessage::RecordBatch(_) | FlightMessage::Metrics(_) => IllegalFlightMessagesSnafu { + reason: "The first flight message cannot be a RecordBatch or Metrics message", + } + .fail(), + FlightMessage::Schema(schema) => { + let metrics = Arc::new(ArcSwapOption::from(None)); + let metrics_ref = metrics.clone(); + let schema = Arc::new( + datatypes::schema::Schema::try_from(schema).context(error::ConvertSchemaSnafu)?, + ); + let schema_cloned = schema.clone(); + let stream = Box::pin(stream!({ + while let Some(flight_message_item) = flight_message_stream.next().await { + let flight_message = match flight_message_item { + Ok(message) => message, + Err(e) => { + yield Err(BoxedError::new(e)).context(ExternalSnafu); + break; + } + }; + + match flight_message { + FlightMessage::RecordBatch(arrow_batch) => { + yield Ok(RecordBatch::from_df_record_batch( + schema_cloned.clone(), + arrow_batch, + )) + } + FlightMessage::Metrics(s) => { + match parse_terminal_metrics(&s) { + Ok(m) => { + metrics_ref.swap(Some(Arc::new(m))); + } + Err(e) => { + yield Err(BoxedError::new(e)).context(ExternalSnafu); + } + }; + } + FlightMessage::AffectedRows { .. } | FlightMessage::Schema(_) => { + yield IllegalFlightMessagesSnafu { + reason: format!( + "A Schema message must be succeeded exclusively by a set of RecordBatch messages, flight_message: {:?}", + flight_message + ) + } + .fail() + .map_err(BoxedError::new) + .context(ExternalSnafu); + break; + } + } + } + })); + let record_batch_stream = RecordBatchStreamWrapper { + schema, + stream, + output_ordering: None, + metrics, + span: Span::current(), + }; + Ok(OutputWithMetrics::from_output(Output::new_with_stream( + Box::pin(record_batch_stream), + ))) + } + } +} + #[derive(Clone, Debug, Default)] pub struct Database { // The "catalog" and "schema" to be used in processing the requests at the server side. @@ -238,6 +549,22 @@ impl Database { Ok(()) } + fn put_flow_extensions( + metadata: &mut MetadataMap, + flow_extensions: &[(&str, &str)], + ) -> Result<()> { + if flow_extensions.is_empty() { + return Ok(()); + } + + let value = serde_json::to_string(&flow_extensions.to_vec()) + .expect("flow extension pairs should serialize"); + let key = AsciiMetadataKey::from_static(FLOW_EXTENSIONS_METADATA_KEY); + let value = AsciiMetadataValue::from_str(&value).context(InvalidTonicMetadataValueSnafu)?; + metadata.insert(key, value); + Ok(()) + } + /// Make a request to the database. pub async fn handle(&self, request: Request) -> Result { let mut client = make_database_client(&self.client)?; @@ -333,15 +660,58 @@ impl Database { let request = Request::Query(QueryRequest { query: Some(Query::Sql(sql.as_ref().to_string())), }); - self.do_get(request, hints).await + self.do_get(request, hints, &[]) + .await + .map(OutputWithMetrics::into_output) + } + + /// Executes a SQL query and returns the output with terminal metrics. + /// + /// For stream outputs, callers must consume the stream before reading final + /// terminal metrics from [`OutputWithMetrics::metrics`]. + pub async fn sql_with_terminal_metrics( + &self, + sql: S, + hints: &[(&str, &str)], + ) -> Result + where + S: AsRef, + { + self.query_with_terminal_metrics_and_flow_extensions( + QueryRequest { + query: Some(Query::Sql(sql.as_ref().to_string())), + }, + hints, + &[], + ) + .await } /// Executes a logical plan directly without SQL parsing. pub async fn logical_plan(&self, logical_plan: Vec) -> Result { - let request = Request::Query(QueryRequest { - query: Some(Query::LogicalPlan(logical_plan)), - }); - self.do_get(request, &[]).await + self.query_with_terminal_metrics_and_flow_extensions( + QueryRequest { + query: Some(Query::LogicalPlan(logical_plan)), + }, + &[], + &[], + ) + .await + .map(OutputWithMetrics::into_output) + } + + /// Executes a query and carries flow extensions through Flight metadata. + /// + /// This is the lower-level terminal-metrics API for Flow callers that need + /// to pass JSON-bearing flow extensions without going through hint metadata. + pub async fn query_with_terminal_metrics_and_flow_extensions( + &self, + request: QueryRequest, + hints: &[(&str, &str)], + flow_extensions: &[(&str, &str)], + ) -> Result { + self.do_get(Request::Query(request), hints, flow_extensions) + .await } /// Creates a new table using the provided table expression. @@ -349,7 +719,9 @@ impl Database { let request = Request::Ddl(DdlRequest { expr: Some(DdlExpr::CreateTable(expr)), }); - self.do_get(request, &[]).await + self.do_get(request, &[], &[]) + .await + .map(OutputWithMetrics::into_output) } /// Alters an existing table using the provided alter expression. @@ -357,17 +729,26 @@ impl Database { let request = Request::Ddl(DdlRequest { expr: Some(DdlExpr::AlterTable(expr)), }); - self.do_get(request, &[]).await + self.do_get(request, &[], &[]) + .await + .map(OutputWithMetrics::into_output) } - async fn do_get(&self, request: Request, hints: &[(&str, &str)]) -> Result { + async fn do_get( + &self, + request: Request, + hints: &[(&str, &str)], + flow_extensions: &[(&str, &str)], + ) -> Result { let request = self.to_rpc_request(request); let request = Ticket { ticket: request.encode_to_vec().into(), }; let mut request = tonic::Request::new(request); - Self::put_hints(request.metadata_mut(), hints)?; + let metadata = request.metadata_mut(); + Self::put_hints(metadata, hints)?; + Self::put_flow_extensions(metadata, flow_extensions)?; let mut client = self.client.make_flight_client(false, false)?; @@ -389,7 +770,7 @@ impl Database { let flight_data_stream = response.into_inner(); let mut decoder = FlightDecoder::default(); - let mut flight_message_stream = flight_data_stream.map(move |flight_data| { + let flight_message_stream = flight_data_stream.map(move |flight_data| { flight_data .map_err(Error::from) .and_then(|data| decoder.try_decode(&data).context(ConvertFlightDataSnafu))? @@ -398,70 +779,7 @@ impl Database { }) }); - let Some(first_flight_message) = flight_message_stream.next().await else { - return IllegalFlightMessagesSnafu { - reason: "Expect the response not to be empty", - } - .fail(); - }; - - let first_flight_message = first_flight_message?; - - match first_flight_message { - FlightMessage::AffectedRows(rows) => { - ensure!( - flight_message_stream.next().await.is_none(), - IllegalFlightMessagesSnafu { - reason: "Expect 'AffectedRows' Flight messages to be the one and the only!" - } - ); - Ok(Output::new_with_affected_rows(rows)) - } - FlightMessage::RecordBatch(_) | FlightMessage::Metrics(_) => { - IllegalFlightMessagesSnafu { - reason: "The first flight message cannot be a RecordBatch or Metrics message", - } - .fail() - } - FlightMessage::Schema(schema) => { - let schema = Arc::new( - datatypes::schema::Schema::try_from(schema) - .context(error::ConvertSchemaSnafu)?, - ); - let schema_cloned = schema.clone(); - let stream = Box::pin(stream!({ - while let Some(flight_message) = flight_message_stream.next().await { - let flight_message = flight_message - .map_err(BoxedError::new) - .context(ExternalSnafu)?; - match flight_message { - FlightMessage::RecordBatch(arrow_batch) => { - yield Ok(RecordBatch::from_df_record_batch( - schema_cloned.clone(), - arrow_batch, - )) - } - FlightMessage::Metrics(_) => {} - FlightMessage::AffectedRows(_) | FlightMessage::Schema(_) => { - yield IllegalFlightMessagesSnafu {reason: format!("A Schema message must be succeeded exclusively by a set of RecordBatch messages, flight_message: {:?}", flight_message)} - .fail() - .map_err(BoxedError::new) - .context(ExternalSnafu); - break; - } - } - } - })); - let record_batch_stream = RecordBatchStreamWrapper { - schema, - stream, - output_ordering: None, - metrics: Default::default(), - span: Span::current(), - }; - Ok(Output::new_with_stream(Box::pin(record_batch_stream))) - } - } + output_from_flight_message_stream(flight_message_stream).await } /// Ingest a stream of [RecordBatch]es that belong to a table, using Arrow Flight's "`DoPut`" @@ -512,16 +830,104 @@ struct FlightContext { #[cfg(test)] mod tests { - use std::assert_matches::assert_matches; + use std::sync::Arc; + use std::task::{Context, Poll}; use api::v1::auth_header::AuthScheme; use api::v1::{AuthHeader, Basic}; use common_error::status_code::StatusCode; + use common_query::OutputData; + use common_recordbatch::{OrderOption, RecordBatch, RecordBatchStream}; + use datatypes::prelude::{ConcreteDataType, VectorRef}; + use datatypes::schema::{ColumnSchema, Schema}; + use datatypes::vectors::Int32Vector; + use futures_util::StreamExt; use tonic::{Code, Status}; use super::*; use crate::error::TonicSnafu; + struct MockMetricsStream { + schema: datatypes::schema::SchemaRef, + batch: Option, + metrics: RecordBatchMetrics, + terminal_metrics_only: bool, + } + + impl Stream for MockMetricsStream { + type Item = common_recordbatch::error::Result; + + fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(self.batch.take().map(Ok)) + } + } + + impl RecordBatchStream for MockMetricsStream { + fn name(&self) -> &str { + "MockMetricsStream" + } + + fn schema(&self) -> datatypes::schema::SchemaRef { + self.schema.clone() + } + + fn output_ordering(&self) -> Option<&[OrderOption]> { + None + } + + fn metrics(&self) -> Option { + if self.terminal_metrics_only && self.batch.is_some() { + return None; + } + Some(self.metrics.clone()) + } + } + + fn terminal_metrics_json() -> String { + terminal_metrics_json_with_seq(42) + } + + fn terminal_metrics_json_with_seq(seq: u64) -> String { + serde_json::to_string(&RecordBatchMetrics { + region_watermarks: vec![common_recordbatch::adapter::RegionWatermarkEntry { + region_id: 7, + watermark: Some(seq), + }], + ..Default::default() + }) + .unwrap() + } + + #[test] + fn test_put_flow_extensions_preserves_comma_bearing_values() { + let mut metadata = MetadataMap::new(); + Database::put_flow_extensions( + &mut metadata, + &[ + ("flow.return_region_seq", "true"), + ("flow.incremental_after_seqs", r#"{"1":10,"2":20}"#), + ], + ) + .unwrap(); + + let value = metadata + .get(FLOW_EXTENSIONS_METADATA_KEY) + .unwrap() + .to_str() + .unwrap(); + let decoded: Vec<(String, String)> = serde_json::from_str(value).unwrap(); + assert_eq!( + decoded, + vec![ + ("flow.return_region_seq".to_string(), "true".to_string()), + ( + "flow.incremental_after_seqs".to_string(), + r#"{"1":10,"2":20}"#.to_string() + ), + ] + ); + } + #[test] fn test_flight_ctx() { let mut ctx = FlightContext::default(); @@ -536,12 +942,12 @@ mod tests { auth_scheme: Some(basic), }); - assert_matches!( + assert!(matches!( ctx.auth_header, Some(AuthHeader { auth_scheme: Some(AuthScheme::Basic(_)), }) - ) + )); } #[test] @@ -558,4 +964,198 @@ mod tests { assert_eq!(expected.to_string(), actual.to_string()); } + + #[tokio::test] + async fn test_query_with_terminal_metrics_tracks_terminal_only_metrics() { + let schema = Arc::new(Schema::new(vec![ColumnSchema::new( + "v", + ConcreteDataType::int32_datatype(), + false, + )])); + let batch = RecordBatch::new( + schema.clone(), + vec![Arc::new(Int32Vector::from_slice([1, 2])) as VectorRef], + ) + .unwrap(); + let output = Output::new_with_stream(Box::pin(MockMetricsStream { + schema, + batch: Some(batch), + metrics: RecordBatchMetrics { + region_watermarks: vec![common_recordbatch::adapter::RegionWatermarkEntry { + region_id: 7, + watermark: Some(42), + }], + ..Default::default() + }, + terminal_metrics_only: true, + })); + + let result = OutputWithMetrics::from_output(output); + let terminal_metrics = result.metrics.clone(); + assert!(!terminal_metrics.is_ready()); + assert!(terminal_metrics.get().is_none()); + + let OutputData::Stream(mut stream) = result.output.data else { + panic!("expected stream output"); + }; + while stream.next().await.is_some() {} + + assert!(terminal_metrics.is_ready()); + assert_eq!( + terminal_metrics.participating_regions(), + Some(std::collections::BTreeSet::from([7_u64])) + ); + assert_eq!( + terminal_metrics.region_watermark_map(), + Some(std::collections::HashMap::from([(7_u64, 42_u64)])) + ); + } + + #[test] + fn test_parse_terminal_metrics_rejects_invalid_json() { + assert!(parse_terminal_metrics("{not-json}").is_err()); + } + + #[tokio::test] + async fn test_affected_rows_inline_metrics_are_parsed() { + let output = output_from_flight_message_stream(futures_util::stream::iter(vec![Ok( + FlightMessage::AffectedRows { + rows: 3, + metrics: Some(terminal_metrics_json()), + }, + )] + as Vec>)) + .await + .unwrap(); + + assert!(matches!(output.output.data, OutputData::AffectedRows(3))); + assert!(output.metrics.is_ready()); + assert_eq!( + output.metrics.region_watermark_map(), + Some(std::collections::HashMap::from([(7, 42)])) + ); + } + + #[tokio::test] + async fn test_affected_rows_inline_metrics_rejects_trailing_metrics() { + let metrics_json = terminal_metrics_json(); + let err = output_from_flight_message_stream(futures_util::stream::iter(vec![ + Ok(FlightMessage::AffectedRows { + rows: 3, + metrics: Some(metrics_json.clone()), + }), + Ok(FlightMessage::Metrics(metrics_json)), + ] + as Vec>)) + .await + .unwrap_err(); + + assert!( + err.to_string().contains("already carries Metrics"), + "unexpected error: {err:?}" + ); + } + + #[tokio::test] + async fn test_invalid_terminal_metrics_after_record_batch_yields_batch_then_error() { + let schema = Arc::new(Schema::new(vec![ColumnSchema::new( + "v", + ConcreteDataType::int32_datatype(), + false, + )])); + let batch = RecordBatch::new( + schema.clone(), + vec![Arc::new(Int32Vector::from_slice([1])) as VectorRef], + ) + .unwrap(); + let output = output_from_flight_message_stream(futures_util::stream::iter(vec![ + Ok(FlightMessage::Schema(schema.arrow_schema().clone())), + Ok(FlightMessage::RecordBatch(batch.into_df_record_batch())), + Ok(FlightMessage::Metrics("{not-json}".to_string())), + ] + as Vec>)) + .await + .unwrap(); + let terminal_metrics = output.metrics.clone(); + let OutputData::Stream(mut record_batch_stream) = output.output.data else { + panic!("expected stream output"); + }; + + let batch = record_batch_stream.next().await.unwrap().unwrap(); + assert_eq!(batch.num_rows(), 1); + + let err = record_batch_stream.next().await.unwrap().unwrap_err(); + assert_eq!("External error", err.to_string()); + assert!( + format!("{err:?}").contains("Invalid terminal metrics message"), + "unexpected error: {err:?}" + ); + assert!(record_batch_stream.next().await.is_none()); + assert!(terminal_metrics.is_ready()); + assert!(terminal_metrics.get().is_none()); + } + + #[tokio::test] + async fn test_record_batch_stream_continues_after_partial_metrics() { + let schema = Arc::new(Schema::new(vec![ColumnSchema::new( + "v", + ConcreteDataType::int32_datatype(), + false, + )])); + let first_batch = RecordBatch::new( + schema.clone(), + vec![Arc::new(Int32Vector::from_slice([1])) as VectorRef], + ) + .unwrap(); + let second_batch = RecordBatch::new( + schema.clone(), + vec![Arc::new(Int32Vector::from_slice([2])) as VectorRef], + ) + .unwrap(); + let output = output_from_flight_message_stream(futures_util::stream::iter(vec![ + Ok(FlightMessage::Schema(schema.arrow_schema().clone())), + Ok(FlightMessage::RecordBatch( + first_batch.into_df_record_batch(), + )), + Ok(FlightMessage::Metrics(terminal_metrics_json_with_seq(1))), + Ok(FlightMessage::RecordBatch( + second_batch.into_df_record_batch(), + )), + Ok(FlightMessage::Metrics(terminal_metrics_json_with_seq(2))), + ] + as Vec>)) + .await + .unwrap(); + let terminal_metrics = output.metrics.clone(); + let OutputData::Stream(mut record_batch_stream) = output.output.data else { + panic!("expected stream output"); + }; + + let first_batch = record_batch_stream.next().await.unwrap().unwrap(); + assert_eq!(first_batch.num_rows(), 1); + let second_batch = record_batch_stream.next().await.unwrap().unwrap(); + assert_eq!(second_batch.num_rows(), 1); + assert!(record_batch_stream.next().await.is_none()); + + assert!(terminal_metrics.is_ready()); + assert_eq!( + terminal_metrics.region_watermark_map(), + Some(std::collections::HashMap::from([(7, 2)])) + ); + } + + #[test] + fn test_output_metrics_distinguishes_empty_region_watermarks_from_absence() { + let metrics = OutputMetrics::default(); + metrics.update(Some(RecordBatchMetrics::default())); + + assert_eq!( + metrics.participating_regions(), + Some(std::collections::BTreeSet::new()) + ); + assert_eq!( + metrics.region_watermark_map(), + Some(std::collections::HashMap::new()) + ); + } } diff --git a/src/client/src/error.rs b/src/client/src/error.rs index 5fdb383358..aa6940dd8b 100644 --- a/src/client/src/error.rs +++ b/src/client/src/error.rs @@ -173,20 +173,31 @@ impl ErrorExt for Error { define_from_tonic_status!(Error, Tonic); impl Error { - pub fn should_retry(&self) -> bool { - // TODO(weny): figure out each case of these codes. - matches!( - self, - Self::RegionServer { - code: Code::Cancelled, - .. - } | Self::RegionServer { - code: Code::DeadlineExceeded, - .. - } | Self::RegionServer { - code: Code::Unavailable, - .. + /// Returns the gRPC status code if this error is caused by a gRPC request failure. + pub fn tonic_code(&self) -> Option { + match self { + Self::FlightGet { tonic_code, .. } + | Self::RegionServer { + code: tonic_code, .. } - ) + | Self::FlowServer { + code: tonic_code, .. + } + | Self::Tonic { tonic_code, .. } => Some(*tonic_code), + _ => None, + } + } + + /// Returns true if the error is a connection error that may be resolved by retrying the request. + pub fn is_connection_error(&self) -> bool { + matches!(self.tonic_code(), Some(Code::Unavailable)) + } + + pub fn should_retry(&self) -> bool { + self.is_connection_error() + || matches!( + self.tonic_code(), + Some(Code::Cancelled) | Some(Code::DeadlineExceeded) + ) } } diff --git a/src/client/src/lib.rs b/src/client/src/lib.rs index bf383acff9..147dffc145 100644 --- a/src/client/src/lib.rs +++ b/src/client/src/lib.rs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![feature(assert_matches)] - mod client; pub mod client_manager; pub mod database; @@ -34,7 +32,7 @@ pub use common_recordbatch::{RecordBatches, SendableRecordBatchStream}; use snafu::OptionExt; pub use self::client::Client; -pub use self::database::Database; +pub use self::database::{Database, OutputMetrics, OutputWithMetrics}; pub use self::error::{Error, Result}; use crate::error::{IllegalDatabaseResponseSnafu, ServerSnafu}; diff --git a/src/cmd/Cargo.toml b/src/cmd/Cargo.toml index 74309e2024..2a5a599a91 100644 --- a/src/cmd/Cargo.toml +++ b/src/cmd/Cargo.toml @@ -30,10 +30,6 @@ base64.workspace = true cache.workspace = true catalog.workspace = true chrono.workspace = true -datafusion-physical-plan.workspace = true -datafusion.workspace = true -datafusion-common.workspace = true -either = "1.15" clap.workspace = true cli.workspace = true client.workspace = true @@ -51,14 +47,19 @@ common-procedure.workspace = true common-query.workspace = true common-recordbatch.workspace = true common-runtime.workspace = true +common-stat.workspace = true common-telemetry = { workspace = true, features = [ "deadlock_detection", ] } common-time.workspace = true common-version.workspace = true common-wal.workspace = true +datafusion.workspace = true +datafusion-common.workspace = true +datafusion-physical-plan.workspace = true datanode.workspace = true datatypes.workspace = true +either = "1.15" etcd-client.workspace = true flow.workspace = true frontend = { workspace = true, default-features = false } @@ -71,7 +72,7 @@ meta-client.workspace = true meta-srv.workspace = true metric-engine.workspace = true mito2.workspace = true -moka.workspace = true +moka = { workspace = true, features = ["future"] } object-store.workspace = true parquet = { workspace = true, features = ["object_store"] } plugins.workspace = true @@ -81,21 +82,19 @@ query.workspace = true rand.workspace = true regex.workspace = true reqwest.workspace = true -standalone.workspace = true serde.workspace = true serde_json.workspace = true servers.workspace = true session.workspace = true -similar-asserts.workspace = true snafu.workspace = true -common-stat.workspace = true +sqlparser.workspace = true +standalone.workspace = true store-api.workspace = true table.workspace = true tokio.workspace = true toml.workspace = true tonic.workspace = true tracing-appender.workspace = true -sqlparser.workspace = true [target.'cfg(unix)'.dependencies] pprof = { version = "0.14", features = [ @@ -110,14 +109,9 @@ api.workspace = true client = { workspace = true, features = ["testing"] } common-test-util.workspace = true common-version.workspace = true -serde.workspace = true -temp-env = "0.3" -tempfile.workspace = true file-engine.workspace = true mito2.workspace = true - -[target.'cfg(not(windows))'.dev-dependencies] -rexpect = "0.5" - -[package.metadata.cargo-udeps.ignore] -development = ["rexpect"] +serde.workspace = true +similar-asserts.workspace = true +temp-env = "0.3" +tempfile.workspace = true diff --git a/src/cmd/src/bin/greptime.rs b/src/cmd/src/bin/greptime.rs index a34d8e0f38..8c791bddcf 100644 --- a/src/cmd/src/bin/greptime.rs +++ b/src/cmd/src/bin/greptime.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![recursion_limit = "256"] #![doc = include_str!("../../../../README.md")] use clap::{Parser, Subcommand}; @@ -20,11 +21,11 @@ use cmd::error::{InitTlsProviderSnafu, Result}; use cmd::options::GlobalOptions; use cmd::{App, cli, datanode, flownode, frontend, metasrv, standalone}; use common_base::Plugins; -use common_version::{verbose_version, version}; -use servers::install_ring_crypto_provider; +use common_version::{product_name, verbose_version, version}; +use servers::install_default_crypto_provider; #[derive(Parser)] -#[command(name = "greptime", author, version, long_version = verbose_version(), about)] +#[command(name = product_name(), author, version, long_version = verbose_version(), about)] #[command(propagate_version = true)] pub(crate) struct Command { #[clap(subcommand)] @@ -52,11 +53,11 @@ enum SubCommand { #[clap(name = "metasrv")] Metasrv(metasrv::Command), - /// Run greptimedb as a standalone service. + /// Start service in standalone mode. #[clap(name = "standalone")] Standalone(standalone::Command), - /// Execute the cli tools for greptimedb. + /// Execute the cli tools. #[clap(name = "cli")] Cli(cli::Command), } @@ -97,7 +98,7 @@ async fn main() -> Result<()> { async fn main_body() -> Result<()> { setup_human_panic(); - install_ring_crypto_provider().map_err(|msg| InitTlsProviderSnafu { msg }.build())?; + install_default_crypto_provider().map_err(|msg| InitTlsProviderSnafu { msg }.build())?; start(Command::parse()).await } @@ -148,7 +149,7 @@ async fn start(cli: Command) -> Result<()> { fn setup_human_panic() { human_panic::setup_panic!( - human_panic::Metadata::new("GreptimeDB", version()) + human_panic::Metadata::new(product_name(), version()) .homepage("https://github.com/GreptimeTeam/greptimedb/discussions") ); diff --git a/src/cmd/src/cli.rs b/src/cmd/src/cli.rs index 84e797c291..09ec9646d8 100644 --- a/src/cmd/src/cli.rs +++ b/src/cmd/src/cli.rs @@ -102,31 +102,79 @@ impl Command { #[cfg(test)] mod tests { + use std::net::TcpListener; + use std::ops::RangeInclusive; + use clap::Parser; use client::{Client, Database}; use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME}; use common_telemetry::logging::LoggingOptions; + use rand::Rng; use crate::error::Result as CmdResult; use crate::options::GlobalOptions; use crate::{App, cli, standalone}; + fn random_standalone_addrs() -> (String, String, String, String) { + let offset = choose_random_unused_port_offset(14000..=24000, 10); + + ( + format!("127.0.0.1:{}", 4000 + offset), + format!("127.0.0.1:{}", 4001 + offset), + format!("127.0.0.1:{}", 4002 + offset), + format!("127.0.0.1:{}", 4003 + offset), + ) + } + + fn choose_random_unused_port_offset( + port_range: RangeInclusive, + max_attempts: usize, + ) -> u16 { + let mut rng = rand::rng(); + + for _ in 0..max_attempts { + let http_port = rng.random_range(port_range.clone()); + let offset = http_port - 4000; + let ports = [4000 + offset, 4001 + offset, 4002 + offset, 4003 + offset]; + + let listeners = ports + .into_iter() + .map(|port| TcpListener::bind(("127.0.0.1", port))) + .collect::, _>>(); + + if listeners.is_ok() { + return offset; + } + } + + panic!("failed to find unused standalone test ports"); + } + #[tokio::test(flavor = "multi_thread")] async fn test_export_create_table_with_quoted_names() -> CmdResult<()> { let output_dir = tempfile::tempdir().unwrap(); + let (http_addr, rpc_addr, mysql_addr, postgres_addr) = random_standalone_addrs(); let standalone = standalone::Command::parse_from([ "standalone", "start", "--data-home", &*output_dir.path().to_string_lossy(), + "--http-addr", + &http_addr, + "--grpc-bind-addr", + &rpc_addr, + "--mysql-addr", + &mysql_addr, + "--postgres-addr", + &postgres_addr, ]); let standalone_opts = standalone.load_options(&GlobalOptions::default()).unwrap(); let mut instance = standalone.build(standalone_opts).await?; instance.start().await?; - let client = Client::with_urls(["127.0.0.1:4001"]); + let client = Client::with_urls([rpc_addr.as_str()]); let database = Database::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, client); database .sql(r#"CREATE DATABASE "cli.export.create_table";"#) @@ -149,7 +197,7 @@ mod tests { "data", "export", "--addr", - "127.0.0.1:4000", + &http_addr, "--output-dir", &*output_dir.path().to_string_lossy(), "--target", diff --git a/src/cmd/src/datanode.rs b/src/cmd/src/datanode.rs index 06e2568b72..3c106bd43f 100644 --- a/src/cmd/src/datanode.rs +++ b/src/cmd/src/datanode.rs @@ -197,13 +197,17 @@ pub struct StartCommand { #[clap(long)] node_id: Option, /// The address to bind the gRPC server. - #[clap(long, alias = "rpc-addr")] - rpc_bind_addr: Option, + #[clap(long = "grpc-bind-addr", alias = "rpc-bind-addr", alias = "rpc-addr")] + grpc_bind_addr: Option, /// The address advertised to the metasrv, and used for connections from outside the host. /// If left empty or unset, the server will automatically use the IP address of the first network interface - /// on the host, with the same port number as the one specified in `rpc_bind_addr`. - #[clap(long, alias = "rpc-hostname")] - rpc_server_addr: Option, + /// on the host, with the same port number as the one specified in `grpc_bind_addr`. + #[clap( + long = "grpc-server-addr", + alias = "rpc-server-addr", + alias = "rpc-hostname" + )] + grpc_server_addr: Option, #[clap(long, value_delimiter = ',', num_args = 1..)] metasrv_addrs: Option>, #[clap(short, long)] @@ -256,20 +260,20 @@ impl StartCommand { tokio_console_addr: global_options.tokio_console_addr.clone(), }; - if let Some(addr) = &self.rpc_bind_addr { + if let Some(addr) = &self.grpc_bind_addr { opts.grpc.bind_addr.clone_from(addr); } else if let Some(addr) = &opts.rpc_addr { warn!( - "Use the deprecated attribute `DatanodeOptions.rpc_addr`, please use `grpc.addr` instead." + "Use the deprecated attribute `DatanodeOptions.rpc_addr`, please use `grpc.bind_addr` instead." ); opts.grpc.bind_addr.clone_from(addr); } - if let Some(server_addr) = &self.rpc_server_addr { + if let Some(server_addr) = &self.grpc_server_addr { opts.grpc.server_addr.clone_from(server_addr); } else if let Some(server_addr) = &opts.rpc_hostname { warn!( - "Use the deprecated attribute `DatanodeOptions.rpc_hostname`, please use `grpc.hostname` instead." + "Use the deprecated attribute `DatanodeOptions.rpc_hostname`, please use `grpc.server_addr` instead." ); opts.grpc.server_addr.clone_from(server_addr); } @@ -356,10 +360,11 @@ impl StartCommand { #[cfg(test)] mod tests { - use std::assert_matches::assert_matches; + use std::assert_matches; use std::io::Write; use std::time::Duration; + use clap::{CommandFactory, Parser}; use common_config::ENV_VAR_SEP; use common_test_util::temp_dir::create_named_temp_file; use object_store::config::{FileConfig, GcsConfig, ObjectStoreConfig, S3Config}; @@ -402,8 +407,8 @@ mod tests { node_id = 42 [grpc] - addr = "127.0.0.1:3001" - hostname = "127.0.0.1" + bind_addr = "127.0.0.1:3001" + server_addr = "127.0.0.1" runtime_size = 8 [meta_client] @@ -449,6 +454,7 @@ mod tests { let options = cmd.load_options(&Default::default()).unwrap().component; assert_eq!("127.0.0.1:3001".to_string(), options.grpc.bind_addr); + assert_eq!("127.0.0.1".to_string(), options.grpc.server_addr); assert_eq!(Some(42), options.node_id); let DatanodeWalConfig::RaftEngine(raft_engine_config) = options.wal else { @@ -661,4 +667,55 @@ mod tests { }, ); } + + #[test] + fn test_parse_grpc_cli_aliases() { + let command = StartCommand::try_parse_from([ + "datanode", + "--grpc-bind-addr", + "127.0.0.1:13001", + "--grpc-server-addr", + "10.0.0.1:13001", + ]) + .unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:13001")); + assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.1:13001")); + + let command = StartCommand::try_parse_from([ + "datanode", + "--rpc-bind-addr", + "127.0.0.1:23001", + "--rpc-server-addr", + "10.0.0.2:23001", + ]) + .unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:23001")); + assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.2:23001")); + + let command = StartCommand::try_parse_from([ + "datanode", + "--rpc-addr", + "127.0.0.1:33001", + "--rpc-hostname", + "10.0.0.3:33001", + ]) + .unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:33001")); + assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.3:33001")); + } + + #[test] + fn test_help_uses_grpc_option_names() { + let mut cmd = StartCommand::command(); + let mut help = Vec::new(); + cmd.write_long_help(&mut help).unwrap(); + let help = String::from_utf8(help).unwrap(); + + assert!(help.contains("--grpc-bind-addr")); + assert!(help.contains("--grpc-server-addr")); + assert!(!help.contains("--rpc-bind-addr")); + assert!(!help.contains("--rpc-server-addr")); + assert!(!help.contains("--rpc-addr")); + assert!(!help.contains("--rpc-hostname")); + } } diff --git a/src/cmd/src/datanode/builder.rs b/src/cmd/src/datanode/builder.rs index cfde0c349a..90f38b22bd 100644 --- a/src/cmd/src/datanode/builder.rs +++ b/src/cmd/src/datanode/builder.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use cache::build_datanode_cache_registry; -use catalog::kvbackend::MetaKvBackend; +use catalog::kvbackend::new_read_only_meta_kv_backend; use common_base::Plugins; use common_meta::cache::LayeredCacheRegistryBuilder; use common_telemetry::info; @@ -99,9 +99,7 @@ impl InstanceBuilder { .await .context(MetaClientInitSnafu)?; - let backend = Arc::new(MetaKvBackend { - client: client.clone(), - }); + let backend = new_read_only_meta_kv_backend(client.clone()); let mut builder = DatanodeBuilder::new(dn_opts.clone(), plugins.clone(), backend.clone()); let registry = Arc::new( diff --git a/src/cmd/src/datanode/objbench.rs b/src/cmd/src/datanode/objbench.rs index d8f53b9d71..a298430c83 100644 --- a/src/cmd/src/datanode/objbench.rs +++ b/src/cmd/src/datanode/objbench.rs @@ -20,13 +20,14 @@ use clap::Parser; use colored::Colorize; use datanode::config::RegionEngineConfig; use datanode::store; -use either::Either; +use futures::stream; use mito2::access_layer::{ AccessLayer, AccessLayerRef, Metrics, OperationType, SstWriteRequest, WriteType, }; use mito2::cache::{CacheManager, CacheManagerRef}; use mito2::config::{FulltextIndexConfig, MitoConfig, Mode}; -use mito2::read::Source; +use mito2::read::FlatSource; +use mito2::sst::FormatType; use mito2::sst::file::{FileHandle, FileMeta}; use mito2::sst::file_purger::{FilePurger, FilePurgerRef}; use mito2::sst::index::intermediate::IntermediateManager; @@ -189,6 +190,7 @@ impl ObjbenchCommand { sequence: None, partition_expr: None, num_series: 0, + ..Default::default() }; let src_handle = FileHandle::new(file_meta, new_noop_file_purger()); @@ -231,6 +233,10 @@ impl ObjbenchCommand { let reader_build_elapsed = reader_build_start.elapsed(); let total_rows = reader.parquet_metadata().file_metadata().num_rows(); println!("{} Reader built in {:?}", "✓".green(), reader_build_elapsed); + let reader_stream = Box::pin(stream::try_unfold(reader, |mut reader| async move { + let batch = reader.next_record_batch().await?; + Ok(batch.map(|batch| (batch, reader))) + })); // Build write request let fulltext_index_config = FulltextIndexConfig { @@ -238,13 +244,16 @@ impl ObjbenchCommand { ..Default::default() }; + let source = + FlatSource::new_stream(region_meta.schema.arrow_schema().clone(), reader_stream); let write_req = SstWriteRequest { op_type: OperationType::Flush, metadata: region_meta, - source: Either::Left(Source::Reader(Box::new(reader))), + source, cache_manager, storage: None, max_sequence: None, + sst_write_format: FormatType::PrimaryKey, index_options: Default::default(), index_config: mito_engine_config.index.clone(), inverted_index_config: MitoConfig::default().inverted_index, diff --git a/src/cmd/src/datanode/scanbench.rs b/src/cmd/src/datanode/scanbench.rs index fdda1d97bb..b26705991c 100644 --- a/src/cmd/src/datanode/scanbench.rs +++ b/src/cmd/src/datanode/scanbench.rs @@ -53,7 +53,9 @@ use store_api::metadata::RegionMetadata; use store_api::path_utils::WAL_DIR; use store_api::region_engine::{PrepareRequest, QueryScanContext, RegionEngine}; use store_api::region_request::{PathType, RegionOpenRequest, RegionRequest}; -use store_api::storage::{RegionId, ScanRequest, TimeSeriesDistribution, TimeSeriesRowSelector}; +use store_api::storage::{ + ProjectionInput, RegionId, ScanRequest, TimeSeriesDistribution, TimeSeriesRowSelector, +}; use tokio::fs; use crate::datanode::objbench::{build_object_store, parse_config}; @@ -102,10 +104,6 @@ pub struct ScanbenchCommand { #[clap(long, value_name = "FILE")] pprof_file: Option, - /// Force reading the region in flat format. - #[clap(long, default_value_t = false)] - force_flat_format: bool, - /// Enable WAL replay when opening the region. #[clap(long, default_value_t = false)] enable_wal: bool, @@ -580,12 +578,11 @@ impl ScanbenchCommand { }; println!( - "{} Scanner: {}, Parallelism: {}, Iterations: {}, Force flat format: {}", + "{} Scanner: {}, Parallelism: {}, Iterations: {}", "ℹ".blue(), self.scanner, self.parallelism, self.iterations, - self.force_flat_format, ); // Start profiling if pprof_file is specified (unless pprof_after_warmup is set) @@ -620,13 +617,13 @@ impl ScanbenchCommand { let mut total_rows_all = 0u64; let mut total_elapsed_all = std::time::Duration::ZERO; + let projection_input = projection.map(ProjectionInput::new); for iteration in 0..self.iterations { let request = ScanRequest { - projection: projection.clone(), + projection_input: projection_input.clone(), filters: filters.clone(), series_row_selector, distribution, - force_flat_format: self.force_flat_format, ..Default::default() }; @@ -662,7 +659,7 @@ impl ScanbenchCommand { // Sort ranges within each partition by start time ascending for partition in &mut partitions { - partition.sort_by(|a, b| a.start.cmp(&b.start)); + partition.sort_by_key(|a| a.start); } scanner @@ -677,7 +674,9 @@ impl ScanbenchCommand { // Scan all partitions let num_partitions = scanner.properties().partitions.len(); - let ctx = QueryScanContext::default(); + let ctx = QueryScanContext { + explain_verbose: self.verbose, + }; let metrics_set = ExecutionPlanMetricsSet::new(); let mut scan_futures = FuturesUnordered::new(); diff --git a/src/cmd/src/flownode.rs b/src/cmd/src/flownode.rs index 3f8458cddf..df7a0725e9 100644 --- a/src/cmd/src/flownode.rs +++ b/src/cmd/src/flownode.rs @@ -19,7 +19,9 @@ use std::time::Duration; use cache::{build_fundamental_cache_registry, with_default_composite_cache_registry}; use catalog::information_extension::DistributedInformationExtension; -use catalog::kvbackend::{CachedKvBackendBuilder, KvBackendCatalogManagerBuilder, MetaKvBackend}; +use catalog::kvbackend::{ + CachedKvBackendBuilder, KvBackendCatalogManagerBuilder, new_read_only_meta_kv_backend, +}; use clap::Parser; use client::client_manager::NodeClients; use common_base::Plugins; @@ -46,8 +48,8 @@ use snafu::{OptionExt, ResultExt, ensure}; use tracing_appender::non_blocking::WorkerGuard; use crate::error::{ - BuildCacheRegistrySnafu, InitMetadataSnafu, LoadLayeredConfigSnafu, MetaClientInitSnafu, - MissingConfigSnafu, OtherSnafu, Result, ShutdownFlownodeSnafu, StartFlownodeSnafu, + BuildCacheRegistrySnafu, LoadLayeredConfigSnafu, MetaClientInitSnafu, MissingConfigSnafu, + OtherSnafu, Result, ShutdownFlownodeSnafu, StartFlownodeSnafu, }; use crate::options::{GlobalOptions, GreptimeOptions}; use crate::{App, create_resource_limit_metrics, log_versions, maybe_activate_heap_profile}; @@ -139,13 +141,17 @@ struct StartCommand { #[clap(long)] node_id: Option, /// Bind address for the gRPC server. - #[clap(long, alias = "rpc-addr")] - rpc_bind_addr: Option, + #[clap(long = "grpc-bind-addr", alias = "rpc-bind-addr", alias = "rpc-addr")] + grpc_bind_addr: Option, /// The address advertised to the metasrv, and used for connections from outside the host. /// If left empty or unset, the server will automatically use the IP address of the first network interface - /// on the host, with the same port number as the one specified in `rpc_bind_addr`. - #[clap(long, alias = "rpc-hostname")] - rpc_server_addr: Option, + /// on the host, with the same port number as the one specified in `grpc_bind_addr`. + #[clap( + long = "grpc-server-addr", + alias = "rpc-server-addr", + alias = "rpc-hostname" + )] + grpc_server_addr: Option, /// Metasrv address list; #[clap(long, value_delimiter = ',', num_args = 1..)] metasrv_addrs: Option>, @@ -207,11 +213,11 @@ impl StartCommand { tokio_console_addr: global_options.tokio_console_addr.clone(), }; - if let Some(addr) = &self.rpc_bind_addr { + if let Some(addr) = &self.grpc_bind_addr { opts.grpc.bind_addr.clone_from(addr); } - if let Some(server_addr) = &self.rpc_server_addr { + if let Some(server_addr) = &self.grpc_server_addr { opts.grpc.server_addr.clone_from(server_addr); } @@ -296,13 +302,14 @@ impl StartCommand { let cache_ttl = meta_config.metadata_cache_ttl; let cache_tti = meta_config.metadata_cache_tti; + let readonly_meta_backend = new_read_only_meta_kv_backend(meta_client.clone()); + // TODO(discord9): add helper function to ease the creation of cache registry&such - let cached_meta_backend = - CachedKvBackendBuilder::new(Arc::new(MetaKvBackend::new(meta_client.clone()))) - .cache_max_capacity(cache_max_capacity) - .cache_ttl(cache_ttl) - .cache_tti(cache_tti) - .build(); + let cached_meta_backend = CachedKvBackendBuilder::new(readonly_meta_backend.clone()) + .cache_max_capacity(cache_max_capacity) + .cache_ttl(cache_ttl) + .cache_tti(cache_tti) + .build(); let cached_meta_backend = Arc::new(cached_meta_backend); // Builds cache registry @@ -312,7 +319,7 @@ impl StartCommand { .build(), ); let fundamental_cache_registry = - build_fundamental_cache_registry(Arc::new(MetaKvBackend::new(meta_client.clone()))); + build_fundamental_cache_registry(readonly_meta_backend.clone()); let layered_cache_registry = Arc::new( with_default_composite_cache_registry( layered_cache_builder.add_cache_registry(fundamental_cache_registry), @@ -342,10 +349,6 @@ impl StartCommand { let table_metadata_manager = Arc::new(TableMetadataManager::new(cached_meta_backend.clone())); - table_metadata_manager - .init() - .await - .context(InitMetadataSnafu)?; let executor = HandlerGroupExecutor::new(vec![ Arc::new(ParseMailboxMessageHandler), @@ -432,3 +435,61 @@ impl StartCommand { Ok(Instance::new(flownode, guard)) } } + +#[cfg(test)] +mod tests { + use clap::{CommandFactory, Parser}; + + use super::*; + + #[test] + fn test_parse_grpc_cli_aliases() { + let command = StartCommand::try_parse_from([ + "flownode", + "--grpc-bind-addr", + "127.0.0.1:14004", + "--grpc-server-addr", + "10.0.0.1:14004", + ]) + .unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:14004")); + assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.1:14004")); + + let command = StartCommand::try_parse_from([ + "flownode", + "--rpc-bind-addr", + "127.0.0.1:24004", + "--rpc-server-addr", + "10.0.0.2:24004", + ]) + .unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:24004")); + assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.2:24004")); + + let command = StartCommand::try_parse_from([ + "flownode", + "--rpc-addr", + "127.0.0.1:34004", + "--rpc-hostname", + "10.0.0.3:34004", + ]) + .unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:34004")); + assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.3:34004")); + } + + #[test] + fn test_help_uses_grpc_option_names() { + let mut cmd = StartCommand::command(); + let mut help = Vec::new(); + cmd.write_long_help(&mut help).unwrap(); + let help = String::from_utf8(help).unwrap(); + + assert!(help.contains("--grpc-bind-addr")); + assert!(help.contains("--grpc-server-addr")); + assert!(!help.contains("--rpc-bind-addr")); + assert!(!help.contains("--rpc-server-addr")); + assert!(!help.contains("--rpc-addr")); + assert!(!help.contains("--rpc-hostname")); + } +} diff --git a/src/cmd/src/frontend.rs b/src/cmd/src/frontend.rs index cb802791c5..5e19e90f89 100644 --- a/src/cmd/src/frontend.rs +++ b/src/cmd/src/frontend.rs @@ -23,7 +23,7 @@ use catalog::information_extension::DistributedInformationExtension; use catalog::information_schema::InformationExtensionRef; use catalog::kvbackend::{ CachedKvBackendBuilder, CatalogManagerConfiguratorRef, KvBackendCatalogManagerBuilder, - MetaKvBackend, + new_read_only_meta_kv_backend, }; use catalog::process_manager::ProcessManager; use clap::Parser; @@ -152,21 +152,33 @@ impl SubCommand { #[derive(Debug, Default, Parser)] pub struct StartCommand { /// The address to bind the gRPC server. - #[clap(long, alias = "rpc-addr")] - rpc_bind_addr: Option, + #[clap(long = "grpc-bind-addr", alias = "rpc-bind-addr", alias = "rpc-addr")] + grpc_bind_addr: Option, /// The address advertised to the metasrv, and used for connections from outside the host. /// If left empty or unset, the server will automatically use the IP address of the first network interface - /// on the host, with the same port number as the one specified in `rpc_bind_addr`. - #[clap(long, alias = "rpc-hostname")] - rpc_server_addr: Option, + /// on the host, with the same port number as the one specified in `grpc_bind_addr`. + #[clap( + long = "grpc-server-addr", + alias = "rpc-server-addr", + alias = "rpc-hostname" + )] + grpc_server_addr: Option, /// The address to bind the internal gRPC server. - #[clap(long, alias = "internal-rpc-addr")] - internal_rpc_bind_addr: Option, + #[clap( + long = "internal-grpc-bind-addr", + alias = "internal-rpc-bind-addr", + alias = "internal-rpc-addr" + )] + internal_grpc_bind_addr: Option, /// The address advertised to the metasrv, and used for connections from outside the host. /// If left empty or unset, the server will automatically use the IP address of the first network interface - /// on the host, with the same port number as the one specified in `internal_rpc_bind_addr`. - #[clap(long, alias = "internal-rpc-hostname")] - internal_rpc_server_addr: Option, + /// on the host, with the same port number as the one specified in `internal_grpc_bind_addr`. + #[clap( + long = "internal-grpc-server-addr", + alias = "internal-rpc-server-addr", + alias = "internal-rpc-hostname" + )] + internal_grpc_server_addr: Option, #[clap(long)] http_addr: Option, #[clap(long)] @@ -258,16 +270,16 @@ impl StartCommand { opts.http.disable_dashboard = disable_dashboard; } - if let Some(addr) = &self.rpc_bind_addr { + if let Some(addr) = &self.grpc_bind_addr { opts.grpc.bind_addr.clone_from(addr); opts.grpc.tls = merge_tls_option(&opts.grpc.tls, tls_opts.clone()); } - if let Some(addr) = &self.rpc_server_addr { + if let Some(addr) = &self.grpc_server_addr { opts.grpc.server_addr.clone_from(addr); } - if let Some(addr) = &self.internal_rpc_bind_addr { + if let Some(addr) = &self.internal_grpc_bind_addr { if let Some(internal_grpc) = &mut opts.internal_grpc { internal_grpc.bind_addr = addr.clone(); } else { @@ -280,7 +292,7 @@ impl StartCommand { } } - if let Some(addr) = &self.internal_rpc_server_addr { + if let Some(addr) = &self.internal_grpc_server_addr { if let Some(internal_grpc) = &mut opts.internal_grpc { internal_grpc.server_addr = addr.clone(); } else { @@ -381,13 +393,14 @@ impl StartCommand { .await .context(error::StartFrontendSnafu)?; + let readonly_meta_backend = new_read_only_meta_kv_backend(meta_client.clone()); + // TODO(discord9): add helper function to ease the creation of cache registry&such - let cached_meta_backend = - CachedKvBackendBuilder::new(Arc::new(MetaKvBackend::new(meta_client.clone()))) - .cache_max_capacity(cache_max_capacity) - .cache_ttl(cache_ttl) - .cache_tti(cache_tti) - .build(); + let cached_meta_backend = CachedKvBackendBuilder::new(readonly_meta_backend.clone()) + .cache_max_capacity(cache_max_capacity) + .cache_ttl(cache_ttl) + .cache_tti(cache_tti) + .build(); let cached_meta_backend = Arc::new(cached_meta_backend); // Builds cache registry @@ -397,7 +410,7 @@ impl StartCommand { .build(), ); let fundamental_cache_registry = - build_fundamental_cache_registry(Arc::new(MetaKvBackend::new(meta_client.clone()))); + build_fundamental_cache_registry(readonly_meta_backend.clone()); let layered_cache_registry = Arc::new( with_default_composite_cache_registry( layered_cache_builder.add_cache_registry(fundamental_cache_registry), @@ -515,6 +528,7 @@ mod tests { use std::time::Duration; use auth::{Identity, Password, UserProviderRef}; + use clap::{CommandFactory, Parser}; use common_base::readable_size::ReadableSize; use common_config::ENV_VAR_SEP; use common_test_util::temp_dir::create_named_temp_file; @@ -530,8 +544,8 @@ mod tests { http_addr: Some("127.0.0.1:1234".to_string()), mysql_addr: Some("127.0.0.1:5678".to_string()), postgres_addr: Some("127.0.0.1:5432".to_string()), - internal_rpc_bind_addr: Some("127.0.0.1:4010".to_string()), - internal_rpc_server_addr: Some("10.0.0.24:4010".to_string()), + internal_grpc_bind_addr: Some("127.0.0.1:4010".to_string()), + internal_grpc_server_addr: Some("10.0.0.24:4010".to_string()), influxdb_enable: Some(false), disable_dashboard: Some(false), ..Default::default() @@ -609,6 +623,7 @@ mod tests { disable_dashboard: false, ..Default::default() }, + meta_client: Some(MetaClientOptions::default()), user_provider: Some("static_user_provider:cmd:test=test".to_string()), ..Default::default() }; @@ -744,4 +759,97 @@ mod tests { }, ); } + + #[test] + fn test_parse_grpc_cli_aliases() { + let command = StartCommand::try_parse_from([ + "frontend", + "--grpc-bind-addr", + "127.0.0.1:14001", + "--grpc-server-addr", + "10.0.0.1:14001", + "--internal-grpc-bind-addr", + "127.0.0.1:14010", + "--internal-grpc-server-addr", + "10.0.0.1:14010", + ]) + .unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:14001")); + assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.1:14001")); + assert_eq!( + command.internal_grpc_bind_addr.as_deref(), + Some("127.0.0.1:14010") + ); + assert_eq!( + command.internal_grpc_server_addr.as_deref(), + Some("10.0.0.1:14010") + ); + + let command = StartCommand::try_parse_from([ + "frontend", + "--rpc-bind-addr", + "127.0.0.1:24001", + "--rpc-server-addr", + "10.0.0.2:24001", + "--internal-rpc-bind-addr", + "127.0.0.1:24010", + "--internal-rpc-server-addr", + "10.0.0.2:24010", + ]) + .unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:24001")); + assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.2:24001")); + assert_eq!( + command.internal_grpc_bind_addr.as_deref(), + Some("127.0.0.1:24010") + ); + assert_eq!( + command.internal_grpc_server_addr.as_deref(), + Some("10.0.0.2:24010") + ); + + let command = StartCommand::try_parse_from([ + "frontend", + "--rpc-addr", + "127.0.0.1:34001", + "--rpc-hostname", + "10.0.0.3:34001", + "--internal-rpc-addr", + "127.0.0.1:34010", + "--internal-rpc-hostname", + "10.0.0.3:34010", + ]) + .unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:34001")); + assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.3:34001")); + assert_eq!( + command.internal_grpc_bind_addr.as_deref(), + Some("127.0.0.1:34010") + ); + assert_eq!( + command.internal_grpc_server_addr.as_deref(), + Some("10.0.0.3:34010") + ); + } + + #[test] + fn test_help_uses_grpc_option_names() { + let mut cmd = StartCommand::command(); + let mut help = Vec::new(); + cmd.write_long_help(&mut help).unwrap(); + let help = String::from_utf8(help).unwrap(); + + assert!(help.contains("--grpc-bind-addr")); + assert!(help.contains("--grpc-server-addr")); + assert!(help.contains("--internal-grpc-bind-addr")); + assert!(help.contains("--internal-grpc-server-addr")); + assert!(!help.contains("--rpc-bind-addr")); + assert!(!help.contains("--rpc-server-addr")); + assert!(!help.contains("--rpc-addr")); + assert!(!help.contains("--rpc-hostname")); + assert!(!help.contains("--internal-rpc-bind-addr")); + assert!(!help.contains("--internal-rpc-server-addr")); + assert!(!help.contains("--internal-rpc-addr")); + assert!(!help.contains("--internal-rpc-hostname")); + } } diff --git a/src/cmd/src/lib.rs b/src/cmd/src/lib.rs index 46ca4c8a76..27564597d7 100644 --- a/src/cmd/src/lib.rs +++ b/src/cmd/src/lib.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![feature(assert_matches)] +#![recursion_limit = "256"] use async_trait::async_trait; use common_error::ext::ErrorExt; diff --git a/src/cmd/src/metasrv.rs b/src/cmd/src/metasrv.rs index 2ce5fb3a02..bf3cb2f5e7 100644 --- a/src/cmd/src/metasrv.rs +++ b/src/cmd/src/metasrv.rs @@ -21,8 +21,8 @@ use clap::Parser; use common_base::Plugins; use common_config::Configurable; use common_meta::distributed_time_constants::init_distributed_time_constants; -use common_telemetry::info; use common_telemetry::logging::{DEFAULT_LOGGING_DIR, TracingOptions}; +use common_telemetry::{info, warn}; use common_version::{short_version, verbose_version}; use meta_srv::bootstrap::{MetasrvInstance, metasrv_builder}; use meta_srv::metasrv::BackendImpl; @@ -141,13 +141,17 @@ impl SubCommand { #[derive(Default, Parser)] pub struct StartCommand { /// The address to bind the gRPC server. - #[clap(long, alias = "bind-addr")] - rpc_bind_addr: Option, + #[clap(long = "grpc-bind-addr", alias = "rpc-bind-addr", alias = "bind-addr")] + grpc_bind_addr: Option, /// The communication server address for the frontend and datanode to connect to metasrv. /// If left empty or unset, the server will automatically use the IP address of the first network interface - /// on the host, with the same port number as the one specified in `rpc_bind_addr`. - #[clap(long, alias = "server-addr")] - rpc_server_addr: Option, + /// on the host, with the same port number as the one specified in `grpc_bind_addr`. + #[clap( + long = "grpc-server-addr", + alias = "rpc-server-addr", + alias = "server-addr" + )] + grpc_server_addr: Option, #[clap(long, alias = "store-addr", value_delimiter = ',', num_args = 1..)] store_addrs: Option>, #[clap(short, long)] @@ -179,8 +183,8 @@ pub struct StartCommand { impl Debug for StartCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("StartCommand") - .field("rpc_bind_addr", &self.rpc_bind_addr) - .field("rpc_server_addr", &self.rpc_server_addr) + .field("grpc_bind_addr", &self.grpc_bind_addr) + .field("grpc_server_addr", &self.grpc_server_addr) .field("store_addrs", &self.sanitize_store_addrs()) .field("config_file", &self.config_file) .field("selector", &self.selector) @@ -240,18 +244,24 @@ impl StartCommand { }; #[allow(deprecated)] - if let Some(addr) = &self.rpc_bind_addr { + if let Some(addr) = &self.grpc_bind_addr { opts.bind_addr.clone_from(addr); opts.grpc.bind_addr.clone_from(addr); } else if !opts.bind_addr.is_empty() { + warn!( + "Use the deprecated attribute `MetasrvOptions.bind_addr`, please use `grpc.bind_addr` instead." + ); opts.grpc.bind_addr.clone_from(&opts.bind_addr); } #[allow(deprecated)] - if let Some(addr) = &self.rpc_server_addr { + if let Some(addr) = &self.grpc_server_addr { opts.server_addr.clone_from(addr); opts.grpc.server_addr.clone_from(addr); } else if !opts.server_addr.is_empty() { + warn!( + "Use the deprecated attribute `MetasrvOptions.server_addr`, please use `grpc.server_addr` instead." + ); opts.grpc.server_addr.clone_from(&opts.server_addr); } @@ -353,6 +363,7 @@ impl StartCommand { mod tests { use std::io::Write; + use clap::{CommandFactory, Parser}; use common_base::readable_size::ReadableSize; use common_config::ENV_VAR_SEP; use common_test_util::temp_dir::create_named_temp_file; @@ -363,8 +374,8 @@ mod tests { #[test] fn test_read_from_cmd() { let cmd = StartCommand { - rpc_bind_addr: Some("127.0.0.1:3002".to_string()), - rpc_server_addr: Some("127.0.0.1:3002".to_string()), + grpc_bind_addr: Some("127.0.0.1:3002".to_string()), + grpc_server_addr: Some("127.0.0.1:3002".to_string()), store_addrs: Some(vec!["127.0.0.1:2380".to_string()]), selector: Some("LoadBased".to_string()), ..Default::default() @@ -432,8 +443,8 @@ mod tests { #[test] fn test_load_log_options_from_cli() { let cmd = StartCommand { - rpc_bind_addr: Some("127.0.0.1:3002".to_string()), - rpc_server_addr: Some("127.0.0.1:3002".to_string()), + grpc_bind_addr: Some("127.0.0.1:3002".to_string()), + grpc_server_addr: Some("127.0.0.1:3002".to_string()), store_addrs: Some(vec!["127.0.0.1:2380".to_string()]), selector: Some("LoadBased".to_string()), ..Default::default() @@ -520,4 +531,55 @@ mod tests { }, ); } + + #[test] + fn test_parse_grpc_cli_aliases() { + let command = StartCommand::try_parse_from([ + "metasrv", + "--grpc-bind-addr", + "127.0.0.1:13002", + "--grpc-server-addr", + "10.0.0.1:13002", + ]) + .unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:13002")); + assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.1:13002")); + + let command = StartCommand::try_parse_from([ + "metasrv", + "--rpc-bind-addr", + "127.0.0.1:23002", + "--rpc-server-addr", + "10.0.0.2:23002", + ]) + .unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:23002")); + assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.2:23002")); + + let command = StartCommand::try_parse_from([ + "metasrv", + "--bind-addr", + "127.0.0.1:33002", + "--server-addr", + "10.0.0.3:33002", + ]) + .unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:33002")); + assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.3:33002")); + } + + #[test] + fn test_help_uses_grpc_option_names() { + let mut cmd = StartCommand::command(); + let mut help = Vec::new(); + cmd.write_long_help(&mut help).unwrap(); + let help = String::from_utf8(help).unwrap(); + + assert!(help.contains("--grpc-bind-addr")); + assert!(help.contains("--grpc-server-addr")); + assert!(!help.contains("--rpc-bind-addr")); + assert!(!help.contains("--rpc-server-addr")); + assert!(!help.contains("--bind-addr")); + assert!(!help.contains("--server-addr")); + } } diff --git a/src/cmd/src/standalone.rs b/src/cmd/src/standalone.rs index 92638d3c4a..6f35e74b65 100644 --- a/src/cmd/src/standalone.rs +++ b/src/cmd/src/standalone.rs @@ -32,15 +32,17 @@ use common_meta::cache::LayeredCacheRegistryBuilder; use common_meta::ddl::flow_meta::FlowMetadataAllocator; use common_meta::ddl::table_meta::TableMetadataAllocator; use common_meta::ddl::{DdlContext, NoopRegionFailureDetectorControl}; -use common_meta::ddl_manager::{DdlManager, DdlManagerConfiguratorRef}; +use common_meta::ddl_manager::{DdlManager, DdlManagerConfiguratorRef, DdlManagerRef}; use common_meta::key::flow::FlowMetadataManager; use common_meta::key::{TableMetadataManager, TableMetadataManagerRef}; use common_meta::kv_backend::KvBackendRef; -use common_meta::procedure_executor::LocalProcedureExecutor; +use common_meta::node_manager::{FlownodeRef, NodeManagerRef}; +use common_meta::procedure_executor::{LocalProcedureExecutor, ProcedureExecutorRef}; use common_meta::region_keeper::MemoryRegionKeeper; use common_meta::region_registry::LeaderRegionRegistry; -use common_meta::sequence::SequenceBuilder; +use common_meta::sequence::{Sequence, SequenceBuilder}; use common_meta::wal_provider::{WalProviderRef, build_wal_provider}; +use common_options::plugin_options::StandaloneFlag; use common_procedure::ProcedureManagerRef; use common_query::prelude::set_default_prefix; use common_telemetry::info; @@ -49,6 +51,7 @@ use common_time::timezone::set_default_timezone; use common_version::{short_version, verbose_version}; use datanode::config::DatanodeOptions; use datanode::datanode::{Datanode, DatanodeBuilder}; +use datanode::region_server::RegionServer; use flow::{ FlownodeBuilder, FlownodeInstance, FlownodeOptions, FrontendClient, FrontendInvoker, GrpcQueryHandlerWithBoxedError, @@ -58,6 +61,7 @@ use frontend::instance::StandaloneDatanodeManager; use frontend::instance::builder::FrontendBuilder; use frontend::server::Services; use meta_srv::metasrv::{FLOW_ID_SEQ, TABLE_ID_SEQ}; +use plugins::PluginOptions; use plugins::frontend::context::{ CatalogManagerConfigureContext, StandaloneCatalogManagerConfigureContext, }; @@ -121,6 +125,7 @@ pub struct Instance { flownode: FlownodeInstance, procedure_manager: ProcedureManagerRef, wal_provider: WalProviderRef, + leader_services_controller: Box, // Keep the logging guard to prevent the worker from being dropped. _guard: Vec, } @@ -130,6 +135,18 @@ impl Instance { pub fn server_addr(&self, name: &str) -> Option { self.frontend.server_handlers().addr(name) } + + /// Get the mutable Frontend component of this Standalone instance for externally modification + /// by others (might not be in this code base, so don't delete this function). + pub fn mut_frontend(&mut self) -> &mut Frontend { + &mut self.frontend + } + + /// Get the Datanode component of this Standalone instance for externally usage + /// by others (might not be in this code base, so don't delete this function). + pub fn datanode(&self) -> &Datanode { + &self.datanode + } } #[async_trait] @@ -141,15 +158,13 @@ impl App for Instance { async fn start(&mut self) -> Result<()> { self.datanode.start_telemetry(); - self.procedure_manager - .start() - .await - .context(error::StartProcedureManagerSnafu)?; - - self.wal_provider - .start() - .await - .context(error::StartWalProviderSnafu)?; + self.leader_services_controller + .start( + self.procedure_manager.clone(), + self.wal_provider.clone(), + self.datanode.region_server(), + ) + .await?; plugins::start_frontend_plugins(self.frontend.instance.plugins().clone()) .await @@ -171,10 +186,12 @@ impl App for Instance { .await .context(error::ShutdownFrontendSnafu)?; - self.procedure_manager - .stop() - .await - .context(error::StopProcedureManagerSnafu)?; + self.leader_services_controller + .stop( + self.procedure_manager.clone(), + self.datanode.region_server(), + ) + .await?; self.datanode .shutdown() @@ -196,8 +213,8 @@ impl App for Instance { pub struct StartCommand { #[clap(long)] http_addr: Option, - #[clap(long, alias = "rpc-addr")] - rpc_bind_addr: Option, + #[clap(long = "grpc-bind-addr", alias = "rpc-bind-addr", alias = "rpc-addr")] + grpc_bind_addr: Option, #[clap(long)] mysql_addr: Option, #[clap(long)] @@ -283,7 +300,7 @@ impl StartCommand { .to_string(); } - if let Some(addr) = &self.rpc_bind_addr { + if let Some(addr) = &self.grpc_bind_addr { // frontend grpc addr conflict with datanode default grpc addr let datanode_grpc_addr = DatanodeOptions::default().grpc.bind_addr; if addr.eq(&datanode_grpc_addr) { @@ -342,9 +359,19 @@ impl StartCommand { info!("Standalone start command: {:#?}", self); info!("Standalone options: {opts:#?}"); + let (mut instance, _) = + Self::build_with(opts.component, opts.plugins, InstanceCreator::default()).await?; + instance._guard.extend(guard); + Ok(instance) + } + + pub async fn build_with( + mut opts: StandaloneOptions, + plugin_opts: Vec, + creator: InstanceCreator, + ) -> Result<(Instance, InstanceCreatorResult)> { let mut plugins = Plugins::new(); - let plugin_opts = opts.plugins; - let mut opts = opts.component; + plugins.insert(StandaloneFlag); set_default_prefix(opts.default_column_prefix.as_deref()) .map_err(BoxedError::new) .context(error::BuildCliSnafu)?; @@ -370,8 +397,10 @@ impl StartCommand { .context(error::CreateDirSnafu { dir: data_home })?; let metadata_dir = metadata_store_dir(data_home); - let kv_backend = standalone::build_metadata_kvbackend(metadata_dir, opts.metadata_store) - .context(error::BuildMetadataKvbackendSnafu)?; + let kv_backend = creator + .metadata_kv_backend_creator + .create(metadata_dir, &opts) + .await?; let procedure_manager = standalone::build_procedure_manager(kv_backend.clone(), opts.procedure); @@ -392,6 +421,9 @@ impl StartCommand { let mut builder = DatanodeBuilder::new(dn_opts, plugins.clone(), kv_backend.clone()); builder.with_cache_registry(layered_cache_registry.clone()); + if let Some(writable) = creator.open_regions_writable_override { + builder.with_open_regions_writable_override(writable); + } let datanode = builder.build().await.context(error::StartDatanodeSnafu)?; let information_extension = Arc::new(StandaloneInformationExtension::new( @@ -462,17 +494,16 @@ impl StartCommand { .await; } - let node_manager = Arc::new(StandaloneDatanodeManager { - region_server: datanode.region_server(), - flow_server: flownode.flow_engine(), - }); + let node_manager = creator + .node_manager_creator + .create( + &kv_backend, + datanode.region_server(), + flownode.flow_engine(), + ) + .await?; - let table_id_allocator = Arc::new( - SequenceBuilder::new(TABLE_ID_SEQ, kv_backend.clone()) - .initial(MIN_USER_TABLE_ID as u64) - .step(10) - .build(), - ); + let table_id_allocator = creator.table_id_allocator_creator.create(&kv_backend); let flow_id_sequence = Arc::new( SequenceBuilder::new(FLOW_ID_SEQ, kv_backend.clone()) .initial(MIN_USER_FLOW_ID as u64) @@ -489,7 +520,7 @@ impl StartCommand { .context(error::BuildWalProviderSnafu)?; let wal_provider = Arc::new(wal_provider); let table_metadata_allocator = Arc::new(TableMetadataAllocator::new( - table_id_allocator, + table_id_allocator.clone(), wal_provider.clone(), )); let flow_metadata_allocator = Arc::new(FlowMetadataAllocator::with_noop_peer_allocator( @@ -532,10 +563,10 @@ impl StartCommand { ddl_manager }; - let procedure_executor = Arc::new(LocalProcedureExecutor::new( - Arc::new(ddl_manager), - procedure_manager.clone(), - )); + let procedure_executor = creator + .procedure_executor_creator + .create(Arc::new(ddl_manager), procedure_manager.clone()) + .await?; let fe_instance = FrontendBuilder::new( fe_opts.clone(), @@ -568,7 +599,7 @@ impl StartCommand { kv_backend.clone(), layered_cache_registry.clone(), procedure_executor, - node_manager, + node_manager.clone(), ) .await .context(StartFlownodeSnafu)?; @@ -584,14 +615,21 @@ impl StartCommand { heartbeat_task: None, }; - Ok(Instance { + let instance = Instance { datanode, frontend, flownode, procedure_manager, wal_provider, - _guard: guard, - }) + leader_services_controller: creator.leader_services_controller, + _guard: vec![], + }; + let result = InstanceCreatorResult { + kv_backend, + node_manager, + table_id_allocator, + }; + Ok((instance, result)) } pub async fn create_table_metadata_manager( @@ -608,6 +646,258 @@ impl StartCommand { } } +#[async_trait] +pub trait NodeManagerCreator: Send + Sync { + async fn create( + &self, + kv_backend: &KvBackendRef, + region_server: RegionServer, + flow_server: FlownodeRef, + ) -> Result; +} + +pub struct DefaultNodeManagerCreator; + +#[async_trait] +impl NodeManagerCreator for DefaultNodeManagerCreator { + async fn create( + &self, + _: &KvBackendRef, + region_server: RegionServer, + flow_server: FlownodeRef, + ) -> Result { + Ok(Arc::new(StandaloneDatanodeManager { + region_server, + flow_server, + })) + } +} + +/// Customizes how standalone opens its metadata KV backend. +/// +/// The default implementation preserves the built-in raft-engine path. Other +/// callers can provide a custom implementation without changing standalone +/// configuration types. +#[async_trait] +pub trait MetadataKvBackendCreator: Send + Sync { + async fn create(&self, metadata_dir: String, opts: &StandaloneOptions) -> Result; +} + +pub struct DefaultMetadataKvBackendCreator; + +#[async_trait] +impl MetadataKvBackendCreator for DefaultMetadataKvBackendCreator { + async fn create(&self, metadata_dir: String, opts: &StandaloneOptions) -> Result { + standalone::build_metadata_kvbackend(metadata_dir, opts.metadata_store) + .context(error::BuildMetadataKvbackendSnafu) + } +} + +pub trait TableIdAllocatorCreator: Send + Sync { + fn create(&self, kv_backend: &KvBackendRef) -> Arc; +} + +struct DefaultTableIdAllocatorCreator; + +impl TableIdAllocatorCreator for DefaultTableIdAllocatorCreator { + fn create(&self, kv_backend: &KvBackendRef) -> Arc { + Arc::new( + SequenceBuilder::new(TABLE_ID_SEQ, kv_backend.clone()) + .initial(MIN_USER_TABLE_ID as u64) + .step(10) + .build(), + ) + } +} + +#[async_trait] +pub trait ProcedureExecutorCreator: Send + Sync { + async fn create( + &self, + ddl_manager: DdlManagerRef, + procedure_manager: ProcedureManagerRef, + ) -> Result; +} + +pub struct DefaultProcedureExecutorCreator; + +#[async_trait] +impl ProcedureExecutorCreator for DefaultProcedureExecutorCreator { + async fn create( + &self, + ddl_manager: DdlManagerRef, + procedure_manager: ProcedureManagerRef, + ) -> Result { + Ok(Arc::new(LocalProcedureExecutor::new( + ddl_manager, + procedure_manager, + ))) + } +} + +#[async_trait] +pub trait StandaloneLeaderServicesController: Send + Sync { + /// Starts services that manage standalone metadata or WAL state. + /// + /// The default implementation starts the procedure manager and WAL provider + /// during instance startup. + async fn start( + &self, + procedure_manager: ProcedureManagerRef, + wal_provider: WalProviderRef, + region_server: RegionServer, + ) -> Result<()>; + + /// Stops services started by [`StandaloneLeaderServicesController::start`]. + async fn stop( + &self, + procedure_manager: ProcedureManagerRef, + region_server: RegionServer, + ) -> Result<()>; +} + +pub struct DefaultStandaloneLeaderServicesController; + +#[async_trait] +impl StandaloneLeaderServicesController for DefaultStandaloneLeaderServicesController { + async fn start( + &self, + procedure_manager: ProcedureManagerRef, + wal_provider: WalProviderRef, + _region_server: RegionServer, + ) -> Result<()> { + procedure_manager + .start() + .await + .context(error::StartProcedureManagerSnafu)?; + wal_provider + .start() + .await + .context(error::StartWalProviderSnafu) + } + + async fn stop( + &self, + procedure_manager: ProcedureManagerRef, + _region_server: RegionServer, + ) -> Result<()> { + procedure_manager + .stop() + .await + .context(error::StopProcedureManagerSnafu) + } +} + +/// `InstanceCreator` is used for grouping various component creators for building the +/// Standalone instance, suitable for customizing how the instance can be built. +pub struct InstanceCreator { + /// Hook for replacing metadata KV construction while reusing the rest of the + /// standalone build flow. + metadata_kv_backend_creator: Box, + node_manager_creator: Box, + table_id_allocator_creator: Box, + procedure_executor_creator: Box, + leader_services_controller: Box, + open_regions_writable_override: Option, +} + +impl InstanceCreator { + pub fn new( + node_manager_creator: Box, + table_id_allocator_creator: Box, + procedure_executor_creator: Box, + ) -> Self { + Self { + metadata_kv_backend_creator: Box::new(DefaultMetadataKvBackendCreator), + node_manager_creator, + table_id_allocator_creator, + procedure_executor_creator, + leader_services_controller: Box::new(DefaultStandaloneLeaderServicesController), + open_regions_writable_override: None, + } + } + + pub fn with_metadata_kv_backend_creator( + mut self, + metadata_kv_backend_creator: Box, + ) -> Self { + self.metadata_kv_backend_creator = metadata_kv_backend_creator; + self + } + + /// Wraps the metadata backend creator while retaining the default creator. + /// + /// This is useful for callers that need to add runtime behavior around + /// metadata access without reimplementing backend selection. + pub fn map_metadata_kv_backend_creator(mut self, f: F) -> Self + where + F: FnOnce(Box) -> Box, + { + self.metadata_kv_backend_creator = f(self.metadata_kv_backend_creator); + self + } + + /// Wraps node-manager creation while preserving the selected standalone node manager. + pub fn map_node_manager_creator(mut self, f: F) -> Self + where + F: FnOnce(Box) -> Box, + { + self.node_manager_creator = f(self.node_manager_creator); + self + } + + /// Wraps procedure-executor creation while preserving the current setup. + pub fn map_procedure_executor_creator(mut self, f: F) -> Self + where + F: FnOnce(Box) -> Box, + { + self.procedure_executor_creator = f(self.procedure_executor_creator); + self + } + + /// Replaces startup/shutdown ownership for procedure manager and WAL provider. + pub fn with_leader_services_controller( + mut self, + leader_services_controller: Box, + ) -> Self { + self.leader_services_controller = leader_services_controller; + self + } + + /// Overrides whether regions opened during startup should become writable. + /// + /// `None` keeps the default startup behavior (regions open writable). + /// + /// Warning: setting this to `false` in standalone mode will leave reopened regions + /// permanently read-only. Standalone has no metasrv heartbeat or region-role + /// reconciliation, so there is no path to promote regions to Leader after startup. + pub fn with_open_regions_writable_override(mut self, writable: bool) -> Self { + self.open_regions_writable_override = Some(writable); + self + } +} + +impl Default for InstanceCreator { + fn default() -> Self { + Self { + metadata_kv_backend_creator: Box::new(DefaultMetadataKvBackendCreator), + node_manager_creator: Box::new(DefaultNodeManagerCreator), + table_id_allocator_creator: Box::new(DefaultTableIdAllocatorCreator), + procedure_executor_creator: Box::new(DefaultProcedureExecutorCreator), + leader_services_controller: Box::new(DefaultStandaloneLeaderServicesController), + open_regions_writable_override: None, + } + } +} + +/// `InstanceCreatorResult` is expected to be used paired with [InstanceCreator]. +/// It stores the created and other important components for further reusing. +pub struct InstanceCreatorResult { + pub kv_backend: KvBackendRef, + pub node_manager: NodeManagerRef, + pub table_id_allocator: Arc, +} + #[cfg(test)] mod tests { use std::default::Default; @@ -615,8 +905,10 @@ mod tests { use std::time::Duration; use auth::{Identity, Password, UserProviderRef}; + use clap::{CommandFactory, Parser}; use common_base::readable_size::ReadableSize; use common_config::ENV_VAR_SEP; + use common_options::plugin_options::StandaloneFlag; use common_test_util::temp_dir::create_named_temp_file; use common_wal::config::DatanodeWalConfig; use frontend::frontend::FrontendOptions; @@ -634,6 +926,7 @@ mod tests { }; let mut plugins = Plugins::new(); + plugins.insert(StandaloneFlag); plugins::setup_frontend_plugins(&mut plugins, &[], &fe_opts) .await .unwrap(); @@ -860,6 +1153,35 @@ mod tests { ); } + #[test] + fn test_parse_grpc_bind_addr_aliases() { + let command = + StartCommand::try_parse_from(["standalone", "--grpc-bind-addr", "127.0.0.1:14001"]) + .unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:14001")); + + let command = + StartCommand::try_parse_from(["standalone", "--rpc-bind-addr", "127.0.0.1:24001"]) + .unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:24001")); + + let command = + StartCommand::try_parse_from(["standalone", "--rpc-addr", "127.0.0.1:34001"]).unwrap(); + assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:34001")); + } + + #[test] + fn test_help_uses_grpc_option_names() { + let mut cmd = StartCommand::command(); + let mut help = Vec::new(); + cmd.write_long_help(&mut help).unwrap(); + let help = String::from_utf8(help).unwrap(); + + assert!(help.contains("--grpc-bind-addr")); + assert!(!help.contains("--rpc-bind-addr")); + assert!(!help.contains("--rpc-addr")); + } + #[test] fn test_load_default_standalone_options() { let options = diff --git a/src/cmd/tests/load_config_test.rs b/src/cmd/tests/load_config_test.rs index 2300a2250e..6cffcd67c2 100644 --- a/src/cmd/tests/load_config_test.rs +++ b/src/cmd/tests/load_config_test.rs @@ -16,7 +16,7 @@ use std::time::Duration; use cmd::options::GreptimeOptions; use common_base::memory_limit::MemoryLimit; -use common_config::{Configurable, DEFAULT_DATA_HOME}; +use common_config::{Configurable, DEFAULT_DATA_HOME, ENV_VAR_SEP}; use common_options::datanode::{ClientOptions, DatanodeClientOptions}; use common_telemetry::logging::{DEFAULT_LOGGING_DIR, DEFAULT_OTLP_HTTP_ENDPOINT, LoggingOptions}; use common_wal::config::DatanodeWalConfig; @@ -311,3 +311,51 @@ fn test_load_standalone_example_config() { }; similar_asserts::assert_eq!(options, expected); } + +#[test] +fn test_load_standalone_user_provider_from_config() { + let config = tempfile::NamedTempFile::new().unwrap(); + let user_provider = "static_user_provider:file:/tmp/greptimedb-users"; + std::fs::write( + config.path(), + format!("user_provider = \"{user_provider}\"\n"), + ) + .unwrap(); + + let options = + GreptimeOptions::::load_layered_options(config.path().to_str(), "") + .unwrap(); + + assert_eq!( + options.component.user_provider.as_deref(), + Some(user_provider) + ); + + let frontend_options = options.component.frontend_options(); + assert_eq!( + frontend_options.user_provider.as_deref(), + Some(user_provider) + ); +} + +#[test] +fn test_load_heartbeat_env_vars_from_env() { + let env_prefix = "HEARTBEAT_ENV_VARS_UT"; + let env_key = [env_prefix, "HEARTBEAT_ENV_VARS"].join(ENV_VAR_SEP); + + temp_env::with_var(env_key, Some("AZ,REGION"), || { + let expected = vec!["AZ".to_string(), "REGION".to_string()]; + + let datanode = + GreptimeOptions::::load_layered_options(None, env_prefix).unwrap(); + similar_asserts::assert_eq!(datanode.component.heartbeat_env_vars, expected); + + let frontend = + GreptimeOptions::::load_layered_options(None, env_prefix).unwrap(); + similar_asserts::assert_eq!(frontend.component.heartbeat_env_vars, expected); + + let standalone = + GreptimeOptions::::load_layered_options(None, env_prefix).unwrap(); + similar_asserts::assert_eq!(standalone.component.heartbeat_env_vars, expected); + }); +} diff --git a/src/common/base/Cargo.toml b/src/common/base/Cargo.toml index 3ec9e1fa35..44c30cd548 100644 --- a/src/common/base/Cargo.toml +++ b/src/common/base/Cargo.toml @@ -16,16 +16,11 @@ anymap2 = "0.13" async-trait.workspace = true bitvec = "1.0" bytes.workspace = true -common-error.workspace = true -common-macro.workspace = true futures.workspace = true lazy_static.workspace = true -paste.workspace = true pin-project.workspace = true -rand.workspace = true regex.workspace = true serde = { version = "1.0", features = ["derive"] } -snafu.workspace = true tokio.workspace = true zeroize = { version = "1.6", default-features = false, features = ["alloc"] } diff --git a/src/common/catalog/src/lib.rs b/src/common/catalog/src/lib.rs index 1e9534532e..592e0df8fe 100644 --- a/src/common/catalog/src/lib.rs +++ b/src/common/catalog/src/lib.rs @@ -74,9 +74,9 @@ pub fn parse_catalog_and_schema_from_db_string(db: &str) -> (String, String) { pub fn parse_optional_catalog_and_schema_from_db_string(db: &str) -> (Option, String) { let parts = db.splitn(2, '-').collect::>(); if parts.len() == 2 { - (Some(parts[0].to_lowercase()), parts[1].to_lowercase()) + (Some(parts[0].to_string()), parts[1].to_string()) } else { - (None, db.to_lowercase()) + (None, db.to_string()) } } @@ -118,7 +118,7 @@ mod tests { ); assert_eq!( - (Some("catalog".to_string()), "schema".to_string()), + (Some("CATALOG".to_string()), "SCHEMA".to_string()), parse_optional_catalog_and_schema_from_db_string("CATALOG-SCHEMA") ); diff --git a/src/common/config/Cargo.toml b/src/common/config/Cargo.toml index 2737f82a58..27b238add7 100644 --- a/src/common/config/Cargo.toml +++ b/src/common/config/Cargo.toml @@ -18,7 +18,6 @@ notify.workspace = true object-store.workspace = true serde.workspace = true serde_json.workspace = true -serde_with.workspace = true snafu.workspace = true toml.workspace = true diff --git a/src/common/config/src/config.rs b/src/common/config/src/config.rs index e25c46a0c0..85ce3d206f 100644 --- a/src/common/config/src/config.rs +++ b/src/common/config/src/config.rs @@ -53,7 +53,7 @@ pub trait Configurable: Serialize + DeserializeOwned + Default + Sized { env.try_parsing(true) .separator(ENV_VAR_SEP) - .ignore_empty(true) + .ignore_empty(false) }; // Workaround: Replacement for `Config::try_from(&default_opts)` due to @@ -237,4 +237,31 @@ mod tests { }, ); } + + #[derive(Debug, Serialize, Deserialize, Default)] + struct SimpleConfig { + name: Option, + prefix: Option, + } + + impl Configurable for SimpleConfig {} + + #[test] + fn test_empty_env_var_is_not_ignored() { + let env_prefix = "SIMPLE_CFG_UT"; + temp_env::with_vars( + [( + [env_prefix.to_string(), "PREFIX".to_string()].join(ENV_VAR_SEP), + Some(""), + )], + || { + let opts = SimpleConfig::load_layered_options(None, env_prefix).unwrap(); + // With ignore_empty(false), an empty env var should yield Some("") + // rather than None (which was the previous behavior with ignore_empty(true)). + assert_eq!(opts.prefix, Some("".to_string())); + // Unset env var should remain None. + assert_eq!(opts.name, None); + }, + ); + } } diff --git a/src/common/datasource/Cargo.toml b/src/common/datasource/Cargo.toml index 6ec9a14733..7df293d6d3 100644 --- a/src/common/datasource/Cargo.toml +++ b/src/common/datasource/Cargo.toml @@ -28,12 +28,10 @@ common-runtime.workspace = true common-telemetry.workspace = true datafusion.workspace = true datafusion-datasource.workspace = true -datafusion-orc.workspace = true datatypes.workspace = true futures.workspace = true lazy_static.workspace = true object-store.workspace = true -object_store_opendal.workspace = true orc-rust = { version = "0.7", default-features = false, features = ["async"] } parquet.workspace = true paste.workspace = true @@ -47,3 +45,4 @@ url.workspace = true [dev-dependencies] common-test-util.workspace = true +datafusion-orc.workspace = true diff --git a/src/common/datasource/src/file_format.rs b/src/common/datasource/src/file_format.rs index 7f4a7c65b4..a6a358c9e4 100644 --- a/src/common/datasource/src/file_format.rs +++ b/src/common/datasource/src/file_format.rs @@ -42,7 +42,6 @@ use datafusion::physical_plan::SendableRecordBatchStream; use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet; use futures::{StreamExt, TryStreamExt}; use object_store::ObjectStore; -use object_store_opendal::OpendalStore; use snafu::ResultExt; use tokio::io::AsyncWriteExt; use tokio_util::compat::FuturesAsyncWriteCompatExt; @@ -317,7 +316,7 @@ pub async fn file_to_stream( .with_file_compression_type(df_compression) .build(); - let store = Arc::new(OpendalStore::new(store.clone())); + let store = Arc::new(object_store::compat::OpendalStore::new(store.clone())); let file_opener = config.file_source().create_file_opener(store, &config, 0)?; let stream = FileStream::new(&config, 0, file_opener, &ExecutionPlanMetricsSet::new())?; diff --git a/src/common/datasource/src/file_format/parquet.rs b/src/common/datasource/src/file_format/parquet.rs index c2c14b4680..9c8e8d6ce8 100644 --- a/src/common/datasource/src/file_format/parquet.rs +++ b/src/common/datasource/src/file_format/parquet.rs @@ -23,7 +23,9 @@ use datafusion::error::Result as DatafusionResult; use datafusion::parquet::arrow::async_reader::AsyncFileReader; use datafusion::parquet::arrow::{ArrowWriter, parquet_to_arrow_schema}; use datafusion::parquet::errors::{ParquetError, Result as ParquetResult}; -use datafusion::parquet::file::metadata::ParquetMetaData; +use datafusion::parquet::file::metadata::{ + PageIndexPolicy, ParquetMetaData, ParquetMetaDataReader, +}; use datafusion::physical_plan::SendableRecordBatchStream; use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet; use datafusion_datasource::PartitionedFile; @@ -94,35 +96,40 @@ impl DefaultParquetFileReaderFactory { } impl ParquetFileReaderFactory for DefaultParquetFileReaderFactory { - // TODO(weny): Supports [`metadata_size_hint`]. - // The upstream has a implementation supports [`metadata_size_hint`], - // however it coupled with Box. fn create_reader( &self, _partition_index: usize, partitioned_file: PartitionedFile, - _metadata_size_hint: Option, + metadata_size_hint: Option, _metrics: &ExecutionPlanMetricsSet, ) -> DatafusionResult> { let path = partitioned_file.path().to_string(); let object_store = self.object_store.clone(); - Ok(Box::new(LazyParquetFileReader::new(object_store, path))) + Ok(Box::new(LazyParquetFileReader::new( + object_store, + path, + metadata_size_hint, + ))) } } pub struct LazyParquetFileReader { object_store: ObjectStore, reader: Option>, + file_size: Option, + metadata_size_hint: Option, path: String, } impl LazyParquetFileReader { - pub fn new(object_store: ObjectStore, path: String) -> Self { + pub fn new(object_store: ObjectStore, path: String, metadata_size_hint: Option) -> Self { LazyParquetFileReader { object_store, path, reader: None, + file_size: None, + metadata_size_hint, } } @@ -130,6 +137,7 @@ impl LazyParquetFileReader { async fn maybe_initialize(&mut self) -> result::Result<(), object_store::Error> { if self.reader.is_none() { let meta = self.object_store.stat(&self.path).await?; + self.file_size = Some(meta.content_length()); let reader = self .object_store .reader(&self.path) @@ -166,8 +174,19 @@ impl AsyncFileReader for LazyParquetFileReader { self.maybe_initialize() .await .map_err(|e| ParquetError::External(Box::new(e)))?; - // Safety: Must initialized - self.reader.as_mut().unwrap().get_metadata(options).await + + let metadata_opts = options.map(|o| o.metadata_options().clone()); + let metadata_reader = ParquetMetaDataReader::new() + .with_metadata_options(metadata_opts) + .with_page_index_policy(PageIndexPolicy::from( + options.is_some_and(|o| o.page_index()), + )) + .with_prefetch_hint(self.metadata_size_hint); + + let metadata = metadata_reader + .load_and_finish(self.reader.as_mut().unwrap(), self.file_size.unwrap()) + .await?; + Ok(Arc::new(metadata)) }) } } diff --git a/src/common/datasource/src/file_format/tests.rs b/src/common/datasource/src/file_format/tests.rs index ad54472d33..6ef669ab7b 100644 --- a/src/common/datasource/src/file_format/tests.rs +++ b/src/common/datasource/src/file_format/tests.rs @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::assert_matches::assert_matches; use std::collections::HashMap; use std::sync::Arc; -use std::vec; +use std::{assert_matches, vec}; use common_test_util::find_workspace_path; use datafusion::assert_batches_eq; @@ -45,7 +44,7 @@ struct Test<'a> { impl Test<'_> { async fn run(self, store: &ObjectStore) { - let store = Arc::new(object_store_opendal::OpendalStore::new(store.clone())); + let store = Arc::new(object_store::compat::OpendalStore::new(store.clone())); let file_opener = self .file_source .create_file_opener(store, &self.config, 0) diff --git a/src/common/datasource/src/lib.rs b/src/common/datasource/src/lib.rs index 91663ce22c..f4c7fdf120 100644 --- a/src/common/datasource/src/lib.rs +++ b/src/common/datasource/src/lib.rs @@ -12,9 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![feature(assert_matches)] -#![feature(type_alias_impl_trait)] - pub mod buffered_writer; pub mod compressed_writer; pub mod compression; diff --git a/src/common/datasource/src/test_util.rs b/src/common/datasource/src/test_util.rs index ea2b0c768c..0a13d9c6e8 100644 --- a/src/common/datasource/src/test_util.rs +++ b/src/common/datasource/src/test_util.rs @@ -103,7 +103,7 @@ pub async fn setup_stream_to_json_test(origin_path: &str, threshold: impl Fn(usi test_util::TEST_BATCH_SIZE, schema.clone(), FileCompressionType::UNCOMPRESSED, - Arc::new(object_store_opendal::OpendalStore::new(store.clone())), + Arc::new(object_store::compat::OpendalStore::new(store.clone())), true, ); @@ -157,7 +157,7 @@ pub async fn setup_stream_to_csv_test( let csv_opener = csv_source .create_file_opener( - Arc::new(object_store_opendal::OpendalStore::new(store.clone())), + Arc::new(object_store::compat::OpendalStore::new(store.clone())), &config, 0, ) diff --git a/src/common/frontend/src/error.rs b/src/common/frontend/src/error.rs index cee8c6df77..19fb29aeb7 100644 --- a/src/common/frontend/src/error.rs +++ b/src/common/frontend/src/error.rs @@ -30,7 +30,7 @@ pub enum Error { #[snafu(display("Failed to list nodes from metasrv"))] Meta { - source: Box, + source: Box, #[snafu(implicit)] location: Location, }, @@ -52,7 +52,7 @@ pub enum Error { #[snafu(display("Failed to invoke list process service"))] CreateChannel { - source: common_grpc::error::Error, + source: Box, #[snafu(implicit)] location: Location, }, diff --git a/src/common/frontend/src/selector.rs b/src/common/frontend/src/selector.rs index 804169d1dd..bba36ef81b 100644 --- a/src/common/frontend/src/selector.rs +++ b/src/common/frontend/src/selector.rs @@ -16,7 +16,7 @@ use std::fmt::Debug; use std::time::Duration; use common_grpc::channel_manager::{ChannelConfig, ChannelManager}; -use common_meta::cluster::{ClusterInfo, NodeInfo, Role}; +use common_meta::peer::{Peer, PeerDiscovery}; use greptime_proto::v1::frontend::{ KillProcessRequest, KillProcessResponse, ListProcessRequest, ListProcessResponse, frontend_client, @@ -62,7 +62,7 @@ impl FrontendClient for frontend_client::FrontendClient(&self, predicate: F) -> Result> where - F: Fn(&NodeInfo) -> bool + Send; + F: Fn(&Peer) -> bool + Send; } #[derive(Debug, Clone)] @@ -75,22 +75,23 @@ pub struct MetaClientSelector { impl FrontendSelector for MetaClientSelector { async fn select(&self, predicate: F) -> Result> where - F: Fn(&NodeInfo) -> bool + Send, + F: Fn(&Peer) -> bool + Send, { - let nodes = self + let peers = self .meta_client - .list_nodes(Some(Role::Frontend)) + .active_frontends() .await .map_err(Box::new) .context(MetaSnafu)?; - nodes + peers .into_iter() .filter(predicate) - .map(|node| { + .map(|peer| { let channel = self .channel_manager - .get(node.peer.addr) + .get(peer.addr) + .map_err(Box::new) .context(error::CreateChannelSnafu)?; let client = frontend_client::FrontendClient::new(channel); Ok(Box::new(client) as FrontendClientPtr) diff --git a/src/common/function/Cargo.toml b/src/common/function/Cargo.toml index d164b9285d..2829f92985 100644 --- a/src/common/function/Cargo.toml +++ b/src/common/function/Cargo.toml @@ -47,8 +47,8 @@ geo-types = { version = "0.7", optional = true } geohash = { version = "0.13", optional = true } h3o = { version = "0.6", optional = true } hyperloglogplus = "0.4" +icu_properties.workspace = true jsonb.workspace = true -jsonpath-rust = "0.7.5" memchr = "2.7" mito-codec.workspace = true nalgebra.workspace = true diff --git a/src/common/function/src/admin/flush_compact_region.rs b/src/common/function/src/admin/flush_compact_region.rs index 60fd19ef5a..6dcf92117a 100644 --- a/src/common/function/src/admin/flush_compact_region.rs +++ b/src/common/function/src/admin/flush_compact_region.rs @@ -128,7 +128,7 @@ mod tests { }; let result = f.invoke_async_with_args(func_args).await.unwrap_err(); assert_eq!( - "Execution error: Handler error: Missing TableMutationHandler, not expected", + "Execution error: Missing TableMutationHandler, not expected", result.to_string() ); } diff --git a/src/common/function/src/admin/flush_compact_table.rs b/src/common/function/src/admin/flush_compact_table.rs index 3298a95061..20b95e4379 100644 --- a/src/common/function/src/admin/flush_compact_table.rs +++ b/src/common/function/src/admin/flush_compact_table.rs @@ -355,7 +355,7 @@ mod tests { }; let result = f.invoke_async_with_args(func_args).await.unwrap_err(); assert_eq!( - "Execution error: Handler error: Missing TableMutationHandler, not expected", + "Execution error: Missing TableMutationHandler, not expected", result.to_string() ); } diff --git a/src/common/function/src/admin/migrate_region.rs b/src/common/function/src/admin/migrate_region.rs index 91b5540b1a..d958b3cce9 100644 --- a/src/common/function/src/admin/migrate_region.rs +++ b/src/common/function/src/admin/migrate_region.rs @@ -173,7 +173,7 @@ mod tests { }; let result = f.invoke_async_with_args(func_args).await.unwrap_err(); assert_eq!( - "Execution error: Handler error: Missing ProcedureServiceHandler, not expected", + "Execution error: Missing ProcedureServiceHandler, not expected", result.to_string() ); } diff --git a/src/common/function/src/aggrs/aggr_wrapper.rs b/src/common/function/src/aggrs/aggr_wrapper.rs index 3780d39582..6242ab9454 100644 --- a/src/common/function/src/aggrs/aggr_wrapper.rs +++ b/src/common/function/src/aggrs/aggr_wrapper.rs @@ -25,7 +25,7 @@ use std::hash::{Hash, Hasher}; use std::sync::Arc; -use arrow::array::StructArray; +use arrow::array::{ArrayRef, BooleanArray, StructArray}; use arrow_schema::{FieldRef, Fields}; use common_telemetry::debug; use datafusion::functions_aggregate::all_default_aggregate_functions; @@ -38,8 +38,8 @@ use datafusion_common::{Column, ScalarValue}; use datafusion_expr::expr::{AggregateFunction, AggregateFunctionParams}; use datafusion_expr::function::StateFieldsArgs; use datafusion_expr::{ - Accumulator, Aggregate, AggregateUDF, AggregateUDFImpl, Expr, ExprSchemable, LogicalPlan, - Signature, + Accumulator, Aggregate, AggregateUDF, AggregateUDFImpl, EmitTo, Expr, ExprSchemable, + GroupsAccumulator, LogicalPlan, Signature, }; use datafusion_physical_expr::aggregate::AggregateFunctionExpr; use datatypes::arrow::datatypes::{DataType, Field}; @@ -322,6 +322,14 @@ impl StateWrapper { ); }) } + + fn fix_inner_acc_args<'b>( + &self, + mut acc_args: datafusion_expr::function::AccumulatorArgs<'b>, + ) -> datafusion_common::Result> { + acc_args.return_field = self.deduce_aggr_return_type(&acc_args)?; + Ok(acc_args) + } } impl AggregateUDFImpl for StateWrapper { @@ -331,15 +339,32 @@ impl AggregateUDFImpl for StateWrapper { ) -> datafusion_common::Result> { // fix and recover proper acc args for the original aggregate function. let state_type = acc_args.return_type().clone(); - let inner = { - let mut new_acc_args = acc_args.clone(); - new_acc_args.return_field = self.deduce_aggr_return_type(&acc_args)?; - self.inner.accumulator(new_acc_args)? - }; + let inner = self.inner.accumulator(self.fix_inner_acc_args(acc_args)?)?; Ok(Box::new(StateAccum::new(inner, state_type)?)) } + fn groups_accumulator_supported( + &self, + acc_args: datafusion_expr::function::AccumulatorArgs, + ) -> bool { + self.fix_inner_acc_args(acc_args) + .map(|args| self.inner.inner().groups_accumulator_supported(args)) + .unwrap_or(false) + } + + fn create_groups_accumulator( + &self, + acc_args: datafusion_expr::function::AccumulatorArgs, + ) -> datafusion_common::Result> { + let state_type = acc_args.return_type().clone(); + let inner = self + .inner + .inner() + .create_groups_accumulator(self.fix_inner_acc_args(acc_args)?)?; + Ok(Box::new(StateGroupsAccum::new(inner, state_type)?)) + } + fn as_any(&self) -> &dyn std::any::Any { self } @@ -462,6 +487,118 @@ pub struct StateAccum { state_fields: Fields, } +pub struct StateGroupsAccum { + inner: Box, + state_fields: Fields, +} + +impl StateGroupsAccum { + fn new( + inner: Box, + state_type: DataType, + ) -> datafusion_common::Result { + let DataType::Struct(fields) = state_type else { + return Err(datafusion_common::DataFusionError::Internal(format!( + "Expected a struct type for state, got: {:?}", + state_type + ))); + }; + Ok(Self { + inner, + state_fields: fields, + }) + } + + fn wrap_state_arrays(&self, arrays: Vec) -> datafusion_common::Result { + let array_type = arrays + .iter() + .map(|array| array.data_type().clone()) + .collect::>(); + let expected_type = self + .state_fields + .iter() + .map(|field| field.data_type().clone()) + .collect::>(); + if array_type != expected_type { + debug!( + "State mismatch, expected: {}, got: {} for expected fields: {:?} and given array types: {:?}", + self.state_fields.len(), + arrays.len(), + self.state_fields, + array_type, + ); + let guess_schema = arrays + .iter() + .enumerate() + .map(|(index, array)| { + Field::new( + format!("col_{index}[mismatch_state]").as_str(), + array.data_type().clone(), + true, + ) + }) + .collect::(); + let array = StructArray::try_new(guess_schema, arrays, None)?; + return Ok(Arc::new(array)); + } + + Ok(Arc::new(StructArray::try_new( + self.state_fields.clone(), + arrays, + None, + )?)) + } +} + +impl GroupsAccumulator for StateGroupsAccum { + fn update_batch( + &mut self, + values: &[ArrayRef], + group_indices: &[usize], + opt_filter: Option<&BooleanArray>, + total_num_groups: usize, + ) -> datafusion_common::Result<()> { + self.inner + .update_batch(values, group_indices, opt_filter, total_num_groups) + } + + fn merge_batch( + &mut self, + values: &[ArrayRef], + group_indices: &[usize], + opt_filter: Option<&BooleanArray>, + total_num_groups: usize, + ) -> datafusion_common::Result<()> { + self.inner + .merge_batch(values, group_indices, opt_filter, total_num_groups) + } + + fn evaluate(&mut self, emit_to: EmitTo) -> datafusion_common::Result { + let state = self.inner.state(emit_to)?; + self.wrap_state_arrays(state) + } + + fn state(&mut self, emit_to: EmitTo) -> datafusion_common::Result> { + self.inner.state(emit_to) + } + + fn convert_to_state( + &self, + values: &[ArrayRef], + opt_filter: Option<&BooleanArray>, + ) -> datafusion_common::Result> { + self.inner.convert_to_state(values, opt_filter) + } + + fn supports_convert_to_state(&self) -> bool { + self.inner.supports_convert_to_state() + } + + fn size(&self) -> usize { + self.inner.size() + } +} + impl StateAccum { pub fn new( inner: Box, diff --git a/src/common/function/src/aggrs/aggr_wrapper/tests.rs b/src/common/function/src/aggrs/aggr_wrapper/tests.rs index 8821b9fd24..de3a77df6b 100644 --- a/src/common/function/src/aggrs/aggr_wrapper/tests.rs +++ b/src/common/function/src/aggrs/aggr_wrapper/tests.rs @@ -40,10 +40,13 @@ use datafusion_common::arrow::array::AsArray; use datafusion_common::arrow::datatypes::{Float64Type, UInt64Type}; use datafusion_common::{Column, TableReference}; use datafusion_expr::expr::{AggregateFunction, NullTreatment}; +use datafusion_expr::function::AccumulatorArgs; use datafusion_expr::{ - Aggregate, ColumnarValue, Expr, LogicalPlan, ScalarFunctionArgs, SortExpr, TableScan, lit, + Aggregate, AggregateUDFImpl, ColumnarValue, Expr, LogicalPlan, ScalarFunctionArgs, SortExpr, + TableScan, lit, }; use datafusion_physical_expr::aggregate::AggregateExprBuilder; +use datafusion_physical_expr::expressions::col; use datafusion_physical_expr::{EquivalenceProperties, Partitioning}; use datatypes::arrow_array::StringArray; use futures::{Stream, StreamExt as _}; @@ -256,6 +259,38 @@ fn dummy_table_scan_with_ts() -> LogicalPlan { ) } +fn create_avg_state_groups_accumulator() -> Box { + let state_wrapper = StateWrapper::new((*avg_udaf()).clone()).unwrap(); + let schema = Arc::new(arrow_schema::Schema::new(vec![Field::new( + "number", + DataType::Float64, + true, + )])); + let expr = col("number", &schema).unwrap(); + let expr_field = expr.return_field(&schema).unwrap(); + let return_field = Arc::new(Field::new( + "__avg_state(number)", + state_wrapper.return_type(&[DataType::Float64]).unwrap(), + true, + )); + let exprs = [expr]; + let expr_fields = [expr_field]; + let acc_args = AccumulatorArgs { + return_field, + schema: &schema, + ignore_nulls: false, + order_bys: &[], + is_reversed: false, + name: "__avg_state(number)", + is_distinct: false, + exprs: &exprs, + expr_fields: &expr_fields, + }; + + assert!(state_wrapper.groups_accumulator_supported(acc_args.clone())); + state_wrapper.create_groups_accumulator(acc_args).unwrap() +} + #[tokio::test] async fn test_sum_udaf() { let ctx = SessionContext::new(); @@ -796,6 +831,95 @@ async fn test_last_value_order_by_udaf() { assert_eq!(merge_eval_res, ScalarValue::Int64(Some(4))); } +#[test] +fn test_avg_state_groups_accumulator_evaluate() { + let mut state_accum = create_avg_state_groups_accumulator(); + let values = vec![Arc::new(Float64Array::from(vec![ + Some(1.0), + Some(2.0), + None, + Some(3.0), + Some(4.0), + Some(5.0), + ])) as ArrayRef]; + let group_indices = vec![0, 1, 0, 0, 1, 2]; + + state_accum + .update_batch(&values, &group_indices, None, 3) + .unwrap(); + + let result = state_accum.evaluate(EmitTo::All).unwrap(); + let result = result.as_any().downcast_ref::().unwrap(); + + assert_eq!( + result + .column(0) + .as_any() + .downcast_ref::() + .unwrap(), + &UInt64Array::from(vec![2, 2, 1]) + ); + assert_eq!( + result + .column(1) + .as_any() + .downcast_ref::() + .unwrap(), + &Float64Array::from(vec![4.0, 6.0, 5.0]) + ); +} + +#[test] +fn test_avg_state_groups_accumulator_state_merge_evaluate() { + let mut source_accum = create_avg_state_groups_accumulator(); + let source_values = vec![Arc::new(Float64Array::from(vec![ + Some(1.0), + Some(2.0), + None, + Some(3.0), + Some(4.0), + Some(5.0), + ])) as ArrayRef]; + let source_group_indices = vec![0, 1, 0, 0, 1, 2]; + + source_accum + .update_batch(&source_values, &source_group_indices, None, 3) + .unwrap(); + let source_state = source_accum.state(EmitTo::All).unwrap(); + + let mut merged_accum = create_avg_state_groups_accumulator(); + let merged_values = + vec![Arc::new(Float64Array::from(vec![Some(10.0), Some(20.0), Some(30.0)])) as ArrayRef]; + let merged_group_indices = vec![0, 1, 2]; + + merged_accum + .update_batch(&merged_values, &merged_group_indices, None, 3) + .unwrap(); + merged_accum + .merge_batch(&source_state, &[1, 2, 0], None, 3) + .unwrap(); + + let result = merged_accum.evaluate(EmitTo::All).unwrap(); + let result = result.as_any().downcast_ref::().unwrap(); + + assert_eq!( + result + .column(0) + .as_any() + .downcast_ref::() + .unwrap(), + &UInt64Array::from(vec![2, 3, 3]) + ); + assert_eq!( + result + .column(1) + .as_any() + .downcast_ref::() + .unwrap(), + &Float64Array::from(vec![15.0, 24.0, 36.0]) + ); +} + /// For testing whether the UDAF state fields are correctly implemented. /// esp. for our own custom UDAF's state fields. /// By compare eval results before and after split to state/merge functions. diff --git a/src/common/function/src/flush_flow.rs b/src/common/function/src/flush_flow.rs index c4ea554585..35f347bcb2 100644 --- a/src/common/function/src/flush_flow.rs +++ b/src/common/function/src/flush_flow.rs @@ -149,7 +149,7 @@ mod test { let result = f.invoke_async_with_args(func_args).await.unwrap_err(); assert_eq!( - "Execution error: Handler error: Missing FlowServiceHandler, not expected", + "Execution error: Missing FlowServiceHandler, not expected", result.to_string() ); } diff --git a/src/common/function/src/lib.rs b/src/common/function/src/lib.rs index 36fd27381d..7abd595367 100644 --- a/src/common/function/src/lib.rs +++ b/src/common/function/src/lib.rs @@ -13,7 +13,6 @@ // limitations under the License. #![feature(try_blocks)] -#![feature(assert_matches)] mod admin; mod flush_flow; diff --git a/src/common/function/src/scalars/ip/ipv4.rs b/src/common/function/src/scalars/ip/ipv4.rs index dfaab40d68..b03fbf0a85 100644 --- a/src/common/function/src/scalars/ip/ipv4.rs +++ b/src/common/function/src/scalars/ip/ipv4.rs @@ -20,7 +20,10 @@ use common_query::error::InvalidFuncArgsSnafu; use datafusion_common::arrow::array::{Array, AsArray, StringViewBuilder, UInt32Builder}; use datafusion_common::arrow::compute; use datafusion_common::arrow::datatypes::{DataType, UInt32Type}; -use datafusion_expr::{ColumnarValue, ScalarFunctionArgs, Signature, TypeSignature, Volatility}; +use datafusion_expr::{ + Coercion, ColumnarValue, ScalarFunctionArgs, Signature, TypeSignature, TypeSignatureClass, + Volatility, +}; use derive_more::Display; use crate::function::{Function, extract_args}; @@ -44,7 +47,7 @@ impl Default for Ipv4NumToString { fn default() -> Self { Self { signature: Signature::new( - TypeSignature::Exact(vec![DataType::UInt32]), + TypeSignature::Coercible(vec![Coercion::new_exact(TypeSignatureClass::Integer)]), Volatility::Immutable, ), aliases: ["inet_ntoa".to_string()], @@ -70,6 +73,14 @@ impl Function for Ipv4NumToString { args: ScalarFunctionArgs, ) -> datafusion_common::Result { let [arg0] = extract_args(self.name(), &args)?; + let arg0 = compute::cast_with_options( + &arg0, + &DataType::UInt32, + &compute::CastOptions { + safe: false, + ..Default::default() + }, + )?; let uint_vec = arg0.as_primitive::(); let size = uint_vec.len(); @@ -171,7 +182,7 @@ mod tests { use std::sync::Arc; use arrow_schema::Field; - use datafusion_common::arrow::array::{StringViewArray, UInt32Array}; + use datafusion_common::arrow::array::{Int64Array, StringViewArray, UInt32Array}; use super::*; @@ -200,6 +211,51 @@ mod tests { assert_eq!(result.value(3), "255.255.255.255"); } + #[test] + fn test_ipv4_num_to_string_accepts_int64() { + let func = Ipv4NumToString::default(); + + // Test data + let values = vec![167772161i64, 3232235521i64, 0i64, 4294967295i64]; + let input = ColumnarValue::Array(Arc::new(Int64Array::from(values))); + + let args = ScalarFunctionArgs { + args: vec![input], + arg_fields: vec![], + number_rows: 4, + return_field: Arc::new(Field::new("x", DataType::Utf8View, false)), + config_options: Arc::new(Default::default()), + }; + let result = func.invoke_with_args(args).unwrap(); + let result = result.to_array(4).unwrap(); + let result = result.as_string_view(); + + assert_eq!(result.value(0), "10.0.0.1"); + assert_eq!(result.value(1), "192.168.0.1"); + assert_eq!(result.value(2), "0.0.0.0"); + assert_eq!(result.value(3), "255.255.255.255"); + } + + #[test] + fn test_ipv4_num_to_string_rejects_negative_int64() { + let func = Ipv4NumToString::default(); + + // Test data + let values = vec![-1i64]; + let input = ColumnarValue::Array(Arc::new(Int64Array::from(values))); + + let args = ScalarFunctionArgs { + args: vec![input], + arg_fields: vec![], + number_rows: 1, + return_field: Arc::new(Field::new("x", DataType::Utf8View, false)), + config_options: Arc::new(Default::default()), + }; + let result = func.invoke_with_args(args); + + assert!(result.is_err()); + } + #[test] fn test_ipv4_string_to_num() { let func = Ipv4StringToNum::default(); diff --git a/src/common/function/src/scalars/json/json_get.rs b/src/common/function/src/scalars/json/json_get.rs index a7b2243c3d..d081ad917b 100644 --- a/src/common/function/src/scalars/json/json_get.rs +++ b/src/common/function/src/scalars/json/json_get.rs @@ -12,24 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::str::FromStr; use std::sync::Arc; -use arrow::array::{ArrayRef, BinaryViewArray, StringViewArray, StructArray}; +use arrow::array::{ArrayRef, BinaryViewArray, new_null_array}; use arrow::compute; -use arrow::datatypes::{Float64Type, Int64Type, UInt64Type}; +use arrow::datatypes::Float64Type; use arrow_schema::Field; use datafusion_common::arrow::array::{ Array, AsArray, BinaryViewBuilder, BooleanBuilder, Float64Builder, Int64Builder, StringViewBuilder, }; use datafusion_common::arrow::datatypes::DataType; -use datafusion_common::{DataFusionError, Result}; +use datafusion_common::{DataFusionError, Result, ScalarValue, exec_datafusion_err, exec_err}; use datafusion_expr::{ColumnarValue, ScalarFunctionArgs, Signature, Volatility}; use datatypes::arrow_array::{int_array_value_at_index, string_array_value_at_index}; -use datatypes::json::JsonStructureSettings; +use datatypes::vectors::json::array::JsonArray; use derive_more::Display; -use jsonpath_rust::JsonPath; use serde_json::Value; use crate::function::{Function, extract_args}; @@ -52,6 +50,7 @@ fn get_json_by_path(json: &[u8], path: &str) -> Option> { enum JsonResultValue<'a> { Jsonb(Vec), + #[expect(unused)] JsonStructByColumn(&'a ArrayRef, usize), JsonStructByValue(&'a Value), } @@ -64,57 +63,20 @@ trait JsonGetResultBuilder { fn build(&mut self) -> ArrayRef; } -/// Common implementation for JSON get scalar functions. -/// -/// `JsonGet` encapsulates the logic for extracting values from JSON inputs -/// based on a path expression. Different JSON get functions reuse this -/// implementation by supplying their own `JsonGetResultBuilder` to control -/// how the resulting values are materialized into an Arrow array. -#[derive(Debug)] -struct JsonGet { - signature: Signature, -} - -impl JsonGet { - fn invoke(&self, args: ScalarFunctionArgs, builder_factory: F) -> Result - where - F: Fn(usize) -> B, - B: JsonGetResultBuilder, - { - let [arg0, arg1] = extract_args("JSON_GET", &args)?; - - let arg1 = compute::cast(&arg1, &DataType::Utf8View)?; - let paths = arg1.as_string_view(); - - let mut builder = (builder_factory)(arg0.len()); - match arg0.data_type() { - DataType::Binary | DataType::LargeBinary | DataType::BinaryView => { - let arg0 = compute::cast(&arg0, &DataType::BinaryView)?; - let jsons = arg0.as_binary_view(); - jsonb_get(jsons, paths, &mut builder)?; - } - DataType::Struct(_) => { - let jsons = arg0.as_struct(); - json_struct_get(jsons, paths, &mut builder)? - } - _ => { - return Err(DataFusionError::Execution(format!( - "JSON_GET not supported argument type {}", - arg0.data_type(), - ))); - } - }; - - Ok(ColumnarValue::Array(builder.build())) - } -} - -impl Default for JsonGet { - fn default() -> Self { - Self { - signature: Signature::any(2, Volatility::Immutable), +fn result_builder(len: usize, with_type: &DataType) -> Result> { + let builder = match with_type { + DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => { + Box::new(StringResultBuilder(StringViewBuilder::with_capacity(len))) + as Box } - } + DataType::Int64 => Box::new(IntResultBuilder(Int64Builder::with_capacity(len))), + DataType::Float64 => Box::new(FloatResultBuilder(Float64Builder::with_capacity(len))), + DataType::Boolean => Box::new(BoolResultBuilder(BooleanBuilder::with_capacity(len))), + t => { + return exec_err!("json_get with unknown type {t}"); + } + }; + Ok(builder) } // TODO: refactor this to StringLikeArrayBuilder from Arrow 57 @@ -154,7 +116,7 @@ impl JsonGetResultBuilder for StringResultBuilder { #[derive(Default, Display, Debug)] #[display("{}", Self::NAME.to_ascii_uppercase())] -pub struct JsonGetString(JsonGet); +pub struct JsonGetString(JsonGetWithType); impl JsonGetString { pub const NAME: &'static str = "json_get_string"; @@ -173,10 +135,10 @@ impl Function for JsonGetString { &self.0.signature } - fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result { - self.0.invoke(args, |len: usize| { - StringResultBuilder(StringViewBuilder::with_capacity(len)) - }) + fn invoke_with_args(&self, mut args: ScalarFunctionArgs) -> Result { + args.args + .push(ColumnarValue::Scalar(ScalarValue::Utf8View(None))); + self.0.invoke_with_args(args) } } @@ -205,7 +167,7 @@ impl JsonGetResultBuilder for IntResultBuilder { #[derive(Default, Display, Debug)] #[display("{}", Self::NAME.to_ascii_uppercase())] -pub struct JsonGetInt(JsonGet); +pub struct JsonGetInt(JsonGetWithType); impl JsonGetInt { pub const NAME: &'static str = "json_get_int"; @@ -224,10 +186,10 @@ impl Function for JsonGetInt { &self.0.signature } - fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result { - self.0.invoke(args, |len: usize| { - IntResultBuilder(Int64Builder::with_capacity(len)) - }) + fn invoke_with_args(&self, mut args: ScalarFunctionArgs) -> Result { + args.args + .push(ColumnarValue::Scalar(ScalarValue::Int64(None))); + self.0.invoke_with_args(args) } } @@ -264,7 +226,7 @@ impl JsonGetResultBuilder for FloatResultBuilder { #[derive(Default, Display, Debug)] #[display("{}", Self::NAME.to_ascii_uppercase())] -pub struct JsonGetFloat(JsonGet); +pub struct JsonGetFloat(JsonGetWithType); impl JsonGetFloat { pub const NAME: &'static str = "json_get_float"; @@ -283,10 +245,10 @@ impl Function for JsonGetFloat { &self.0.signature } - fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result { - self.0.invoke(args, |len: usize| { - FloatResultBuilder(Float64Builder::with_capacity(len)) - }) + fn invoke_with_args(&self, mut args: ScalarFunctionArgs) -> Result { + args.args + .push(ColumnarValue::Scalar(ScalarValue::Float64(None))); + self.0.invoke_with_args(args) } } @@ -323,7 +285,7 @@ impl JsonGetResultBuilder for BoolResultBuilder { #[derive(Default, Display, Debug)] #[display("{}", Self::NAME.to_ascii_uppercase())] -pub struct JsonGetBool(JsonGet); +pub struct JsonGetBool(JsonGetWithType); impl JsonGetBool { pub const NAME: &'static str = "json_get_bool"; @@ -342,24 +304,23 @@ impl Function for JsonGetBool { &self.0.signature } - fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result { - self.0.invoke(args, |len: usize| { - BoolResultBuilder(BooleanBuilder::with_capacity(len)) - }) + fn invoke_with_args(&self, mut args: ScalarFunctionArgs) -> Result { + args.args + .push(ColumnarValue::Scalar(ScalarValue::Boolean(None))); + self.0.invoke_with_args(args) } } fn jsonb_get( jsons: &BinaryViewArray, - paths: &StringViewArray, + path: &str, builder: &mut dyn JsonGetResultBuilder, ) -> Result<()> { let size = jsons.len(); for i in 0..size { let json = jsons.is_valid(i).then(|| jsons.value(i)); - let path = paths.is_valid(i).then(|| paths.value(i)); - let result = match (json, path) { - (Some(json), Some(path)) => get_json_by_path(json, path), + let result = match json { + Some(json) => get_json_by_path(json, path), _ => None, }; if let Some(v) = result { @@ -371,141 +332,90 @@ fn jsonb_get( Ok(()) } -fn json_struct_get( - jsons: &StructArray, - paths: &StringViewArray, - builder: &mut dyn JsonGetResultBuilder, -) -> Result<()> { - let size = jsons.len(); - for i in 0..size { - if jsons.is_null(i) || paths.is_null(i) { - builder.append_null(); - continue; +fn json_struct_get(array: &ArrayRef, path: &str, with_type: &DataType) -> Result { + let path = path.trim_start_matches("$"); + + // Fast path: if the JSON array fields can be directly indexed into by the `path`, simply get + // the sub-array (`column_by_name`). + let mut direct = true; + let mut current = array; + for segment in path.split(".").filter(|s| !s.is_empty()) { + if matches!(current.data_type(), DataType::Binary) { + direct = false; + break; } - let path = paths.value(i); - // naively assume the JSON path is our kind of indexing to the field, by removing its "root" - let field_path = path.trim().replace("$.", ""); - let column = jsons.column_by_name(&field_path); + let Some(json) = current.as_struct_opt() else { + return exec_err!("unknown JSON array datatype: {}", current.data_type()); + }; + let Some(sub_json) = json.column_by_name(segment) else { + return Ok(new_null_array(with_type, array.len())); + }; + current = sub_json; + } - if let Some(column) = column { - builder.append_value(JsonResultValue::JsonStructByColumn(column, i))?; - } else { - let Some(raw) = jsons - .column_by_name(JsonStructureSettings::RAW_FIELD) - .and_then(|x| string_array_value_at_index(x, i)) - else { + // Build the result array with optional value mapper. + fn build_with(input: &ArrayRef, with_type: &DataType, value_mapper: F) -> Result + where + for<'a> F: Fn(&'a Value) -> Option<&'a Value>, + { + let json_array = JsonArray::from(input); + + let mut builder = result_builder(input.len(), with_type)?; + for i in 0..input.len() { + if input.is_null(i) { builder.append_null(); continue; - }; + } - let path: JsonPath = JsonPath::try_from(path).map_err(|e| { - DataFusionError::Execution(format!("{path} is not a valid JSON path: {e}")) - })?; - // the wanted field is not retrievable from the JSON struct columns directly, we have - // to combine everything (columns and the "_raw") into a complete JSON value to find it - let value = json_struct_to_value(raw, jsons, i)?; + let value = json_array + .try_get_value(i) + .map_err(|e| exec_datafusion_err!("{e}"))?; + let value = value_mapper(&value); - match path.find(&value) { - Value::Null => builder.append_null(), - Value::Array(values) => match values.as_slice() { - [] => builder.append_null(), - [x] => builder.append_value(JsonResultValue::JsonStructByValue(x))?, - _ => builder.append_value(JsonResultValue::JsonStructByValue(&value))?, - }, - value => builder.append_value(JsonResultValue::JsonStructByValue(&value))?, + if let Some(value) = value { + builder.append_value(JsonResultValue::JsonStructByValue(value))?; + } else { + builder.append_null(); } } + Ok(builder.build()) } - Ok(()) -} - -fn json_struct_to_value(raw: &str, jsons: &StructArray, i: usize) -> Result { - let Ok(mut json) = Value::from_str(raw) else { - return Err(DataFusionError::Internal(format!( - "inner field '{}' is not a valid JSON string", - JsonStructureSettings::RAW_FIELD - ))); - }; - - for (column_name, column) in jsons.column_names().into_iter().zip(jsons.columns()) { - if column_name == JsonStructureSettings::RAW_FIELD { - continue; - } - - let (json_pointer, field) = if let Some((json_object, field)) = column_name.rsplit_once(".") - { - let json_pointer = format!("/{}", json_object.replace(".", "/")); - (json_pointer, field) - } else { - ("".to_string(), column_name) - }; - let Some(json_object) = json - .pointer_mut(&json_pointer) - .and_then(|x| x.as_object_mut()) - else { - return Err(DataFusionError::Internal(format!( - "value at JSON pointer '{}' is not an object", - json_pointer - ))); - }; - - macro_rules! insert { - ($column: ident, $i: ident, $json_object: ident, $field: ident) => {{ - if let Some(value) = $column - .is_valid($i) - .then(|| serde_json::Value::from($column.value($i))) - { - $json_object.insert($field.to_string(), value); + if direct { + let casted = if current.data_type() != with_type { + match (current.data_type(), with_type) { + (DataType::Binary, _) => { + // Fall back to the slow path if the found JSON sub-array is serialized to bytes + // (because of JSON type conflicting) + build_with(current, with_type, |v| Some(v))? } - }}; - } - - match column.data_type() { - // boolean => Value::Bool - DataType::Boolean => { - let column = column.as_boolean(); - insert!(column, i, json_object, field); + (DataType::List(_) | DataType::Struct(_), with_type) if with_type.is_string() => { + // Special handle for wanted array is string (Arrow cast is not working here if + // the datatype is list or struct), because it could be used in displaying the + // result. + build_with(current, with_type, |v| Some(v))? + } + (_, with_type) if with_type.is_string() => { + // Same special handle for wanted array is string as above, except for simply + // casting by Arrow is more desirable. + arrow_cast::cast(current.as_ref(), with_type)? + } + _ => new_null_array(with_type, current.len()), } - // int => Value::Number - DataType::Int64 => { - let column = column.as_primitive::(); - insert!(column, i, json_object, field); - } - DataType::UInt64 => { - let column = column.as_primitive::(); - insert!(column, i, json_object, field); - } - DataType::Float64 => { - let column = column.as_primitive::(); - insert!(column, i, json_object, field); - } - // string => Value::String - DataType::Utf8 => { - let column = column.as_string::(); - insert!(column, i, json_object, field); - } - DataType::LargeUtf8 => { - let column = column.as_string::(); - insert!(column, i, json_object, field); - } - DataType::Utf8View => { - let column = column.as_string_view(); - insert!(column, i, json_object, field); - } - // other => Value::Array and Value::Object - _ => { - return Err(DataFusionError::NotImplemented(format!( - "{} is not yet supported to be executed with field {} of datatype {}", - JsonGetString::NAME, - column_name, - column.data_type() - ))); - } - } + } else { + current.clone() + }; + return Ok(casted); } - Ok(json) + + // Slow path: reconstruct the JSON array from serialized representation of conflicting JSON + // values: `serde_json::Value`. + let mut pointer = path.replace(".", "/"); + if !pointer.starts_with("/") { + pointer = format!("/{}", pointer); + } + build_with(array, with_type, |value| value.pointer(&pointer)) } /// This function is mostly called as `json_get(value, 'attr')::type` and rewritten by @@ -513,12 +423,12 @@ fn json_struct_to_value(raw: &str, jsons: &StructArray, i: usize) -> Result datafusion_common::Result { - let [arg0, arg1, _] = extract_args(self.name(), &args)?; + let args_len = args.args.len(); + if args_len != 2 && args_len != 3 { + return exec_err!("json_get expects 2 or 3 arguments, got {args_len}"); + } + + let arg0 = args.args[0].to_array(args.number_rows)?; let len = arg0.len(); - let arg1 = compute::cast(&arg1, &DataType::Utf8View)?; - let paths = arg1.as_string_view(); - - // mapping datatypes returned from return_field_from_args - let mut builder: Box = match args.return_field.data_type() { - DataType::Utf8View => { - Box::new(StringResultBuilder(StringViewBuilder::with_capacity(len))) - } - DataType::Int64 => Box::new(IntResultBuilder(Int64Builder::with_capacity(len))), - DataType::Float64 => Box::new(FloatResultBuilder(Float64Builder::with_capacity(len))), - DataType::Boolean => Box::new(BoolResultBuilder(BooleanBuilder::with_capacity(len))), - _type => { - return Err(DataFusionError::Internal(format!( - "Unsupported return type {}", - _type - ))); - } + let path = if let ColumnarValue::Scalar(path) = &args.args[1] + && let Some(Some(path)) = path.try_as_str() + { + path + } else { + return exec_err!( + r#"json_get expects a string literal "path" argument, got {}"#, + args.args[1] + ); }; - match arg0.data_type() { + let with_type = args + .args + .get(2) + .map(|x| x.data_type()) + .unwrap_or(DataType::Utf8View); + + let result = match arg0.data_type() { DataType::Binary | DataType::LargeBinary | DataType::BinaryView => { let arg0 = compute::cast(&arg0, &DataType::BinaryView)?; let jsons = arg0.as_binary_view(); - jsonb_get(jsons, paths, builder.as_mut())?; - } - DataType::Struct(_) => { - let jsons = arg0.as_struct(); - json_struct_get(jsons, paths, builder.as_mut())?; + + let mut builder = result_builder(len, &with_type)?; + jsonb_get(jsons, path, builder.as_mut())?; + builder.build() } + DataType::Struct(_) => json_struct_get(&arg0, path, &with_type)?, _ => { - return Err(DataFusionError::Execution(format!( - "JSON_GET not supported argument type {}", - arg0.data_type(), - ))); + return exec_err!("JSON_GET not supported argument type {}", arg0.data_type()); } }; - Ok(ColumnarValue::Array(builder.build())) + Ok(ColumnarValue::Array(result)) } } @@ -686,8 +596,8 @@ impl Function for JsonGetObject { mod tests { use std::sync::Arc; - use arrow::array::{Float64Array, Int64Array, StructArray}; - use arrow_schema::Field; + use arrow::array::{BooleanArray, Int64Array, StructArray}; + use arrow_schema::{Field, Fields}; use datafusion_common::ScalarValue; use datafusion_common::arrow::array::{BinaryArray, BinaryViewArray, StringArray}; use datafusion_common::arrow::datatypes::{Float64Type, Int64Type}; @@ -712,29 +622,35 @@ mod tests { /// } /// ``` fn test_json_struct() -> ArrayRef { + let payload_fields = Fields::from(vec![ + Field::new("code", DataType::Int64, true), + Field::new("success", DataType::Boolean, true), + Field::new("result", DataType::Binary, true), + ]); Arc::new(StructArray::new( vec![ Field::new("kind", DataType::Utf8, true), - Field::new("payload.code", DataType::Int64, true), - Field::new("payload.result.time_cost", DataType::Float64, true), - Field::new(JsonStructureSettings::RAW_FIELD, DataType::Utf8View, true), + Field::new("payload", DataType::Struct(payload_fields.clone()), true), ] .into(), vec![ Arc::new(StringArray::from_iter([Some("foo")])) as ArrayRef, - Arc::new(Int64Array::from_iter([Some(404)])), - Arc::new(Float64Array::from_iter([Some(1.234)])), - Arc::new(StringViewArray::from_iter([Some( - json! ({ - "payload": { - "success": false, - "result": { - "error": "not found" - } - } - }) - .to_string(), - )])), + Arc::new(StructArray::new( + payload_fields, + vec![ + Arc::new(Int64Array::from_iter([Some(404)])) as ArrayRef, + Arc::new(BooleanArray::from_iter([Some(false)])), + Arc::new(BinaryArray::from_iter([Some( + json!({ + "error": "not found", + "time_cost": 1.234 + }) + .to_string() + .as_bytes(), + )])), + ], + None, + )), ], None, )) @@ -1156,7 +1072,7 @@ mod tests { args: vec![ ColumnarValue::Array(json.clone()), ColumnarValue::Scalar(path.into()), - ColumnarValue::Scalar(ScalarValue::Utf8(Some("string".to_string()))), + ColumnarValue::Scalar(ScalarValue::Utf8View(None)), ], arg_fields: vec![], number_rows: 1, @@ -1194,7 +1110,7 @@ mod tests { args: vec![ ColumnarValue::Array(json), ColumnarValue::Scalar((*path).into()), - ColumnarValue::Scalar(ScalarValue::Utf8(Some("int".to_string()))), + ColumnarValue::Scalar(ScalarValue::Int64(None)), ], arg_fields: vec![], number_rows: 1, @@ -1232,7 +1148,7 @@ mod tests { args: vec![ ColumnarValue::Array(json), ColumnarValue::Scalar((*path).into()), - ColumnarValue::Scalar(ScalarValue::Utf8(Some("float".to_string()))), + ColumnarValue::Scalar(ScalarValue::Float64(None)), ], arg_fields: vec![], number_rows: 1, @@ -1270,7 +1186,7 @@ mod tests { args: vec![ ColumnarValue::Array(json), ColumnarValue::Scalar((*path).into()), - ColumnarValue::Scalar(ScalarValue::Utf8(Some("bool".to_string()))), + ColumnarValue::Scalar(ScalarValue::Boolean(None)), ], arg_fields: vec![], number_rows: 1, diff --git a/src/common/function/src/scalars/json/json_get_rewriter.rs b/src/common/function/src/scalars/json/json_get_rewriter.rs index 69cea9d443..137b307412 100644 --- a/src/common/function/src/scalars/json/json_get_rewriter.rs +++ b/src/common/function/src/scalars/json/json_get_rewriter.rs @@ -40,92 +40,111 @@ impl FunctionRewrite for JsonGetRewriter { _schema: &DFSchema, _config: &ConfigOptions, ) -> Result> { - let transform = match &expr { - Expr::Cast(cast) => rewrite_json_get_cast(cast), - Expr::ScalarFunction(scalar_func) => rewrite_arrow_cast_json_get(scalar_func), - _ => None, - }; - Ok(transform.unwrap_or_else(|| Transformed::no(expr))) + Ok(match expr { + Expr::Cast(cast) => inject_type_from_cast_expr(cast)?, + Expr::ScalarFunction(cast) => inject_type_from_cast_func(cast)?, + expr => Transformed::no(expr), + }) } } -fn is_json_get_function_call(scalar_func: &ScalarFunction) -> bool { - scalar_func.func.name().to_ascii_lowercase() == JsonGetWithType::NAME - && scalar_func.args.len() == 2 +// Expr::Cast( +// Expr::ScalarFunction( +// json_get(column, path), +// +// ) +// ) +// => +// Expr::ScalarFunction( +// json_get(column, path, ) +// ) +fn inject_type_from_cast_expr(cast: Cast) -> Result> { + let Cast { expr, data_type } = cast; + + let mut json_get = match *expr { + Expr::ScalarFunction(f) + if f.func.name().eq_ignore_ascii_case(JsonGetWithType::NAME) && f.args.len() == 2 => + { + f + } + expr => { + return Ok(Transformed::no(Expr::Cast(Cast { + expr: Box::new(expr), + data_type, + }))); + } + }; + + let with_type = ScalarValue::try_new_null(&data_type).map(|x| Expr::Literal(x, None))?; + json_get.args.push(with_type); + Ok(Transformed::yes(Expr::ScalarFunction(json_get))) } -fn rewrite_json_get_cast(cast: &Cast) -> Option> { - let scalar_func = extract_scalar_function(&cast.expr)?; - if is_json_get_function_call(scalar_func) { - let null_expr = Expr::Literal(ScalarValue::Null, None); - let null_cast = Expr::Cast(datafusion::logical_expr::expr::Cast { - expr: Box::new(null_expr), - data_type: cast.data_type.clone(), - }); +// Expr::ScalarFunction( +// arrow_cast( +// Expr::ScalarFunction( +// json_get(column, path), +// ), +// +// ) +// ) +// => +// Expr::ScalarFunction( +// json_get(column, path, ) +// ) +fn inject_type_from_cast_func(cast: ScalarFunction) -> Result> { + let ScalarFunction { func, args } = cast; - let mut args = scalar_func.args.clone(); - args.push(null_cast); - - Some(Transformed::yes(Expr::ScalarFunction(ScalarFunction { - func: scalar_func.func.clone(), - args, - }))) - } else { - None - } -} - -// Handle Arrow cast function: cast(json_get(a, 'path'), 'Int64') -fn rewrite_arrow_cast_json_get(scalar_func: &ScalarFunction) -> Option> { // Check if this is an Arrow cast function // The function name might be "arrow_cast" or similar - let func_name = scalar_func.func.name().to_ascii_lowercase(); + let func_name = func.name().to_ascii_lowercase(); if !func_name.contains("arrow_cast") { - return None; + let original = Expr::ScalarFunction(ScalarFunction { func, args }); + return Ok(Transformed::no(original)); } // Arrow cast function should have exactly 2 arguments: // 1. The expression to cast (could be json_get) // 2. The target type as a string literal - if scalar_func.args.len() != 2 { - return None; + if args.len() != 2 { + let original = Expr::ScalarFunction(ScalarFunction { func, args }); + return Ok(Transformed::no(original)); } + let [arg0, arg1] = args.try_into().unwrap_or_else(|_| unreachable!()); - // Extract the inner json_get function - let json_get_func = extract_scalar_function(&scalar_func.args[0])?; - - // Check if it's a json_get function - if is_json_get_function_call(json_get_func) { - // Get the target type from the second argument - let target_type = extract_string_literal(&scalar_func.args[1])?; - let data_type = parse_data_type_from_string(&target_type)?; - - // Create the null expression with the same type - let null_expr = Expr::Literal(ScalarValue::Null, None); - let null_cast = Expr::Cast(datafusion::logical_expr::expr::Cast { - expr: Box::new(null_expr), - data_type, + let Some(with_type) = arg1 + .as_literal() + .and_then(|x| x.try_as_str()) + .flatten() + .and_then(parse_data_type_from_string) + else { + let original = Expr::ScalarFunction(ScalarFunction { + func, + args: vec![arg0, arg1], }); + return Ok(Transformed::no(original)); + }; - // Create the new json_get_with_type function with the null parameter - let mut args = json_get_func.args.clone(); - args.push(null_cast); + let mut json_get = match arg0 { + Expr::ScalarFunction(f) + if f.func.name().eq_ignore_ascii_case(JsonGetWithType::NAME) && f.args.len() == 2 => + { + f + } + arg0 => { + let original = Expr::ScalarFunction(ScalarFunction { + func, + args: vec![arg0, arg1], + }); + return Ok(Transformed::no(original)); + } + }; - Some(Transformed::yes(Expr::ScalarFunction(ScalarFunction { - func: json_get_func.func.clone(), - args, - }))) - } else { - None - } -} + let with_type = ScalarValue::try_new_null(&with_type).map(|x| Expr::Literal(x, None))?; + json_get.args.push(with_type); -// Extract string literal from an expression -fn extract_string_literal(expr: &Expr) -> Option { - match expr { - Expr::Literal(ScalarValue::Utf8(Some(s)), _) => Some(s.clone()), - _ => None, - } + let rewritten = Expr::ScalarFunction(json_get); + Ok(Transformed::yes(rewritten)) } // Parse a data type from a string representation @@ -149,13 +168,6 @@ fn parse_data_type_from_string(type_str: &str) -> Option { } } -fn extract_scalar_function(expr: &Expr) -> Option<&ScalarFunction> { - match expr { - Expr::ScalarFunction(func) => Some(func), - _ => None, - } -} - #[cfg(test)] mod tests { use arrow_schema::DataType; @@ -221,12 +233,8 @@ mod tests { // Third argument should be a null cast to Int8 match &func.args[2] { - Expr::Cast(Cast { expr, data_type }) => { - assert_eq!(*data_type, DataType::Int8); - match expr.as_ref() { - Expr::Literal(ScalarValue::Null, _) => {} - _ => panic!("Third argument should be a null cast"), - } + Expr::Literal(value, _) => { + assert_eq!(value.data_type(), DataType::Int8); } _ => panic!("Third argument should be a cast expression"), } @@ -314,12 +322,8 @@ mod tests { // Third argument should be a null cast to Int64 match &func.args[2] { - Expr::Cast(Cast { expr, data_type }) => { - assert_eq!(*data_type, DataType::Int64); - match expr.as_ref() { - Expr::Literal(ScalarValue::Null, _) => {} - _ => panic!("Third argument should be a null cast"), - } + Expr::Literal(value, _) => { + assert_eq!(value.data_type(), DataType::Int64); } _ => panic!("Third argument should be a cast expression"), } diff --git a/src/common/function/src/scalars/json/json_to_string.rs b/src/common/function/src/scalars/json/json_to_string.rs index 6c0cc260b2..6364dff4de 100644 --- a/src/common/function/src/scalars/json/json_to_string.rs +++ b/src/common/function/src/scalars/json/json_to_string.rs @@ -19,6 +19,7 @@ use datafusion_common::DataFusionError; use datafusion_common::arrow::array::{Array, AsArray, StringViewBuilder}; use datafusion_common::arrow::datatypes::DataType; use datafusion_expr::{ColumnarValue, ScalarFunctionArgs, Signature, Volatility}; +use datatypes::types::jsonb_to_string; use crate::function::{Function, extract_args}; @@ -74,7 +75,7 @@ impl Function for JsonToStringFunction { for i in 0..size { let json = jsons.is_valid(i).then(|| jsons.value(i)); let result = json - .map(|json| jsonb::from_slice(json).map(|x| x.to_string())) + .map(jsonb_to_string) .transpose() .map_err(|e| DataFusionError::Execution(format!("invalid json binary: {e}")))?; diff --git a/src/common/function/src/scalars/matches.rs b/src/common/function/src/scalars/matches.rs index 821a1b0581..b5de60dc85 100644 --- a/src/common/function/src/scalars/matches.rs +++ b/src/common/function/src/scalars/matches.rs @@ -794,16 +794,12 @@ impl Tokenizer { is_quote_present = true; break; } - ' ' => { - if !is_quoted { - break; - } + ' ' if !is_quoted => { + break; } - '(' | ')' | '+' | '-' => { - if !is_quoted { - self.rewind_one(); - break; - } + '(' | ')' | '+' | '-' if !is_quoted => { + self.rewind_one(); + break; } '\\' => { let Some(next) = self.consume_next(pattern) else { diff --git a/src/common/function/src/scalars/matches_term.rs b/src/common/function/src/scalars/matches_term.rs index 8dfb25cbc0..ec1b34d408 100644 --- a/src/common/function/src/scalars/matches_term.rs +++ b/src/common/function/src/scalars/matches_term.rs @@ -20,6 +20,8 @@ use datafusion_common::arrow::compute; use datafusion_common::arrow::datatypes::DataType; use datafusion_common::{DataFusionError, ScalarValue}; use datafusion_expr::{ColumnarValue, ScalarFunctionArgs, Signature, Volatility}; +use icu_properties::props::Script; +use icu_properties::{CodePointMapData, CodePointMapDataBorrowed}; use memchr::memmem; use crate::function::Function; @@ -27,10 +29,11 @@ use crate::function_registry::FunctionRegistry; /// Exact term/phrase matching function for text columns. /// -/// This function checks if a text column contains exact term/phrase matches -/// with non-alphanumeric boundaries. Designed for: -/// - Whole-word matching (e.g. "cat" in "cat!" but not in "category") +/// This function uses script-aware matching rules: +/// - ASCII-only terms keep whole-word style boundary matching, like Whole-word matching (e.g. "cat" in "cat!" but not in "category") /// - Phrase matching (e.g. "hello world" in "note:hello world!") +/// - Terms containing Han characters match as contiguous substrings +/// - Mixed-script identifiers and numeric terms remain searchable in Chinese text /// /// # Signature /// `matches_term(text: String, term: String) -> Boolean` @@ -43,9 +46,8 @@ use crate::function_registry::FunctionRegistry; /// BooleanVector where each element indicates if the corresponding text /// contains an exact match of the term, following these rules: /// 1. Exact substring match found (case-sensitive) -/// 2. Match boundaries are either: -/// - Start/end of text -/// - Any non-alphanumeric character (including spaces, hyphens, punctuation, etc.) +/// 2. For ASCII-only terms, adjacent ASCII word characters block the match +/// 3. For Han-containing terms, contiguous substring match is sufficient /// /// # Examples /// ``` @@ -60,6 +62,9 @@ use crate::function_registry::FunctionRegistry; /// SELECT matches_term(column, 'critical error') FROM logs; /// -- Match in: "ERROR:critical error!" /// -- No match: "critical_errors" +/// -- Chinese substring examples -- +/// SELECT matches_term(column, '手机') FROM table; +/// -- Text: "登录手机号18888888888的动态key" => true /// /// -- Empty string handling -- /// SELECT matches_term(column, '') FROM table; @@ -204,9 +209,8 @@ impl Function for MatchesTermFunction { /// /// A term is considered matched when: /// 1. The exact sequence appears in the text -/// 2. It is either: -/// - At the start/end of text with adjacent non-alphanumeric character -/// - Surrounded by non-alphanumeric characters +/// 2. ASCII-only terms are not adjacent to ASCII word characters +/// 3. Han-containing terms match as contiguous substrings /// /// # Examples /// ``` @@ -215,28 +219,105 @@ impl Function for MatchesTermFunction { /// assert!(finder.find("dog,cat")); // Term preceded by comma /// assert!(!finder.find("category")); // Partial match rejected /// -/// let finder = MatchesTermFinder::new("world"); -/// assert!(finder.find("hello-world")); // Hyphen boundary +/// let finder = MatchesTermFinder::new("手机"); +/// assert!(finder.find("登录手机号18888888888的动态key")); /// ``` #[derive(Clone, Debug)] pub struct MatchesTermFinder { finder: memmem::Finder<'static>, term: String, - starts_with_non_alnum: bool, - ends_with_non_alnum: bool, + term_kind: TermKind, + starts_with_other: bool, + ends_with_other: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum CharClass { + AsciiWord, + Han, + UnicodeWord, + Other, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum TermKind { + AsciiLike, + UnicodeWord, + HanContaining, +} + +fn classify_char(c: char) -> CharClass { + if c.is_ascii_alphanumeric() { + CharClass::AsciiWord + } else if is_han(c) { + CharClass::Han + } else if c.is_alphanumeric() { + CharClass::UnicodeWord + } else { + CharClass::Other + } +} + +static HAN_SCRIPT_DATA: CodePointMapDataBorrowed<'static, Script> = + CodePointMapData::