diff --git a/.asf.yaml b/.asf.yaml
index 5fc08c541fca..bfb637fc4d2d 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -33,7 +33,22 @@ github:
- HDFS
- RATIS
enabled_merge_buttons:
- squash: true
+ squash: true
squash_commit_message: PR_TITLE
- merge: false
- rebase: false
+ merge: false
+ rebase: false
+ copilot_code_review:
+ enabled: true
+ review_drafts: true
+ rulesets:
+ - name: "Default Branch Protection"
+ type: branch
+ branches:
+ includes:
+ - "~DEFAULT_BRANCH"
+ - "ozone-*"
+ excludes: []
+ bypass_teams:
+ - root
+ restrict_deletion: true
+ restrict_force_push: true
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index e4304a873bf0..3bdf2a0dbfa5 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -15,23 +15,69 @@
version: 2
updates:
- package-ecosystem: maven
+ open-pull-requests-limit: 5
directory: "/"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
+ - dependency-name: "com.fasterxml.jackson:*"
+ versions:
+ - "2.22" # not LTS
+ - dependency-name: "com.github.ekryd.sortpom:sortpom-maven-plugin"
+ versions:
+ - ">=3.1.0" # requires Java 11
+ - dependency-name: "com.github.spotbugs:spotbugs-maven-plugin"
+ versions:
+ - ">=4.0.0.0" # too many new warnings
+ - ">=4.9.0.0" # requires Java 11
+ - dependency-name: "com.googlecode.maven-download-plugin:download-maven-plugin"
+ versions:
+ - ">=1.10.0" # requires Java 11
+ - dependency-name: "dev.langchain4j:*"
+ versions:
+ - ">=0.36.0" # requires Java 17
+ - dependency-name: "info.picocli:*"
+ versions:
+ - "4.7.6" # bug https://github.com/remkop/picocli/issues/2309
+ - "4.7.7" # bug https://github.com/remkop/picocli/issues/2407
+ - dependency-name: "io.netty:*"
+ update-types: ["version-update:semver-minor"]
+ - dependency-name: "org.apache.derby:derby"
+ versions:
+ - "<10.17.1.0" # CVE-2022-46337 https://issues.apache.org/jira/browse/DERBY-7147
+ - ">=10.17.1.0" # requires Java 21
+ - dependency-name: "org.apache.hadoop:*" # using multiple versions
+ - dependency-name: "org.apache.iceberg:*"
+ versions:
+ - ">=1.11.0" # requires Java 17
+ - dependency-name: "org.apache.parquet:*" # update in sync with iceberg
+ - dependency-name: "org.apache.rat:apache-rat-plugin"
+ versions:
+ - "0.17" # bug https://issues.apache.org/jira/browse/RAT-476
+ - ">=0.18" # requires Java 17
+ - dependency-name: "org.aspectj:*" # using multiple versions
+ - dependency-name: "org.jgrapht"
+ versions:
+ - ">=1.5.0" # requires Java 11
+ - dependency-name: "org.jooq:*"
+ update-types: ["version-update:semver-minor"]
+ - dependency-name: "org.mockito:mockito-core"
+ versions:
+ - ">=5.0.0" # requires Java 11
+ - dependency-name: "org.rocksdb:*"
+ update-types: ["version-update:semver-minor"]
schedule:
- interval: "weekly"
- day: "saturday"
- time: "07:00" # UTC
+ interval: "cron"
+ cronjob: "0 5 * * 0,6"
cooldown:
default-days: 7
pull-request-branch-name:
separator: "-"
- package-ecosystem: "github-actions"
+ open-pull-requests-limit: 5
directory: "/"
schedule:
- # 'daily' only runs on weekdays
interval: "cron"
- cronjob: "15 6 * * *"
+ cronjob: "0 6 * * 0,6"
cooldown:
default-days: 7
diff --git a/.github/workflows/asf-allowlist-check.yaml b/.github/workflows/asf-allowlist-check.yaml
new file mode 100644
index 000000000000..16434773f325
--- /dev/null
+++ b/.github/workflows/asf-allowlist-check.yaml
@@ -0,0 +1,49 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+name: "ASF Allowlist Check"
+
+on:
+ workflow_dispatch:
+ pull_request:
+ paths:
+ - ".github/workflows/**"
+ push:
+ branches-ignore:
+ - 'dependabot/**'
+ tags:
+ - '**'
+ paths:
+ - ".github/workflows/**"
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || case(github.repository_owner == 'apache', github.sha, github.ref_name) }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' || github.repository_owner != 'apache' }}
+
+jobs:
+ asf-allowlist-check:
+ runs-on: ubuntu-slim
+ steps:
+ - name: Checkout project
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - name: Check actions
+ uses: apache/infrastructure-actions/allowlist-check@775350a154e610e84c460cb1bbe2d2ab26c15cb3
+ with:
+ scan-glob: ".github/workflows/*.y*ml"
diff --git a/.github/workflows/build-ratis.yml b/.github/workflows/build-ratis.yml
index f5c4e7c539a1..1bbd8bc37cf9 100644
--- a/.github/workflows/build-ratis.yml
+++ b/.github/workflows/build-ratis.yml
@@ -54,6 +54,9 @@ on:
protobuf-version:
description: "Protobuf Version"
value: ${{ jobs.ratis-thirdparty.outputs.protobuf-version }}
+ build-args:
+ description: "Arguments for building with Ratis"
+ value: ${{ jobs.summary.outputs.build-args }}
env:
MAVEN_OPTS: -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.http.retryHandler.class=standard -Dmaven.wagon.http.retryHandler.count=3
permissions: { }
@@ -66,23 +69,23 @@ jobs:
thirdparty-version: ${{ steps.versions.outputs.thirdparty }}
steps:
- name: Checkout project
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
repository: ${{ inputs.repo }}
ref: ${{ inputs.ref }}
- name: Cache for maven dependencies
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.m2/repository
!~/.m2/repository/org/apache/ratis
key: ratis-dependencies-${{ hashFiles('**/pom.xml') }}
- name: Setup java
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: 'temurin'
- java-version: 8
+ java-version: 25
- name: Get component versions
id: versions
run: |
@@ -115,7 +118,7 @@ jobs:
protobuf-version: ${{ steps.versions.outputs.protobuf }}
steps:
- name: Checkout project
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
repository: apache/ratis-thirdparty
@@ -126,19 +129,30 @@ jobs:
echo "grpc=$(mvn help:evaluate -N -q -DforceStdout -Dscan=false -Dexpression=shaded.grpc.version)" >> $GITHUB_OUTPUT
echo "netty=$(mvn help:evaluate -N -q -DforceStdout -Dscan=false -Dexpression=shaded.netty.version)" >> $GITHUB_OUTPUT
echo "protobuf=$(mvn help:evaluate -N -q -DforceStdout -Dscan=false -Dexpression=shaded.protobuf.version)" >> $GITHUB_OUTPUT
- debug:
+ summary:
runs-on: ubuntu-slim
needs:
- ratis
- ratis-thirdparty
+ outputs:
+ build-args: ${{ steps.versions.outputs.build-args }}
steps:
- name: Print versions
+ id: versions
run: |
+ build_args="-Dratis.version=$ratis_version"
+ build_args="$build_args -Dratis.thirdparty.version=$ratis_thirdparty_version"
+ build_args="$build_args -Dratis-thirdparty.grpc.version=$grpc_version"
+ build_args="$build_args -Dratis-thirdparty.netty.version=$netty_version"
+ build_args="$build_args -Dratis-thirdparty.protobuf.version=$protobuf_version"
+ echo "build-args=$build_args" >> "$GITHUB_OUTPUT"
+
echo "Ratis: $ratis_version"
echo "Thirdparty: $ratis_thirdparty_version"
echo "Grpc: $grpc_version"
echo "Netty: $netty_version"
echo "Protobuf: $protobuf_version"
+ echo "Build args for Ozone: $build_args"
env:
ratis_version: ${{ needs.ratis.outputs.ratis-version }}
ratis_thirdparty_version: ${{ needs.ratis.outputs.thirdparty-version }}
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 927bc94dbcb8..5ff9e3bb8a28 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -153,7 +153,7 @@ jobs:
steps:
- name: Checkout project
if: ${{ !inputs.needs-ozone-source-tarball }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ inputs.sha }}
@@ -172,7 +172,7 @@ jobs:
- name: Cache for NPM dependencies
if: ${{ inputs.needs-npm-cache }}
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.pnpm-store
@@ -182,7 +182,7 @@ jobs:
- name: Cache for Maven dependencies
if: ${{ inputs.needs-maven-cache }}
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.m2/repository/*/*/*
@@ -223,7 +223,7 @@ jobs:
- name: Setup java ${{ inputs.java-version }}
if: ${{ inputs.java-version }}
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: 'temurin'
java-version: ${{ inputs.java-version }}
diff --git a/.github/workflows/ci-with-ratis.yml b/.github/workflows/ci-with-ratis.yml
index e6b2c3ef259b..6f616f92b9db 100644
--- a/.github/workflows/ci-with-ratis.yml
+++ b/.github/workflows/ci-with-ratis.yml
@@ -54,5 +54,5 @@ jobs:
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
SONARCLOUD_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }}
with:
- ratis_args: "-Dratis.version=${{ needs.ratis.outputs.ratis-version }} -Dratis.thirdparty.version=${{ needs.ratis.outputs.thirdparty-version }} -Dratis-thirdparty.grpc.version=${{ needs.ratis.outputs.grpc-version }} -Dratis-thirdparty.netty.version=${{ needs.ratis.outputs.netty-version }} -Dratis-thirdparty.protobuf.version=${{ needs.ratis.outputs.protobuf-version }}"
+ ratis_args: ${{ needs.ratis.outputs.build-args }}
ref: ${{ github.event.inputs.ref }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7ac92359ac34..0243a62f65ea 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -38,9 +38,10 @@ on:
required: false
env:
+ # BUILD_ARGS and TEST_JAVA_VERSION are duplicated in populate-cache.yml, please keep in sync
BUILD_ARGS: "-Pdist -Psrc -Dmaven.javadoc.skip=true -Drocks_tools_native"
- TEST_JAVA_VERSION: 21 # JDK version used by CI build and tests; should match the JDK version in apache/ozone-runner image
- # MAVEN_ARGS and MAVEN_OPTS are duplicated in check.yml, please keep in sync
+ TEST_JAVA_VERSION: 25 # JDK version used by CI build and tests; should match the JDK version in apache/ozone-runner image
+ # MAVEN_ARGS and MAVEN_OPTS are duplicated in check.yml and populate-cache.yml, please keep in sync
MAVEN_ARGS: --batch-mode --settings ${{ github.workspace }}/dev-support/ci/maven-settings.xml
MAVEN_OPTS: -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.http.retryHandler.class=standard -Dmaven.wagon.http.retryHandler.count=3
OZONE_WITH_COVERAGE: ${{ github.event_name == 'push' }}
@@ -69,19 +70,19 @@ jobs:
with-coverage: ${{ env.OZONE_WITH_COVERAGE }}
steps:
- name: "Checkout ${{ github.ref }} / ${{ github.sha }} (push)"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
if: github.event_name == 'push'
- name: "Checkout ${{ github.sha }} with its parent (pull request)"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
fetch-depth: 2
persist-credentials: false
if: github.event_name == 'pull_request'
- name: "Checkout ${{ inputs.ref }} given in workflow input (manual dispatch)"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref }}
persist-credentials: false
@@ -172,7 +173,7 @@ jobs:
if: needs.build-info.outputs.needs-compile == 'true'
strategy:
matrix:
- java: [ 8, 11, 17 ]
+ java: [ 8, 17, 25 ]
include:
- os: ubuntu-24.04
- java: 21
@@ -339,7 +340,7 @@ jobs:
pre-script: sudo hostname localhost
ratis-args: ${{ inputs.ratis_args }}
script: integration
- script-args: -Ptest-${{ matrix.profile }} -Drocks_tools_native
+ script-args: -Ptest-${{ matrix.profile }} -Phadoop-native-lib -Drocks_tools_native
sha: ${{ needs.build-info.outputs.sha }}
split: ${{ matrix.profile }}
timeout-minutes: 90
@@ -359,13 +360,13 @@ jobs:
- integration
steps:
- name: Checkout project
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ needs.build-info.outputs.sha }}
- name: Cache for maven dependencies
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.m2/repository/*/*/*
@@ -382,7 +383,7 @@ jobs:
mkdir -p hadoop-ozone/dist/target
tar xzvf target/artifacts/ozone-bin/ozone*.tar.gz -C hadoop-ozone/dist/target
- name: Setup java ${{ env.TEST_JAVA_VERSION }}
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: 'temurin'
java-version: ${{ env.TEST_JAVA_VERSION }}
diff --git a/.github/workflows/close-stale-prs.yaml b/.github/workflows/close-stale-prs.yaml
index 247b8c3091de..25d847b7316a 100644
--- a/.github/workflows/close-stale-prs.yaml
+++ b/.github/workflows/close-stale-prs.yaml
@@ -27,9 +27,10 @@ jobs:
runs-on: ubuntu-slim
steps:
- name: Close Stale PRs
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
+ uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
with:
stale-pr-label: 'stale'
+ exempt-pr-labels: 'design'
exempt-draft-pr: false
days-before-issue-stale: -1
days-before-pr-stale: 21
diff --git a/.github/workflows/generate-config-doc.yml b/.github/workflows/generate-config-doc.yml
index 966561121ced..a0451a7d4b67 100644
--- a/.github/workflows/generate-config-doc.yml
+++ b/.github/workflows/generate-config-doc.yml
@@ -28,13 +28,13 @@ jobs:
runs-on: ubuntu-slim
steps:
- name: Checkout project
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ inputs.sha }}
- name: Set up Python
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.x'
diff --git a/.github/workflows/intermittent-test-check.yml b/.github/workflows/intermittent-test-check.yml
index c6906bb19868..37f1f939b9a5 100644
--- a/.github/workflows/intermittent-test-check.yml
+++ b/.github/workflows/intermittent-test-check.yml
@@ -54,7 +54,7 @@ on:
required: false
java-version:
description: Java version to use
- default: '21'
+ default: '25'
required: true
env:
MAVEN_OPTS: -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.http.retryHandler.class=standard -Dmaven.wagon.http.retryHandler.count=3
@@ -78,7 +78,7 @@ jobs:
outputs:
matrix: ${{steps.generate.outputs.matrix}}
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
@@ -107,12 +107,12 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout project
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
- name: Cache for maven dependencies
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.m2/repository/*/*/*
@@ -128,30 +128,19 @@ jobs:
path: |
~/.m2/repository/org/apache/ratis
- name: Setup java
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: 'temurin'
java-version: ${{ github.event.inputs.java-version }}
- name: Build (most) of Ozone
run: |
args="-DskipRecon -DskipShade -Dmaven.javadoc.skip=true -Drocks_tools_native"
- if [[ "$RATIS_VERSION" != "" ]]; then
- args="$args -Dratis.version=$ratis_version"
- args="$args -Dratis.thirdparty.version=$ratis_thirdparty_version"
- args="$args -Dratis-thirdparty.grpc.version=$grpc_version"
- args="$args -Dratis-thirdparty.netty.version=$netty_version"
- args="$args -Dratis-thirdparty.protobuf.version=$protobuf_version"
- fi
-
+ args="$args $ratis_args"
args="$args -am -pl :$SUBMODULE"
hadoop-ozone/dev-support/checks/build.sh $args
env:
- ratis_version: ${{ needs.ratis.outputs.ratis-version }}
- ratis_thirdparty_version: ${{ needs.ratis.outputs.thirdparty-version }}
- grpc_version: ${{ needs.ratis.outputs.grpc-version }}
- netty_version: ${{ needs.ratis.outputs.netty-version }}
- protobuf_version: ${{ needs.ratis.outputs.protobuf-version }}
+ ratis_args: ${{ needs.ratis.outputs.build-args }}
- name: Store Maven repo for tests
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@@ -172,12 +161,12 @@ jobs:
split: ${{fromJson(needs.prepare-job.outputs.matrix)}} # Define splits
fail-fast: ${{ fromJson(github.event.inputs.fail-fast) }}
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
- name: Cache for maven dependencies
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.m2/repository/*/*/*
@@ -199,9 +188,8 @@ jobs:
name: ozone-repo
path: |
~/.m2/repository/org/apache/ozone
- continue-on-error: true
- name: Setup java
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: 'temurin'
java-version: ${{ github.event.inputs.java-version }}
@@ -212,14 +200,7 @@ jobs:
fi
args="-DexcludedGroups=slow|unhealthy -DskipShade -Drocks_tools_native"
- if [[ "$RATIS_VERSION" != "" ]]; then
- args="$args -Dratis.version=$ratis_version"
- args="$args -Dratis.thirdparty.version=$ratis_thirdparty_version"
- args="$args -Dratis-thirdparty.grpc.version=$grpc_version"
- args="$args -Dratis-thirdparty.netty.version=$netty_version"
- args="$args -Dratis-thirdparty.protobuf.version=$protobuf_version"
- fi
-
+ args="$args $ratis_args"
args="$args -pl :$SUBMODULE"
if [ "$TEST_METHOD" = "ALL" ]; then
@@ -231,18 +212,13 @@ jobs:
set -x
hadoop-ozone/dev-support/checks/junit.sh $args -Dtest="$TEST_CLASS#$TEST_METHOD,Abstract*Test*\$*"
fi
- continue-on-error: true
env:
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
repo_path: ${{ steps.download-ozone-repo.outputs.download-path }}
- ratis_version: ${{ needs.ratis.outputs.ratis-version }}
- ratis_thirdparty_version: ${{ needs.ratis.outputs.thirdparty-version }}
- grpc_version: ${{ needs.ratis.outputs.grpc-version }}
- netty_version: ${{ needs.ratis.outputs.netty-version }}
- protobuf_version: ${{ needs.ratis.outputs.protobuf-version }}
+ ratis_args: ${{ needs.ratis.outputs.build-args }}
- name: Summary of failures
run: hadoop-ozone/dev-support/checks/_summary.sh target/unit/summary.txt
- if: ${{ !cancelled() }}
+ if: ${{ failure() }}
- name: Archive build results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ failure() }}
diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml
index 79bd84b0d54d..2792a5ae38fa 100644
--- a/.github/workflows/label-pr.yml
+++ b/.github/workflows/label-pr.yml
@@ -37,7 +37,7 @@ jobs:
fail-fast: false
steps:
- name: "Checkout project" # required for `gh` CLI
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: |
diff --git a/.github/workflows/populate-cache.yml b/.github/workflows/populate-cache.yml
index 3f88233cdbd4..ea0939ea591b 100644
--- a/.github/workflows/populate-cache.yml
+++ b/.github/workflows/populate-cache.yml
@@ -22,6 +22,8 @@ on:
branches:
- master
- ozone-1.4
+ - ozone-2.0
+ - ozone-2.1
paths:
- 'pom.xml'
- '**/pom.xml'
@@ -31,18 +33,25 @@ on:
permissions: { }
+env:
+ # variables are duplicated from ci.yml, please keep in sync
+ BUILD_ARGS: "-Pdist -Psrc -Dmaven.javadoc.skip=true -Drocks_tools_native"
+ TEST_JAVA_VERSION: 25 # JDK version used by CI build and tests; should match the JDK version in apache/ozone-runner image
+ MAVEN_ARGS: --batch-mode --settings ${{ github.workspace }}/dev-support/ci/maven-settings.xml
+ MAVEN_OPTS: -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.http.retryHandler.class=standard -Dmaven.wagon.http.retryHandler.count=3
+
jobs:
build:
runs-on: ubuntu-24.04
steps:
- name: Checkout project
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Restore cache for Maven dependencies
id: restore-cache
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.m2/repository/*/*/*
@@ -51,10 +60,10 @@ jobs:
- name: Setup Java
if: steps.restore-cache.outputs.cache-hit != 'true'
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: 'temurin'
- java-version: 8
+ java-version: ${{ env.TEST_JAVA_VERSION }}
- name: Get NodeJS version
id: nodejs-version
@@ -64,7 +73,7 @@ jobs:
- name: Restore NodeJS tarballs
id: restore-nodejs
if: steps.restore-cache.outputs.cache-hit != 'true'
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.m2/repository/com/github/eirslett/node
key: nodejs-${{ steps.nodejs-version.outputs.nodejs-version }}
@@ -77,7 +86,18 @@ jobs:
- name: Fetch dependencies
if: steps.restore-cache.outputs.cache-hit != 'true'
- run: mvn --batch-mode --no-transfer-progress --show-version -Pgo-offline -Pdist -Drocks_tools_native clean verify
+ run: mvn $BUILD_ARGS $MAVEN_ARGS --no-transfer-progress --show-version -Pgo-offline clean verify
+
+ - name: Setup Java 8
+ if: steps.restore-cache.outputs.cache-hit != 'true'
+ uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
+ with:
+ distribution: 'temurin'
+ java-version: 8
+
+ - name: Fetch dependencies for Java 8
+ if: steps.restore-cache.outputs.cache-hit != 'true'
+ run: mvn $MAVEN_ARGS --no-transfer-progress --show-version -Pgo-offline -DskipRecon -DskipShade test-compile
- name: Delete Ozone jars from repo
if: steps.restore-cache.outputs.cache-hit != 'true'
@@ -89,7 +109,7 @@ jobs:
- name: Save cache for Maven dependencies
if: steps.restore-cache.outputs.cache-hit != 'true'
- uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.m2/repository/*/*/*
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 7a537c0d526e..0dc2c044eab1 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -25,12 +25,16 @@ on:
permissions: { }
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
jobs:
title:
runs-on: ubuntu-slim
steps:
- name: Checkout project
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check pull request title
diff --git a/.github/workflows/repeat-acceptance.yml b/.github/workflows/repeat-acceptance.yml
index efd014123ba9..3f8ec9b1a23f 100644
--- a/.github/workflows/repeat-acceptance.yml
+++ b/.github/workflows/repeat-acceptance.yml
@@ -18,7 +18,7 @@
# * "Test Suite", which should be ones of the existing suites from regular CI,
# e.g. "cert-rotation", or
# * "Test Filter", which is a regex pattern applied to filter test script's path
-# (examples: "ozone-csi", "test-vault.sh", "ozone/test-ec.sh", "test-.*-rotation.sh")
+# (examples: "ozone-ha", "test-vault.sh", "ozone/test-ec.sh", "test-.*-rotation.sh")
name: repeat-acceptance-test
on:
@@ -47,7 +47,7 @@ env:
OZONE_ACCEPTANCE_SUITE: ${{ github.event.inputs.test-suite}}
OZONE_TEST_SELECTOR: ${{ github.event.inputs.test-filter }}
FAIL_FAST: ${{ github.event.inputs.fail-fast }}
- JAVA_VERSION: 8
+ JAVA_VERSION: 25
SPLITS: ${{ github.event.inputs.splits }}
run-name: ${{ github.event_name == 'workflow_dispatch' && format('{0}[{1}]-{2}', inputs.test-suite || inputs.test-filter, inputs.ref, inputs.splits) || '' }}
permissions: { }
@@ -57,7 +57,7 @@ jobs:
outputs:
matrix: ${{steps.generate.outputs.matrix}}
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
@@ -83,12 +83,12 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout project
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
- name: Cache for npm dependencies
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.pnpm-store
@@ -97,7 +97,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-
- name: Cache for maven dependencies
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.m2/repository/*/*/*
@@ -107,7 +107,7 @@ jobs:
maven-repo-${{ hashFiles('**/pom.xml') }}
maven-repo-
- name: Setup java
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
@@ -134,7 +134,7 @@ jobs:
split: ${{ fromJson(needs.prepare-job.outputs.matrix) }}
fail-fast: ${{ fromJson(github.event.inputs.fail-fast) }}
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
diff --git a/.github/workflows/update-ozone-site-config-doc.yml b/.github/workflows/update-ozone-site-config-doc.yml
index 865445482f6f..5bc9ea2ca012 100644
--- a/.github/workflows/update-ozone-site-config-doc.yml
+++ b/.github/workflows/update-ozone-site-config-doc.yml
@@ -53,21 +53,35 @@ jobs:
- name: Checkout ozone-site repository
if: steps.check-site-repo.outputs.exists == 'true'
+ env:
+ GH_TOKEN: ${{ secrets.OZONE_WEBSITE_BUILD }}
run: |
git config --global url."https://asf-ci-deploy:${{ secrets.OZONE_WEBSITE_BUILD }}@github.com/".insteadOf "https://github.com/"
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- git clone --depth=1 --branch=master https://github.com/$REPO_OWNER/ozone-site.git ozone-site
-
- # Check if $BRANCH_NAME branch exists remotely
+ BRANCH_EXISTS=""
+ if git clone --depth=1 --branch="$BRANCH_NAME" https://github.com/$REPO_OWNER/ozone-site.git ozone-site; then
+ BRANCH_EXISTS="true"
+ else
+ git clone --depth=1 --branch=master https://github.com/$REPO_OWNER/ozone-site.git ozone-site
+ fi
+
cd ozone-site
- if git ls-remote --heads origin $BRANCH_NAME | grep -q $BRANCH_NAME; then
- echo "PR branch exists, checking it out for comparison"
- git fetch --depth=1 origin $BRANCH_NAME
- git checkout -B $BRANCH_NAME FETCH_HEAD
+
+ EXISTING_PR=$(gh pr list --repo "$REPO_OWNER/ozone-site" \
+ --head "$BRANCH_NAME" --base master --json number --jq '.[0].number' || echo "")
+ echo "EXISTING_PR=$EXISTING_PR" >> $GITHUB_ENV
+
+ if [ -n "$EXISTING_PR" ]; then
+ echo "Open PR #$EXISTING_PR exists"
+ elif [[ -n "$BRANCH_EXISTS" ]]; then
+ echo "No open PR, but branch exists, resetting to master"
+ git fetch --depth 1 origin master:master
+ git reset --hard master
else
- echo "PR branch does not exist, staying on master for comparison"
+ echo "No open PR or branch, creating from master"
+ git checkout -b "$BRANCH_NAME" master
fi
- name: Check if documentation changed
@@ -103,7 +117,7 @@ jobs:
- name: Checkout ozone repository for script access
if: steps.check-changes.outputs.changed == 'true'
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.sha }}
@@ -130,27 +144,16 @@ jobs:
if: steps.check-changes.outputs.changed == 'true'
run: |
cd ozone-site
-
- echo "Current branch: $(git branch --show-current)"
- echo "Current commit: $(git rev-parse HEAD)"
-
- # Create/reset branch at current commit
- git checkout -B "$BRANCH_NAME"
- echo "Created/reset branch $BRANCH_NAME at current commit"
-
- git add "$TARGET_FILE"
-
- # Build commit message with JIRA ID if available
+
COMMIT_MSG="[Auto] Update configuration documentation from ozone $SHA"
if [ -n "$JIRA_ID" ]; then
COMMIT_MSG="$JIRA_ID. $COMMIT_MSG"
fi
-
+
+ git add "$TARGET_FILE"
git commit -m "$COMMIT_MSG"
-
- echo "Pushing $BRANCH_NAME to origin"
- git push -f origin "$BRANCH_NAME"
-
+ git push --force-with-lease origin "$BRANCH_NAME"
+
- name: Create or update Pull Request in ozone-site
if: steps.check-changes.outputs.changed == 'true' && github.repository == 'apache/ozone'
env:
@@ -166,10 +169,6 @@ jobs:
../ozone-repo/dev-support/ci/pr_body_config_doc.sh \
"$REPO" "$WORKFLOW" "$RUN_ID" "$REF_NAME" "$SHA" "$JIRA_ID" > pr_body.txt
- # Check if PR already exists
- EXISTING_PR=$(gh pr list --repo "$REPO_OWNER/ozone-site" \
- --head "$BRANCH_NAME" --base master --json number --jq '.[0].number' || echo "")
-
if [ -n "$EXISTING_PR" ]; then
echo "Updating existing PR #$EXISTING_PR with a comment"
gh pr comment "$EXISTING_PR" --repo "$REPO_OWNER/ozone-site" --body-file pr_body.txt
diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml
index 206ff2c068bb..eefd86088af8 100644
--- a/.github/workflows/zizmor.yml
+++ b/.github/workflows/zizmor.yml
@@ -21,20 +21,30 @@ on:
- 'dependabot/**'
tags:
- '**'
+ paths:
+ - '.github/workflows/**'
pull_request:
+ paths:
+ - '.github/workflows/**'
permissions: { }
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || case(github.repository_owner == 'apache', github.sha, github.ref_name) }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' || github.repository_owner != 'apache' }}
+
jobs:
zizmor:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
permissions:
security-events: write
steps:
- name: Checkout project
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run zizmor
- uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
+ uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
+ with:
+ advanced-security: ${{ github.repository_owner == 'apache' }}
diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
index b030457e234f..c826626c7f87 100644
--- a/.mvn/extensions.xml
+++ b/.mvn/extensions.xml
@@ -24,11 +24,11 @@
com.gradledevelocity-maven-extension
- 1.22.2
+ 2.5.0com.gradlecommon-custom-user-data-maven-extension
- 2.2.0
+ 2.3.0
diff --git a/.run/CsiServer.run.xml b/.run/CsiServer.run.xml
deleted file mode 100644
index 3c153386e2a3..000000000000
--- a/.run/CsiServer.run.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000000..85d66a0fa921
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,227 @@
+# AGENTS instructions
+
+## Working Style
+
+- Prefer the smallest correct change. Do not add features, abstractions, refactors, or cleanup that were not asked for.
+- Keep diffs surgical. Every changed line should trace back to the task.
+ Do not reformat, rewrap, or rename adjacent code "while you are here".
+- Match the surrounding module before introducing a new pattern.
+ Reuse existing Ozone helpers, test scaffolding, and service abstractions where possible.
+- Reuse existing Ozone and Ratis utilities when the surrounding code already uses them.
+ Prefer extending an existing helper over duplicating logic or adding a new one-off abstraction.
+- If there are multiple reasonable interpretations, state the tradeoff and ask instead of guessing.
+- Do not wrap lines early just to make them look uniform. The checkstyle maximum (see `hadoop-hdds/dev-support/checkstyle/checkstyle.xml`) is 120 characters for Java. Use the full 120 characters before wrapping; never break a line that fits on one line.
+- Use established Ozone vocabulary in code, docs, and PR text:
+ SCM, OM, datanode, container, pipeline, volume, bucket, key, snapshot,
+ Recon, FSO, OBS, and S3 Gateway.
+ Avoid inventing new architecture terms unless the repo already uses them.
+
+## Repository Snapshot
+
+Apache Ozone is a multi-module Maven project. The root coordinates and version live in [`pom.xml`](./pom.xml).
+
+Tech stack:
+
+- Java 8 bytecode with JDK 21 runtime compatibility (see the `[21,]` profile in `pom.xml`)
+- Maven build
+- Hadoop RPC and gRPC over Protobuf
+- RocksDB for persistent metadata
+- Apache Ratis for replicated state
+- JUnit 5 for tests
+
+Two top-level aggregators:
+
+- `hadoop-hdds/`: storage layer and shared infrastructure.
+ Key submodules include `server-scm`, `container-service`, `framework`,
+ `managed-rocksdb`, and `interface-{admin,client,server}`.
+- `hadoop-ozone/`: Ozone services and clients.
+ Key submodules include `ozone-manager`, `s3gateway`, `recon`, `datanode`,
+ `dist`, `integration-test*`, and `ozonefs*`.
+
+Service boundaries:
+
+1. SCM manages containers, pipelines, and replication metadata.
+2. OM manages namespace, keys, buckets, volumes, snapshots, and most user-visible metadata.
+3. Datanodes serve container data and participate in Ratis pipelines.
+4. Recon provides observability and derived metadata views.
+5. S3 Gateway and OzoneFS expose external APIs on top of OM and HDDS services.
+
+Cross-cutting changes often span multiple layers.
+A feature or bug fix may need updates in `hadoop-hdds/interface-*`,
+server-side handling, client translation code, and integration tests.
+
+## Local Environment
+
+- Use a JDK 21 runtime locally. Source and target compatibility remain Java 8.
+- Ozone formatting conventions are shared through `.editorconfig`.
+- If Maven behaves unexpectedly, check `java -version` and `mvn -version` first.
+
+## Commands
+
+Default local build flags:
+
+- Use `-DskipShade -DskipRecon -DskipDocs` for iterative local work.
+- Drop `-DskipShade` only when you need filesystem artifacts or tests that depend on the shaded Ozone FS jar.
+- Drop `-DskipRecon` only when you are changing Recon UI or server behavior that must be built locally.
+- Drop `-DskipDocs` only when you are changing docs or doc-generation logic.
+
+Primary commands:
+
+- Iterative full build: `mvn clean install -DskipTests -DskipShade -DskipRecon -DskipDocs`
+- Full compile/verify smoke check: `mvn clean verify -DskipTests -DskipShade -DskipRecon -DskipDocs`
+- Rebuild one module and its dependencies:
+ `mvn -pl :ozone-manager -am install -DskipTests -DskipShade -DskipRecon -DskipDocs`
+- Run one unit test class: `mvn -pl :ozone-manager test -Dtest=TestOzoneManagerLock -DskipShade -DskipRecon -DskipDocs`
+- Run one unit test method:
+ `mvn -pl :ozone-manager test -Dtest=TestOzoneManagerLock#testLockingOrder -DskipShade -DskipRecon -DskipDocs`
+- Run one integration test class:
+ `mvn -pl :ozone-integration-test test -Dtest=TestOmContainerLocationCache -DskipShade -DskipRecon`
+
+CI-aligned local checks live under
+[`hadoop-ozone/dev-support/checks/`](./hadoop-ozone/dev-support/checks/).
+Prefer these when validating a change because they match CI layout and reporting:
+
+- `./hadoop-ozone/dev-support/checks/unit.sh`
+- `./hadoop-ozone/dev-support/checks/integration.sh`
+- `./hadoop-ozone/dev-support/checks/checkstyle.sh`
+- `./hadoop-ozone/dev-support/checks/rat.sh`
+- `./hadoop-ozone/dev-support/checks/author.sh`
+
+Notes:
+
+- The check scripts write results under `target//` (or `$OUTPUT_DIR`).
+- `build.sh` honors `FAIL_FAST=true`, `ITERATIONS=N`, and `OZONE_WITH_COVERAGE=true`.
+
+### Local Cluster
+
+- Build a runnable distribution when you need compose assets or a local tarball: `mvn -Pdist -DskipTests package`
+- Start the default compose cluster from
+ `hadoop-ozone/dist/target/ozone-*-SNAPSHOT/compose/ozone`:
+ `OZONE_REPLICATION_FACTOR=3 ./run.sh -d`
+- `.run/` contains IntelliJ run configurations for SCM, OM, Recon, datanodes, shells, S3 Gateway, and HA variants.
+
+## Repository Structure
+
+Key paths:
+
+- `hadoop-hdds/interface-*`: Protobuf definitions and protocol-facing interfaces
+- `hadoop-hdds/server-scm`: SCM server behavior
+- `hadoop-hdds/container-service`: datanode-side container handling
+- `hadoop-hdds/framework`: shared service infrastructure
+- `hadoop-hdds/managed-rocksdb`: RocksDB wrappers and helpers
+- `hadoop-ozone/ozone-manager`: OM request handling and namespace logic
+- `hadoop-ozone/s3gateway`: S3-compatible gateway
+- `hadoop-ozone/recon`: Recon backend and UI
+- `hadoop-ozone/datanode`: Ozone datanode service pieces outside HDDS container-service
+- `hadoop-ozone/integration-test*`: Mini-cluster and integration coverage
+- `hadoop-ozone/dist`: distribution assembly and compose definitions
+- `hadoop-ozone/dev-support/checks`: scripts that mirror CI checks
+- `.run/`: IDE launch configurations for local services and HA topologies
+
+## Change Boundaries
+
+- Keep service responsibilities separated.
+ Do not move OM logic into SCM paths, bypass existing request/response layers,
+ or introduce cross-service shortcuts just because they are convenient.
+- When changing a wire type, expect to update the Protobuf definition,
+ translators, server-side logic, and relevant compatibility or integration tests.
+- Prefer existing bucket-layout, snapshot, and upgrade abstractions over one-off conditionals.
+- Do not hand-edit generated sources or generated web artifacts when a source file or generation step exists.
+- For integration coverage, extend an existing suite, base class, or cluster provider
+ before creating a new `MiniOzoneCluster` lifecycle.
+ Reuse existing cluster utilities where practical.
+
+## Coding Standards
+
+- Use 2-space indentation and stay within 120 characters.
+- Add the Apache license header to new files unless the surrounding area is explicitly exempted by RAT configuration.
+- Do not add `@author` tags.
+- Keep comments concrete and local to the code. Avoid vague architecture prose or newly invented terminology.
+- Prefer existing helpers and utility methods over new abstractions for single-call-site use.
+- When touching code that already follows a specific local pattern,
+ stay consistent with that pattern instead of normalizing the whole file.
+
+## Testing Standards
+
+- New behavior and bug fixes should come with tests.
+- Start with the narrowest useful test:
+ - unit tests for local logic
+ - integration tests when the behavior depends on service boundaries, cluster lifecycle, storage, RPC, or upgrade flows
+- When adding integration coverage, prefer merging it into an existing suite
+ over creating a brand-new test class that spins up another cluster for similar coverage.
+- Before wrapping up a non-trivial change, run `./hadoop-ozone/dev-support/checks/checkstyle.sh`.
+- If you added files or changed license headers, run `./hadoop-ozone/dev-support/checks/rat.sh`.
+- If you touched shell tooling, run `./hadoop-ozone/dev-support/checks/bats.sh`.
+- Use `acceptance.sh` and `kubernetes.sh` only when the changed area actually depends on those environments.
+
+## Commits and PRs
+
+- Every change should map to an Apache Jira in the HDDS project.
+- Branch names usually start with the Jira ID, for example `HDDS-1234`.
+- PR titles must be `HDDS-1234. Short summary of the change`.
+- Prefer commit subjects that also start with the Jira ID when it is known,
+ for example `HDDS-1234. Fix snapshot purge regression`.
+- For larger changes, use incremental commits so reviewers can inspect the delta.
+ Do not rewrite branch history unless explicitly asked.
+- To bring a branch up to date with `master`, merge instead of rebasing: `git merge --no-edit origin/master`
+- Avoid force-push when updating a PR unless a maintainer explicitly asks for rewritten history.
+- PR descriptions should include the Jira link, the problem statement,
+ the chosen approach, and how the patch was tested.
+- When non-trivial content is generated with AI tooling,
+ disclose it in the PR description as `Generated-by: TOOL (MODEL)`.
+ See the ASF generative tooling policy.
+
+## Ask First
+
+- Large new features or design changes that may need an Ozone Enhancement Proposal
+- Large cross-module refactors that are not required for the task
+- New third-party dependencies
+- Protobuf or RPC changes with compatibility impact
+- RocksDB layout, metadata schema, or upgrade/finalization changes
+- Broad terminology or naming cleanups across many files
+
+## Never
+
+- Commit secrets, credentials, or tokens
+- Use destructive git commands unless explicitly requested
+- Hand-edit generated files when the source or generation workflow exists
+- Add unrelated cleanup, formatting churn, or speculative abstractions to the same change
+
+## References
+
+- [`CONTRIBUTING.md`](./CONTRIBUTING.md)
+- [`.github/pull_request_template.md`](./.github/pull_request_template.md)
+- [`hadoop-ozone/dev-support/checks/README.md`](./hadoop-ozone/dev-support/checks/README.md)
+- [`hadoop-hdds/dev-support/checkstyle/checkstyle.xml`](./hadoop-hdds/dev-support/checkstyle/checkstyle.xml)
+- [`dev-support/rat/rat-exclusions.txt`](./dev-support/rat/rat-exclusions.txt)
+- [Ozone Enhancement Proposals](https://ozone.apache.org/docs/next/developer-guide/project/enhancement-proposal)
+
+## Security
+
+When assessing a potential security vulnerability in Apache Ozone, complete
+these steps before drafting any report or reaching any security conclusion.
+
+### Step 1 — Read the threat model
+Read **[THREAT_MODEL.md](THREAT_MODEL.md)**: the multi-service trust boundaries,
+the **secure mode** knob, the properties provided vs. left to the operator, and
+the known non-findings.
+
+### Step 2 — Read the security policy
+Read **[SECURITY.md](SECURITY.md)** for how to report.
+
+### Key scoping facts (see THREAT_MODEL.md)
+- Ozone is a cluster of network services (S3 Gateway, OM, SCM/internal-CA,
+ Datanodes/Ratis, Recon). Roles: untrusted client, authenticated-but-
+ unauthorized user, operator, service peer, bounded-Byzantine datanode.
+- **Secure mode** (`ozone.security.enabled=true`) is load-bearing: a finding
+ that only manifests in non-secure (dev) mode is out of model (section 5a).
+- Ozone does **not** own its dependencies' security — the Kerberos KDC, Ranger
+ policy correctness, the SCM CA private key, KMS keys, and network isolation
+ are the operator's (sections 3/9/10). Route such findings there.
+- Ratis (Raft) safety holds under an honest majority; a Byzantine majority is
+ out of scope.
+- integration-test modules, and test utilities are out of scope.
+
+### Then assess
+Route the finding to exactly one disposition in **THREAT_MODEL.md section 13**,
+citing the section. If it cannot be routed, it is a `MODEL-GAP` — surface it.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 120000
index 000000000000..47dc3e3d863c
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+AGENTS.md
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ad3fa9ae8a4f..d705ccc88500 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -95,7 +95,7 @@ When creating a new jira for any kind of new feature, improvement or bug, please
## New feature development
-For large feature development changes, we use a process called "Ozone Enhancement Proposals" (OEP). This process is designed to ensure that major changes to Ozone are well-designed and have community consensus. If you are planning to propose a significant change, please read the [Ozone Enhancement Proposals](https://ozone.apache.org/docs/edge/design/ozone-enhancement-proposals.html) documentation and create a design document before you start coding. Please note that we only accept design documents in Markdown format; PDF or Google Docs are no longer accepted.
+For large feature development changes, we use a process called "Ozone Enhancement Proposals" (OEP). This process is designed to ensure that major changes to Ozone are well-designed and have community consensus. If you are planning to propose a significant change, please read the [Ozone Enhancement Proposals](https://ozone.apache.org/docs/next/developer-guide/project/enhancement-proposal) documentation and create a design document before you start coding. Please note that we only accept design documents in Markdown format; PDF or Google Docs are no longer accepted.
## Contribute your modifications
diff --git a/NOTICE.txt b/NOTICE.txt
index 6a2e7e0ab97a..6be3917ca9c7 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -1,5 +1,5 @@
Apache Ozone
-Copyright 2025 The Apache Software Foundation
+Copyright 2026 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
diff --git a/SECURITY.md b/SECURITY.md
index 25def4481064..d869e370bfd1 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -13,3 +13,13 @@ This email address is a private mailing list for discussion of potential securit
This mailing list is **NOT** for end-user questions and discussion on security. Please use the dev@ozone.apache.org list for such issues.
In order to post to the list, it is **NOT** necessary to first subscribe to it.
+
+## Threat Model
+
+A threat model for Apache Ozone is maintained in [THREAT_MODEL.md](THREAT_MODEL.md).
+It describes the multi-service trust boundaries (S3 Gateway, OM, SCM/CA,
+Datanodes/Ratis), the load-bearing role of **secure mode**
+(`ozone.security.enabled`), the properties Ozone provides versus those left to
+the operator (Kerberos KDC, Ranger policy correctness, SCM CA key, KMS, network
+isolation), and the recurring non-findings. Triagers of scanner, fuzzer, or
+AI-generated findings should route them through `THREAT_MODEL.md` section 13.
diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md
new file mode 100644
index 000000000000..69f3b9bef136
--- /dev/null
+++ b/THREAT_MODEL.md
@@ -0,0 +1,407 @@
+# Apache Ozone — Threat Model
+
+## §1 Header
+
+- **Project:** Apache Ozone (`apache/ozone`) — a distributed, scalable object
+ store (S3-compatible + Hadoop-FS) built on Hadoop Distributed Data Store
+ (HDDS).
+- **Written against:** `master` @ HEAD (2026-06).
+- **Author:** ASF Security team, via the threat-model-producer rubric (Scovetta
+ rubric) at the Ozone PMC's request (path 3, confirmed siyao@ 2026-06-02).
+- **Status:** v1 — ratified by the Ozone PMC. Maintainer review complete (Siyao Meng / smengcl and Wei-Chiu Chuang / jojochuang, 2026-06/07); wave-1–3 answers folded.
+- **Version binding:** versioned with the project; a report against version *N*
+ is triaged against the model as it stood at *N*.
+- **Reporting cross-reference:** §8-violating findings go to
+ `security@ozone.apache.org` (per [`SECURITY.md`](SECURITY.md)); §3/§9 findings
+ are closed citing this document.
+- **Provenance legend:** *(documented)* = project source/docs/`SECURITY.md`;
+ *(maintainer)* = an Ozone maintainer in this review; *(inferred)* = reasoned
+ from code/architecture — each has a §14 open question.
+- **Draft confidence:** ~24 documented / 2 maintainer / 24 inferred (smengcl confirmed Q-secure + Q-ratis, 2026-06-23).
+
+**What it is.** Ozone is a multi-daemon distributed object store. The
+**Ozone Manager (OM)** owns the namespace/metadata and can issue delegation +
+block tokens; the **Storage Container Manager (SCM)** manages blocks/containers
+and acts as the cluster's **internal Certificate Authority** (root of service
+identity); **Datanodes** store data in containers, replicate via **Ratis
+(Raft)**, and enforce block/container tokens when enabled; the **S3 Gateway**
+exposes an S3-compatible REST API to (potentially internet-facing) clients; **Recon** is a
+read-only management/monitoring service. Clients reach Ozone via the S3 API or
+the `ofs://`/`o3fs://` Hadoop filesystem over Hadoop RPC.
+
+## §2 Scope and intended use
+
+Ozone is deployed as a **cluster of network services**, not an in-process
+library. There is no single "caller"; the roles split:
+
+- **Untrusted client** — an S3 REST client or RPC client outside the cluster
+ trust boundary (the S3 Gateway may be internet-facing).
+- **Authenticated user** — a Kerberos-authenticated principal acting within
+ their granted ACLs; trusted to authenticate, **not** trusted to stay within
+ authorization (an authenticated user attempting to read another tenant's data
+ is in-model).
+- **Operator/admin** — trusted for the deployment (KDC, Ranger policies, CA key,
+ network).
+- **Service peer** — OM/SCM/Datanode talking to each other, and Datanode-to-
+ Datanode Ratis peers; authenticated via SCM-issued certificates, but a
+ **compromised datanode is a distinct (Byzantine) actor** (§7).
+
+**Component families.**
+
+| Family | Entry point | Exposure | In model? |
+| --- | --- | --- | --- |
+| S3 Gateway | S3 REST (AWS SigV4) | **untrusted / internet-facing** | **Yes** |
+| OM (namespace, tokens, ACLs) | Hadoop RPC (SASL/Kerberos) | authenticated clients | **Yes** |
+| SCM (blocks + internal CA) | Hadoop RPC + cert server | services + admin | **Yes** (CA = root of trust) |
+| Datanode (block store, Ratis) | block protocol + Ratis | token-gated clients where enabled + DN peers | **Yes** |
+| Recon | read-only HTTP/RPC | operators | **Yes** (read path) |
+| `ofs`/`o3fs` client libs | in-process in the caller's app | as the caller | client-side (§10) |
+| test/integration modules | test | n/a | No — §3 |
+
+## §3 Out of scope (explicit non-goals)
+
+- **The security-providing infrastructure Ozone depends on but does not own:**
+ the Kerberos KDC, the Ranger policy server + the *correctness of the
+ authorization policies an operator writes*, the KMS/key material for
+ transparent data encryption, and the network perimeter. Ozone consumes these;
+ hardening them is the operator's (§10). *(documented/inferred — SecureOzone,
+ SecurityWithRanger, SecuringTDE, NetworkPorts.)*
+- **`ozone-thirdparty`** — a shaded-dependency packaging repo; build artifact,
+ no runtime attack surface of its own. *(documented.)*
+- **Non-secure mode as a target.** With `ozone.security.enabled=false` Ozone
+ performs **no authentication** — it is a development/sandbox posture. Findings
+ that only manifest in non-secure mode are `OUT-OF-MODEL: non-default-build`
+ (§5a). Secure mode is confirmed as the supported production posture.
+ *(maintainer — smengcl, 2026-06-23: secure mode is the supported posture; and
+ with security enabled the S3 Gateway rejects anonymous access — no plan to
+ support intended anonymous access, [HDDS-7961](https://issues.apache.org/jira/browse/HDDS-7961).)* *(maintainer — jojochuang, 2026-06-25: the S3 user doc
+ states the secure-mode anonymous rejection only implicitly — making it
+ explicit is tracked; note a future S3 web-hosting feature would require
+ anonymous access by design, which would be a documented opt-in exception.)*
+- **Compromise of the SCM CA root key.** If the SCM CA private key is stolen, all
+ service identity collapses by design; protecting it is operational (§10/§7).
+- Test/integration modules (`integration-test-*`, `*TestImpl`).
+
+## §4 Trust boundaries and data flow
+
+Boundaries, outermost first:
+
+1. **S3 Gateway boundary** — untrusted REST clients; AWS SigV4 over per-user S3
+ secrets (derived from the user's Kerberos identity / S3 secret store).
+2. **RPC/control-plane boundary** — clients to OM/SCM over Hadoop RPC with SASL;
+ S3 Gateway may reach OM over the OM gRPC transport, and SCM HA uses internal
+ Inter-SCM gRPC for checkpoint transfer. In secure mode, authenticated via
+ Kerberos / delegation tokens and, for gRPC, TLS/mTLS where configured.
+3. **Block-access boundary** — when block/container tokens are enabled, a client
+ obtains a **block token** from OM, presents it to a Datanode, which verifies
+ the token's signature before serving the block. The Datanode does not
+ re-authenticate the user; the token *is* the capability.
+4. **Service-identity boundary** — OM/SCM/DN use **SCM-issued certificates**
+ for service identity; SCM is the CA. Transport encryption is controlled by
+ separate TLS/RPC privacy knobs (§5a).
+5. **Ratis boundary** — Datanode peers replicate via Raft; a peer holds a
+ legitimate certificate but may behave arbitrarily if compromised (§7).
+
+```
+S3 client ──SigV4──► S3 Gateway ──RPC(Kerberos)──► OM ──(block token)──► client
+RPC client ─Kerberos/deleg token─► OM (ACL check) ─► SCM (block alloc) ─► Datanode
+ Datanode verifies block token, serves block
+services ⇄ services : SCM-issued certs ; Datanodes ⇄ Datanodes : Ratis(Raft)
+```
+
+**Reachability precondition (triager's test):** a finding is in-model only if
+reachable in **secure mode** (`ozone.security.enabled=true`) from the actor that
+owns that boundary — an S3-Gateway finding from an untrusted REST client; an
+OM/SCM finding from an authenticated-but-unauthorized user; a block-access
+finding from a client without a valid token in a token-enabled deployment; a
+consensus finding from a Byzantine Datanode peer below the honest-majority
+threshold (§7).
+
+## §5 Assumptions about the environment
+
+- **Secure deployment** assumes a functioning **Kerberos KDC**, time sync,
+ DNS/rDNS, and a **Ranger** server if Ranger authz is enabled. Kerberos and
+ Ranger integration are documented; time/DNS are deployment preconditions.
+- **PKI:** SCM is the root CA; service certs are issued/rotated by SCM. The CA
+ key's secrecy is assumed. *(documented — `CertificateServer`/`CertificateClient`.)*
+- **Storage:** Datanodes trust their local disks; on-disk encryption (TDE) keys
+ come from an external KMS. *(documented — SecuringTDE.)*
+- **Network:** Ozone exposes documented OM/SCM/Recon/S3G/Datanode listeners;
+ admin/Ratis/datanode service ports are assumed reachable only by the cluster +
+ authorized clients (operator-enforced).
+- Ozone **does** open many network listeners and spawns service processes by
+ design; the "no side effects" inventory does not apply to a server.
+
+## §5a Build-time and configuration variants — **the central knob**
+
+**`ozone.security.enabled` is load-bearing.** With it **true** (secure mode):
+Kerberos authentication on RPC, delegation-token support, and SCM-issued
+certificates for service identity are in the security posture. Several
+authorization, capability, and confidentiality controls are separately
+configured: object ACL checks (`ozone.acl.enabled`), block/container tokens
+(`hdds.block.token.enabled`, `hdds.container.token.enabled`), transport
+encryption, and TDE/KMS. With it **false** (non-secure mode): **no
+authentication at all** — intended only for dev/sandbox.
+
+**The insecure-default problem (wave-1 — answered).** Secure mode **is** the
+supported production posture *(maintainer — smengcl, 2026-06-23)*: operators must
+enable it for any untrusted exposure, so non-secure-mode findings are
+`OUT-OF-MODEL: non-default-build` and §10 carries "run secure mode." For the S3
+Gateway specifically: with security enabled, **anonymous access is rejected** and
+there is no plan to support intended anonymous access
+([HDDS-7961](https://issues.apache.org/jira/browse/HDDS-7961)) — so an
+"unauthenticated S3 request is accepted" finding in secure mode is `VALID`, not a
+disclaimed mode. Other knobs that move the envelope remain deployment choices:
+object ACL authorizer, S3 secret storage backend, block/container-token
+enablement, transport encryption, and TDE/KMS.
+
+**Default authz/capability/crypto state — off by default even in secure mode**
+*(maintainer — jojochuang, 2026-06-25)*. Several controls are disabled in a
+stock install and must be explicitly enabled:
+
+- **Object ACL checks** are off by default (`ozone.acl.enabled=false`); when
+ enabled, **Native ACL is the default authorizer**. Operators can instead
+ configure the Ranger plugin as the authorizer by setting
+ `ozone.acl.authorizer.class` to
+ `org.apache.ranger.authorization.ozone.authorizer.RangerOzoneAuthorizer`.
+ Both are documented authorizers; S3 multi-tenancy setup requires Ranger.
+ *(maintainer — smengcl, 2026-07-07)*
+- **Block/container tokens** are off by default (`hdds.block.token.enabled=false`,
+ `hdds.container.token.enabled=false`). When enabled, the block/container-token
+ lifetime defaults to `hdds.block.token.expiry.time=1d`.
+- **Delegation tokens** are enabled by default when security is enabled. OM
+ delegation tokens renew every `1d` and stop renewing after `7d`.
+- **Token signing keys** are SCM-issued symmetric keys. Defaults are
+ `hdds.secret.key.expiry.duration=9d`, `hdds.secret.key.rotate.duration=1d`,
+ `hdds.secret.key.rotate.check.duration=10m`, and `HmacSHA256`.
+- **gRPC TLS** is off by default (`hdds.grpc.tls.enabled=false`) and protects
+ gRPC traffic when enabled.
+- **TDE/KMS** is optional and protects data at rest only for encrypted buckets;
+ it requires a configured KMS, for example via `hadoop.security.key.provider.path`.
+
+So a finding that assumes ACLs / block/container tokens / transport encryption /
+TDE are active in a default build is `OUT-OF-MODEL: non-default-build` unless the
+operator enabled them (§10); the §10 checklist lists these as required
+production hardening. (Answers the Q-authz / Q-token / Q-tde default-state and
+lifetime/rotation mechanism questions.)
+
+## §6 Assumptions about inputs
+
+Per-boundary input trust (grouped by family):
+
+| Boundary | Input | Attacker-controllable? | Enforced by / caller must |
+| --- | --- | --- | --- |
+| S3 Gateway | REST request, SigV4 signature, headers, object data | **yes** | gateway verifies SigV4 against the user's S3 secret |
+| OM RPC | request, Kerberos/delegation token, names | **yes (authenticated)** | auth; ACL/Ranger if enabled |
+| Datanode | block/container read/write + token | **yes** | tokens verified when enabled |
+| SCM | cert sign request, block alloc | **yes (authenticated service/admin)** | SCM verifies caller identity |
+| Ratis | replicated log entries from a DN peer | **yes if peer compromised** | Raft quorum (honest majority) |
+| TDE | object bytes | n/a (encryption is transparent) | KMS holds keys (operator) |
+
+## §7 Adversary model
+
+- **Untrusted S3/RPC client** — no valid identity; tries to access data,
+ bypass SigV4/Kerberos, or exploit the gateway. In scope.
+- **Authenticated-but-unauthorized user** — a valid Kerberos principal who
+ tries to exceed their ACLs (read another bucket, escalate). In scope —
+ authorization is the defence.
+- **On-path network attacker** — passive/active on the wire. In scope where the
+ deployment has enabled transport encryption for the relevant protocol.
+- **Authenticated-but-Byzantine Datanode peer** — a compromised Datanode holding
+ a legitimate SCM cert that then behaves arbitrarily in Ratis. In scope **up to
+ the Raft honest-majority threshold**: Ratis gives standard Raft safety under an
+ **honest majority** (e.g. 2 of 3 replicas for `RATIS THREE`) — it is **not**
+ Byzantine fault tolerant. At or beyond a Byzantine majority, divergence is
+ possible and **out of scope** (§3 / §8 conditions). Block-integrity defence is
+ partial: Ozone has **checksum verification on normal reads plus replica/container
+ checks**, so ordinary single-replica corruption is detected — but there is **no
+ full guarantee against a Byzantine datanode that forges both data and metadata
+ on the path it serves**. *(maintainer — smengcl, 2026-06-23; checksum behaviour documented in [ozone-site#397](https://github.com/apache/ozone-site/pull/397).)*
+- **Out of scope:** compromised KDC / Ranger / SCM-CA-key / operator host;
+ side-channel/co-tenant adversaries against the host; a client that an operator
+ has authorized to do the thing it did.
+
+## §8 Security properties the project provides (secure mode)
+
+1. **Authenticated RPC.** All OM/SCM/DN RPC requires Kerberos (or a valid
+ delegation token). *Violation:* unauthenticated request accepted. *Severity:*
+ critical. *(documented — SASL/Kerberos; secure-mode posture folded into §5a.)*
+2. **Capability-gated block/container access when tokens are enabled.** A
+ Datanode serves protected blocks/containers only on a valid, unexpired,
+ correctly-signed token. *Violation:* block/container read/write without a
+ valid token in a token-enabled deployment. *Severity:* critical. *(documented
+ — `BlockTokenVerifier`, `ContainerTokenVerifier`, and token configuration.)*
+3. **Authorization when object ACL/Ranger checks are enabled.** Volume/bucket/key
+ operations are checked against the configured Native ACL or Ranger authorizer.
+ *Violation:* an authenticated user accesses data outside their ACLs in an
+ ACL-enabled deployment. *Severity:* critical. *(documented — SecurityAcls,
+ SecurityWithRanger; Ranger CLI/doc gaps tracked by HDDS-4089/HDDS-2093.)*
+4. **Service identity.** Inter-service identity is backed by SCM-issued
+ certificates. *Violation:* a rogue process impersonating a service on a
+ certificate-backed path. *Severity:* critical. *(documented —
+ `CertificateClient`/`CertificateServer`.)*
+5. **Consensus safety (Ratis/Raft).** Committed metadata/data does not diverge or
+ silently lose under the **honest-majority** bound (standard Raft safety, e.g.
+ 2 of 3 for `RATIS THREE`; **not** BFT — §7). *Violation:* fork / divergent
+ replica state / acknowledged-write loss. *Severity:* critical; observable
+ across nodes. *(maintainer — smengcl, 2026-06-23.)*
+ - **Corollary — read-path integrity (partial).** Checksum verification on
+ normal reads plus replica/container checks detect ordinary single-replica
+ corruption; this does **not** extend to a Byzantine datanode that forges both
+ data and metadata on its served path (that is §9 / out of scope).
+ *(maintainer — smengcl, 2026-06-23.)*
+6. **Confidentiality on configured transports / at rest.** Hadoop RPC privacy,
+ gRPC TLS, or HTTPS protect the wire when configured; TDE protects data at
+ rest when enabled (keys in KMS). *Violation:* plaintext on a transport
+ configured for encryption / unencrypted blocks when TDE is configured.
+ *Severity:* high. *(documented — protect-in-transit-traffic, SecuringTDE.)*
+
+## §9 Security properties the project does *not* provide
+
+- **No security in non-secure mode.** With `ozone.security.enabled=false` there
+ is no authentication or token enforcement — by design, dev-only (§5a).
+ - *False friend:* a reachable, "unauthenticated" endpoint in a non-secure dev
+ cluster is **not** a vulnerability in the production (secure) posture.
+- **It does not author your authorization policy.** Ozone enforces ACLs/Ranger
+ *as configured*; an over-broad Ranger policy or world-readable ACL is an
+ operator decision, not an Ozone flaw (§10).
+- **It does not protect its dependencies.** KDC/Ranger/KMS/SCM-CA-key/network
+ security are the operator's (§3/§10).
+- **No defence against a Byzantine majority** of a Ratis ring, and **no full
+ guarantee against a single Byzantine datanode that forges both data and metadata
+ on the path it serves** — Ratis is not BFT, and while checksum + replica/container
+ checks catch ordinary corruption, a peer that can forge consistently on its
+ served path is out of model (§7). *(maintainer — smengcl, 2026-06-23.)*
+- **Block tokens are bearer capabilities** — a leaked block token grants access
+ until expiry; the caller/operator must protect tokens in transit (TLS). The
+ default block/container-token lifetime is `1d` when those tokens are enabled.
+ *(documented — token verifier/secret-manager code.)*
+- **Well-known classes left to the operator/integrator:** SSRF/credential-relay
+ via a misconfigured S3 Gateway, request smuggling at an LB in front of the
+ gateway, and authz-policy errors.
+
+## §10 Downstream responsibilities (operator + client)
+
+- **Run secure mode** (`ozone.security.enabled=true`) for any non-sandbox
+ cluster; require authentication on the S3 Gateway.
+- **Secure the dependencies:** harden/operate the KDC, author least-privilege
+ Ranger/ACL policies, protect the **SCM CA private key**, manage KMS keys,
+ network-isolate datanode/Ratis/admin ports.
+- **Protect tokens and secrets:** enable the relevant transport encryption
+ (Hadoop RPC privacy, gRPC TLS, and/or HTTPS) so block/delegation tokens and S3
+ secrets aren't sniffable; rotate S3 secrets; review token lifetimes for the
+ deployment.
+- **Protect service metadata at rest.** The OM, SCM, and Recon RocksDB stores
+ hold critical credential/identity data — set restrictive file permissions and,
+ ideally, encrypt them on disk. *(maintainer — jojochuang, 2026-06-25.)*
+- **Isolate the KMS** in a separate, firewalled network segment. *(maintainer —
+ jojochuang, 2026-06-25.)*
+- **Client side:** treat data read from Ozone per your own trust needs; protect
+ delegation tokens your app caches.
+
+A consolidated **production secure-deployment checklist** for operators is
+tracked for the Ozone docs (ozone-site) — gathering the secure-mode, ACL, token,
+TDE/KMS, metadata-protection, and network-isolation steps above into one setup
+list. *(requested by jojochuang, 2026-06-25.)*
+
+## §11 Known misuse patterns
+
+- Exposing a **non-secure** cluster (or an unauthenticated S3 Gateway) to an
+ untrusted network.
+- Treating **native ACLs** as sufficient where Ranger fine-grained authz is
+ needed (or vice-versa), or writing world-permissive policies.
+- Leaking **block/delegation tokens** over plaintext channels.
+- Co-locating Datanode/SCM admin ports on an untrusted network segment.
+
+## §11a Known non-findings (recurring false positives)
+
+- **Unauthenticated endpoint reachable with `ozone.security.enabled=false`** —
+ non-finding: non-secure mode is dev-only (§5a/§9). `OUT-OF-MODEL: non-default-build`.
+- **"An authenticated user could request a token / cert"** — non-finding when
+ it's within their identity; tokens/certs are the mechanism, trust is ACL/CA
+ scoped (§6/§8).
+- **Ranger policy too permissive** — operator policy decision, not Ozone code
+ (§9/§10). `OUT-OF-MODEL: trusted-input`.
+- **Findings in `ozone-thirdparty`, `integration-test-*`, `*TestImpl`** —
+ `OUT-OF-MODEL: unsupported-component` (§3).
+- **KDC/KMS/Ranger/SCM-CA-key compromise scenarios** — out of layer (§3/§7).
+- **Hadoop-inherited RPC/SASL "issues"** already fixed upstream — check the
+ Hadoop dependency version before reporting.
+
+## §12 Conditions that would change this model
+
+- A change to secure-mode defaults, transport-encryption defaults, the S3
+ Gateway auth requirement, or the ACL/Ranger authorizer (§5a).
+- A new network surface, a new token type, or a change to block-token
+ verification.
+- A change to the Ratis honest-majority assumptions or block integrity checks.
+- A report unroutable to a §13 disposition → revise §8/§9.
+
+## §13 Triage dispositions
+
+| Disposition | Meaning | Licensed by |
+| --- | --- | --- |
+| `VALID` | A §8 property breaks in secure mode, via an in-scope actor. | §8, §6, §7 |
+| `VALID-HARDENING` | No §8 break, but a §11 misuse is too easy to fall into. | §11 |
+| `OUT-OF-MODEL: trusted-input` | Requires control of operator config (Ranger/ACL/keys). | §6/§10 |
+| `OUT-OF-MODEL: adversary-not-in-scope` | Needs KDC/CA-key/Byzantine-majority. | §7 |
+| `OUT-OF-MODEL: non-default-build` | Only in non-secure mode (or a discouraged knob). | §5a |
+| `OUT-OF-MODEL: unsupported-component` | thirdparty / test / infra Ozone doesn't own. | §3 |
+| `BY-DESIGN: property-disclaimed` | Non-secure mode, policy correctness, dependency security. | §9 |
+| `KNOWN-NON-FINDING` | Matches §11a. | §11a |
+| `MODEL-GAP` | Unroutable. | triggers §12 |
+
+## §14 Open questions for the maintainers
+
+**Wave 1 — the load-bearing ones.**
+
+- **Q-secure.** *(Answered — maintainer, smengcl 2026-06-23: yes, secure mode is
+ the supported production posture; with security enabled the S3 Gateway rejects
+ anonymous access, no plan otherwise — [HDDS-7961](https://issues.apache.org/jira/browse/HDDS-7961). Folded into §3/§5a/§9/§11a.)*
+ Confirm secure mode (`ozone.security.enabled=true`) is the supported production
+ posture, so non-secure-mode findings are `OUT-OF-MODEL: non-default-build`. Does
+ the S3 Gateway ever support intended anonymous access? (§5a/§9/§11a/§13.)
+- **Q-roles.** Confirm the actor split (untrusted client / authenticated-
+ unauthorized user / operator / service peer / Byzantine datanode) and that
+ the in-scope adversaries are the first, second, third-on-the-wire, and the
+ bounded Byzantine peer. (§2/§7.)
+- **Q-ratis.** *(Answered — maintainer, smengcl 2026-06-23: standard Raft safety
+ under an honest majority (2 of 3 for `RATIS THREE`), not BFT; checksum +
+ replica/container checks detect ordinary single-replica corruption, but no full
+ guarantee against a Byzantine datanode forging both data and metadata on its
+ served path. Folded into §7/§8/§9.)* What is the Ratis honest-majority safety
+ bound you stand behind, and is there an independent block/container integrity
+ check so a single Byzantine datanode can't serve corrupted data undetected?
+ (§7/§8.)
+
+**Wave 2 — mechanism confirmations.**
+
+- **Q-authz.** *(Answered — maintainer, smengcl 2026-07-07: when
+ `ozone.acl.enabled=true`, **Native ACL is the default authorizer**; the Ranger
+ plugin is an opt-in alternative via
+ `ozone.acl.authorizer.class=org.apache.ranger.authorization.ozone.authorizer.RangerOzoneAuthorizer`.
+ The §8 authorization property holds for whichever authorizer is configured.
+ Folded into §5a.)* (§8.)
+- **Q-token.** Block/delegation token lifetimes, signing-key rotation, and the
+ bearer-token caveat in §9 — confirm. (§8/§9.)
+- **Q-tde / Q-net / Q-infra.** TDE/KMS production expectations, the
+ network-isolation assumptions, and which dependencies (KDC/Ranger/KMS) you want
+ explicitly named as operator-owned in §3/§10. (§5/§3/§10.)
+
+**Wave 3 — scope & coexistence.**
+
+- **Q-csi / Q-recon.** *(Answered — maintainer, jojochuang 2026-06-25: the CSI
+ driver was out of scope because it was not production-ready and was later
+ removed by [HDDS-15876](https://issues.apache.org/jira/browse/HDDS-15876).
+ Recon remains in scope as part of the production cluster. Folded into §2.)*
+- **Q-doc.** This adds `THREAT_MODEL.md` + `AGENTS.md` alongside your existing
+ `SECURITY.md` (preserved, pointer added). Confirm, and whether the model
+ should become canonical. (§1/§15.)
+
+## §15 Appendix — existing-policy back-map
+
+The repo `SECURITY.md` is a disclosure-process policy (report to
+`security@ozone.apache.org`); it embeds no threat model. This `THREAT_MODEL.md`
+is additive — `SECURITY.md` is preserved and gains a pointer. Ozone's published
+security documentation (secure-mode setup, ACLs, tokens, TDE) is a strong source
+for refining §8/§11a in a later pass.
diff --git a/dev-support/ci/maven-settings.xml b/dev-support/ci/maven-settings.xml
index 43fa07bb52ba..d274c49f5f08 100644
--- a/dev-support/ci/maven-settings.xml
+++ b/dev-support/ci/maven-settings.xml
@@ -31,5 +31,12 @@
https://repository.apache.org/content/repositories/snapshotstrue
+
+ block-jboss
+ repository.jboss.org
+ Block access to JBoss
+ https://repository.jboss.org/nexus/content/groups/public
+ true
+
diff --git a/dev-support/ci/selective_ci_checks.bats b/dev-support/ci/selective_ci_checks.bats
index 092ab497e3cd..891e04506086 100644
--- a/dev-support/ci/selective_ci_checks.bats
+++ b/dev-support/ci/selective_ci_checks.bats
@@ -187,6 +187,28 @@ load bats-assert/load.bash
assert_output -p needs-kubernetes-tests=false
}
+@test "mini-cluster" {
+ run dev-support/ci/selective_ci_checks.sh f0388a195411e68aac360de8ab95735b2eb295de
+
+ assert_output -p 'basic-checks=["rat","author","checkstyle","findbugs","pmd"]'
+ assert_output -p needs-build=true
+ assert_output -p needs-compile=true
+ assert_output -p needs-compose-tests=false
+ assert_output -p needs-integration-tests=true
+ assert_output -p needs-kubernetes-tests=false
+}
+
+@test "test-utils" {
+ run dev-support/ci/selective_ci_checks.sh 60e4ab121853c182e1272b04fd1a93d55b12cfc7
+
+ assert_output -p 'basic-checks=["rat","author","checkstyle","findbugs","pmd"]'
+ assert_output -p needs-build=true
+ assert_output -p needs-compile=true
+ assert_output -p needs-compose-tests=false
+ assert_output -p needs-integration-tests=true
+ assert_output -p needs-kubernetes-tests=false
+}
+
@test "native only" {
run dev-support/ci/selective_ci_checks.sh 5b1319a8c2
diff --git a/dev-support/ci/selective_ci_checks.sh b/dev-support/ci/selective_ci_checks.sh
index 71b61da5d8e4..57e863470771 100755
--- a/dev-support/ci/selective_ci_checks.sh
+++ b/dev-support/ci/selective_ci_checks.sh
@@ -260,12 +260,12 @@ function get_count_doc_files() {
function get_count_integration_files() {
start_end::group_start "Count integration test files"
local pattern_array=(
+ "^hadoop-hdds/test-utils"
"^hadoop-ozone/dev-support/checks/_mvn_unit_report.sh"
"^hadoop-ozone/dev-support/checks/integration.sh"
"^hadoop-ozone/dev-support/checks/junit.sh"
"^hadoop-ozone/integration-test"
"^hadoop-ozone/mini-cluster"
- "^hadoop-ozone/fault-injection-test/mini-chaos-tests"
"src/test/java"
"src/test/resources"
)
diff --git a/dev-support/ci/xml_to_md.py b/dev-support/ci/xml_to_md.py
index d299e3365e6e..c2e80dce9dc6 100644
--- a/dev-support/ci/xml_to_md.py
+++ b/dev-support/ci/xml_to_md.py
@@ -27,6 +27,15 @@
Property = namedtuple('Property', ['name', 'value', 'tag', 'description'])
+def escape_mdx_markup(text):
+ """Escapes special characters to prevent MDX/JSX parsing errors."""
+ if not text:
+ return text
+ text = text.replace('&', '&')
+ text = text.replace('<', '<')
+ text = text.replace('>', '>')
+ return text
+
def extract_xml_from_jar(jar_path, xml_filename):
xml_files = []
with zipfile.ZipFile(jar_path, 'r') as jar:
@@ -120,6 +129,7 @@ def generate_markdown(properties):
# Escape pipe characters and wrap {placeholders} in backticks
description = prop.description.replace('|', '\\|')
description = placeholder_pattern.sub(r'`\1{\2}`', description)
+ description = escape_mdx_markup(description)
value = prop.value
if value:
@@ -127,6 +137,7 @@ def generate_markdown(properties):
value = placeholder_pattern.sub(r'`\1{\2}`', value)
value = value.replace('\n', ' ')
value = multi_space_pattern.sub(' ', value)
+ value = escape_mdx_markup(value)
markdown += f"| `{prop.name}` | {value} | {prop.tag} | {description} |\n"
diff --git a/dev-support/pmd/pmd-ruleset.xml b/dev-support/pmd/pmd-ruleset.xml
index e40cbe8b77bd..f61cd3c5853e 100644
--- a/dev-support/pmd/pmd-ruleset.xml
+++ b/dev-support/pmd/pmd-ruleset.xml
@@ -47,7 +47,9 @@
+
+
diff --git a/dev-support/pom.xml b/dev-support/pom.xml
index 5e47a0ec6105..d21a1b4fbc03 100644
--- a/dev-support/pom.xml
+++ b/dev-support/pom.xml
@@ -17,7 +17,7 @@
org.apache.ozoneozone-main
- 2.2.0-SNAPSHOT
+ 2.3.0-SNAPSHOTozone-dev-supportApache Ozone Dev Support
diff --git a/dev-support/rat/rat-exclusions.txt b/dev-support/rat/rat-exclusions.txt
index 4531b1b601c0..23d3707a65c4 100644
--- a/dev-support/rat/rat-exclusions.txt
+++ b/dev-support/rat/rat-exclusions.txt
@@ -18,9 +18,12 @@
**/*.json
.gitattributes
.github/*
+AGENTS.md
+CLAUDE.md
CONTRIBUTING.md
README.md
SECURITY.md
+THREAT_MODEL.md
# hadoop-hdds/interface-client
src/main/resources/proto.lock
@@ -65,6 +68,8 @@ src/test/resources/ssl/*
# hadoop-ozone/recon
**/pnpm-lock.yaml
src/test/resources/prometheus-test-response.txt
+src/main/resources/chatbot/*.txt
+src/main/resources/chatbot/*.md
# hadoop-ozone/shaded
**/dependency-reduced-pom.xml
diff --git a/hadoop-hdds/annotations/pom.xml b/hadoop-hdds/annotations/pom.xml
index 49f759b9c662..57f26e2ebbed 100644
--- a/hadoop-hdds/annotations/pom.xml
+++ b/hadoop-hdds/annotations/pom.xml
@@ -17,11 +17,11 @@
org.apache.ozonehdds
- 2.2.0-SNAPSHOT
+ 2.3.0-SNAPSHOThdds-annotation-processing
- 2.2.0-SNAPSHOT
+ 2.3.0-SNAPSHOTjarApache Ozone Annotation ProcessingApache Ozone annotation processing tools for validating custom
diff --git a/hadoop-hdds/annotations/src/main/java/org/apache/ozone/annotations/CliOptionStyleProcessor.java b/hadoop-hdds/annotations/src/main/java/org/apache/ozone/annotations/CliOptionStyleProcessor.java
new file mode 100644
index 000000000000..0aec138dd7d0
--- /dev/null
+++ b/hadoop-hdds/annotations/src/main/java/org/apache/ozone/annotations/CliOptionStyleProcessor.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.ozone.annotations;
+
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.regex.Pattern;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.SimpleAnnotationValueVisitor8;
+import javax.tools.Diagnostic;
+
+/**
+ * Validates that picocli options use the preferred Ozone CLI option style.
+ */
+@SupportedAnnotationTypes(CliOptionStyleProcessor.OPTION_ANNOTATION)
+public class CliOptionStyleProcessor extends AbstractProcessor {
+
+ static final String OPTION_ANNOTATION = "picocli.CommandLine.Option";
+ private static final String NAMES_ATTRIBUTE = "names";
+ private static final Pattern CAMEL_CASE = Pattern.compile("--.*[A-Z].*");
+ private static final Pattern UNDER_SCORE = Pattern.compile("--.*_.*");
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latestSupported();
+ }
+
+ @Override
+ public boolean process(Set extends TypeElement> annotations,
+ RoundEnvironment roundEnv) {
+ for (TypeElement annotation : annotations) {
+ if (OPTION_ANNOTATION.contentEquals(annotation.getQualifiedName())) {
+ roundEnv.getElementsAnnotatedWith(annotation)
+ .forEach(this::checkOptionNames);
+ }
+ }
+ return false;
+ }
+
+ private void checkOptionNames(Element element) {
+ for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
+ if (isOptionAnnotation(annotation)) {
+ checkOptionNames(element, annotation);
+ }
+ }
+ }
+
+ private boolean isOptionAnnotation(AnnotationMirror annotation) {
+ return OPTION_ANNOTATION.contentEquals(
+ annotation.getAnnotationType().asElement().toString());
+ }
+
+ private void checkOptionNames(Element element, AnnotationMirror annotation) {
+ for (Entry extends ExecutableElement, ? extends AnnotationValue> entry :
+ annotation.getElementValues().entrySet()) {
+ if (entry.getKey().getSimpleName().contentEquals(NAMES_ATTRIBUTE)) {
+ checkOptionNameValues(element, annotation, entry.getValue());
+ }
+ }
+ }
+
+ private void checkOptionNameValues(Element element, AnnotationMirror annotation,
+ AnnotationValue value) {
+ value.accept(new SimpleAnnotationValueVisitor8() {
+ @Override
+ public Void visitArray(List extends AnnotationValue> values,
+ Void unused) {
+ values.forEach(v -> checkOptionNameValues(element, annotation, v));
+ return null;
+ }
+
+ @Override
+ public Void visitString(String option, Void unused) {
+ checkOptionName(option, element, annotation, value);
+ return null;
+ }
+ }, null);
+ }
+
+ private void checkOptionName(String option, Element element,
+ AnnotationMirror annotation, AnnotationValue value) {
+ if (hasDeprecatedStyle(option)) {
+ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
+ String.format("CLI option '%s' uses a deprecated style. New options "
+ + "should use --dash-separated-style long names or "
+ + "single-character short names.", option),
+ element, annotation, value);
+ }
+ }
+
+ private static boolean hasDeprecatedStyle(String option) {
+ if (option.startsWith("--")) {
+ return CAMEL_CASE.matcher(option).matches()
+ || UNDER_SCORE.matcher(option).matches();
+ }
+ return option.startsWith("-") && option.length() > 2;
+ }
+
+}
diff --git a/hadoop-hdds/cli-common/pom.xml b/hadoop-hdds/cli-common/pom.xml
index 752a5fc57829..1ace7bb68ac9 100644
--- a/hadoop-hdds/cli-common/pom.xml
+++ b/hadoop-hdds/cli-common/pom.xml
@@ -17,11 +17,11 @@
org.apache.ozonehdds-hadoop-dependency-client
- 2.2.0-SNAPSHOT
+ 2.3.0-SNAPSHOT../hadoop-dependency-clienthdds-cli-common
- 2.2.0-SNAPSHOT
+ 2.3.0-SNAPSHOTjarApache Ozone CLI CommonApache Ozone CLI Common
@@ -51,6 +51,11 @@
org.slf4jslf4j-api
+
+ org.apache.ozone
+ hdds-annotation-processing
+ provided
+
@@ -67,6 +72,11 @@
maven-compiler-plugin
+
+ org.apache.ozone
+ hdds-annotation-processing
+ ${hdds.version}
+ org.kohsuke.metainf-servicesmetainf-services
@@ -80,6 +90,7 @@
org.kohsuke.metainf_services.AnnotationProcessorImpl
+ org.apache.ozone.annotations.CliOptionStyleProcessorpicocli.codegen.aot.graalvm.processor.NativeImageConfigGeneratorProcessor
diff --git a/hadoop-hdds/cli-common/src/main/java/org/apache/hadoop/hdds/cli/DeprecatedCliOption.java b/hadoop-hdds/cli-common/src/main/java/org/apache/hadoop/hdds/cli/DeprecatedCliOption.java
new file mode 100644
index 000000000000..2f324778ffe4
--- /dev/null
+++ b/hadoop-hdds/cli-common/src/main/java/org/apache/hadoop/hdds/cli/DeprecatedCliOption.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hadoop.hdds.cli;
+
+import java.io.PrintWriter;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Emits warnings when deprecated CLI option aliases are used
+ * and return the recommended replacement option.
+ */
+public final class DeprecatedCliOption {
+
+ private static final Map DEPRECATED_OPTIONS = buildDeprecatedOptions();
+
+ private DeprecatedCliOption() {
+ // no instances
+ }
+
+ private static Map buildDeprecatedOptions() {
+ Map options = new LinkedHashMap<>();
+ options.put("-conf", "--conf");
+ options.put("-id", "--service-id");
+ options.put("-host", "--service-host");
+ options.put("-nodeid", "--nodeid");
+ options.put("-hostname", "--node-host-address");
+ options.put("-al", "--acls");
+ options.put("-ffc", "--filter-by-factor");
+ options.put("-fst", "--filter-by-state");
+ options.put("-tawt", "--transaction-apply-wait-timeout");
+ options.put("-tact", "--transaction-apply-check-interval");
+ options.put("-pct", "--prepare-check-interval");
+ options.put("-pt", "--prepare-timeout");
+ options.put("--accessId", "--access-id");
+ options.put("--bufferSize", "--buffer-size");
+ options.put("--column_family", "--column-family");
+ options.put("--dnSchema", "--dn-schema");
+ options.put("--expectedGeneration", "--expected-generation");
+ options.put("--fileCount", "--file-count");
+ options.put("--fileSize", "--file-size");
+ options.put("--filterByFactor", "--filter-by-factor");
+ options.put("--filterByState", "--filter-by-state");
+ options.put("--keySize", "--key-size");
+ options.put("--maxDatanodesPercentageToInvolvePerIteration",
+ "--max-datanodes-percentage-to-involve-per-iteration");
+ options.put("--maxSizeEnteringTargetInGB", "--max-size-entering-target-in-gb");
+ options.put("--maxSizeLeavingSourceInGB", "--max-size-leaving-source-in-gb");
+ options.put("--maxSizeToMovePerIterationInGB", "--max-size-to-move-per-iteration-in-gb");
+ options.put("--nameLen", "--name-len");
+ options.put("--newLeaderId", "--new-leader-id");
+ options.put("--numOfBuckets", "--num-of-buckets");
+ options.put("--numOfKeys", "--num-of-keys");
+ options.put("--numOfThreads", "--num-of-threads");
+ options.put("--numOfValidateThreads", "--num-of-validate-threads");
+ options.put("--numOfVolumes", "--num-of-volumes");
+ options.put("--onlyFileNames", "--only-file-names");
+ options.put("--replicationFactor", "--replication-factor");
+ options.put("--replicationType", "--replication-type");
+ options.put("--scmHost", "--scm-host");
+ options.put("--secretKey", "--secret");
+ options.put("--segmentPath", "--segment-path");
+ options.put("--validateWrites", "--validate-writes");
+ return options;
+ }
+
+ /**
+ * If {@code arg} is a deprecated option (with or without {@code =value} part),
+ * print a warning to stderr and return with the recommended replacement option.
+ */
+ public static String toNonDeprecated(String arg, PrintWriter err) {
+ if (arg == null || arg.isEmpty()) {
+ return arg;
+ }
+
+ String result = arg;
+ String[] parts = arg.split("=", 2);
+ String opt = parts[0];
+ String optToUse = DEPRECATED_OPTIONS.getOrDefault(opt, opt);
+
+ if (!Objects.equals(opt, optToUse)) {
+ warn(err, opt, optToUse);
+ result = parts.length == 2
+ ? optToUse + '=' + parts[1]
+ : optToUse;
+ }
+
+ return result;
+ }
+
+ private static void warn(PrintWriter err, String deprecated, String replacement) {
+ err.printf("WARNING: Option '%s' is deprecated. Use '%s' instead.%n",
+ deprecated, replacement);
+ }
+}
diff --git a/hadoop-hdds/cli-common/src/main/java/org/apache/hadoop/hdds/cli/GenericCli.java b/hadoop-hdds/cli-common/src/main/java/org/apache/hadoop/hdds/cli/GenericCli.java
index da46c2600389..b90bf49ce2dc 100644
--- a/hadoop-hdds/cli-common/src/main/java/org/apache/hadoop/hdds/cli/GenericCli.java
+++ b/hadoop-hdds/cli-common/src/main/java/org/apache/hadoop/hdds/cli/GenericCli.java
@@ -25,6 +25,7 @@
import java.nio.file.NoSuchFileException;
import java.util.Map;
import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hdds.HddsUtils;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.ratis.util.ExitUtils;
@@ -55,7 +56,8 @@ public void setConfigurationOverrides(Map configOverrides) {
configOverrides.forEach(config::set);
}
- @Option(names = {"-conf"})
+ @Option(names = {"--conf"},
+ description = "Path to custom configuration file.")
public void setConfigurationPath(String configPath) {
config.addResource(new Path(configPath));
}
@@ -66,12 +68,16 @@ public GenericCli() {
public GenericCli(CommandLine.IFactory factory) {
cmd = new CommandLine(this, factory);
+ ExtensibleParentCommand.addSubcommands(cmd);
+ cmd.getCommandSpec().preprocessor((args, commandSpec, argSpec, info) -> {
+ args.replaceAll(arg -> DeprecatedCliOption.toNonDeprecated(arg, cmd.getErr()));
+ return false;
+ });
+
cmd.setExecutionExceptionHandler((ex, commandLine, parseResult) -> {
printError(ex);
return EXECUTION_ERROR_EXIT_CODE;
});
-
- ExtensibleParentCommand.addSubcommands(cmd);
}
public void run(String[] argv) {
@@ -93,14 +99,19 @@ public void printError(Throwable error) {
final String rawMessage = error.getMessage();
if (verbose || rawMessage == null || rawMessage.isEmpty()) {
error.printStackTrace(cmd.getErr());
- } else {
- if (error instanceof FileSystemException) {
- String errorMessage = handleFileSystemException((FileSystemException) error);
- cmd.getErr().println(errorMessage);
- } else {
- cmd.getErr().println(rawMessage.split("\n")[0]);
- }
+ return;
+ }
+ String aclLine = HddsUtils.formatAccessControlExceptionLine(error);
+ if (aclLine != null) {
+ cmd.getErr().println(aclLine);
+ ExitUtils.terminate(EXECUTION_ERROR_EXIT_CODE, aclLine, null);
+ }
+ if (error instanceof FileSystemException) {
+ String errorMessage = handleFileSystemException((FileSystemException) error);
+ cmd.getErr().println(errorMessage);
+ return;
}
+ cmd.getErr().println(rawMessage.split("\n")[0]);
}
@Override
@@ -134,22 +145,23 @@ protected PrintWriter err() {
}
private static String handleFileSystemException(FileSystemException e) {
- String errorMessage = e.getMessage();
+ StringBuilder sb = new StringBuilder();
+ sb.append("Error: ");
// If reason is set, return the exception's message as it is.
// Otherwise, construct a custom message based on the type of exception
if (e.getReason() == null) {
if (e instanceof NoSuchFileException) {
- errorMessage = "File not found: " + errorMessage;
+ sb.append("File not found: ");
} else if (e instanceof AccessDeniedException) {
- errorMessage = "Access denied: " + errorMessage;
+ sb.append("Access denied: ");
} else if (e instanceof FileAlreadyExistsException) {
- errorMessage = "File already exists: " + errorMessage;
+ sb.append("File already exists: ");
} else {
- errorMessage = e.getClass().getSimpleName() + ": " + errorMessage;
+ sb.append(e.getClass().getSimpleName()).append(": ");
}
}
- return "Error: " + errorMessage;
+ return sb.append(e.getMessage()).toString();
}
}
diff --git a/hadoop-hdds/cli-common/src/test/java/org/apache/hadoop/hdds/cli/TestGenericCliConfiguration.java b/hadoop-hdds/cli-common/src/test/java/org/apache/hadoop/hdds/cli/TestGenericCliConfiguration.java
new file mode 100644
index 000000000000..b61b847fec6f
--- /dev/null
+++ b/hadoop-hdds/cli-common/src/test/java/org/apache/hadoop/hdds/cli/TestGenericCliConfiguration.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hadoop.hdds.cli;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+/**
+ * Tests for {@link GenericCli} configuration option handling.
+ */
+public class TestGenericCliConfiguration {
+
+ private static Path deprecatedConf;
+ private static Path preferredConf;
+
+ private static final class TestGenericCli extends GenericCli {
+ }
+
+ @BeforeAll
+ static void setup() throws IOException {
+ deprecatedConf = writeConf("deprecated");
+ preferredConf = writeConf("preferred");
+ }
+
+ @Test
+ void confOptionsAreExclusive() {
+ CommandLine cmd = new TestGenericCli().getCmd();
+ assertThrows(CommandLine.OverwrittenOptionException.class,
+ () -> cmd.parseArgs("-conf", deprecatedConf.toString(), "--conf", preferredConf.toString()));
+ assertThrows(CommandLine.OverwrittenOptionException.class,
+ () -> cmd.parseArgs("--conf", deprecatedConf.toString(), "-conf", preferredConf.toString()));
+ }
+
+ @Test
+ void deprecatedConfIsUsedWhenNonDeprecatedIsAbsent() {
+ TestGenericCli cli = new TestGenericCli();
+ cli.getCmd().parseArgs("-conf", deprecatedConf.toString());
+
+ assertThat(cli.getOzoneConf().get("test.key")).isEqualTo("deprecated");
+ }
+
+ private static Path writeConf(String value) throws IOException {
+ Path conf = Files.createTempFile("ozone-conf-", ".xml");
+ Files.write(conf,
+ ("test.key" + value
+ + "").getBytes(StandardCharsets.UTF_8));
+ conf.toFile().deleteOnExit();
+ return conf;
+ }
+}
diff --git a/hadoop-hdds/client/pom.xml b/hadoop-hdds/client/pom.xml
index 99b7664cdd55..d32178ff5fef 100644
--- a/hadoop-hdds/client/pom.xml
+++ b/hadoop-hdds/client/pom.xml
@@ -17,12 +17,12 @@
org.apache.ozonehdds-hadoop-dependency-client
- 2.2.0-SNAPSHOT
+ 2.3.0-SNAPSHOT../hadoop-dependency-clienthdds-client
- 2.2.0-SNAPSHOT
+ 2.3.0-SNAPSHOTjarApache Ozone HDDS ClientApache Ozone Distributed Data Store Client Library
@@ -32,6 +32,10 @@
com.google.guavaguava
+
+ commons-io
+ commons-io
+ jakarta.annotationjakarta.annotation-api
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/ContainerClientMetrics.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/ContainerClientMetrics.java
index 64cfb32ddcce..710a2da5ec80 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/ContainerClientMetrics.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/ContainerClientMetrics.java
@@ -20,6 +20,7 @@
import com.google.common.annotations.VisibleForTesting;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.hadoop.hdds.protocol.DatanodeID;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
import org.apache.hadoop.hdds.scm.pipeline.PipelineID;
@@ -79,6 +80,29 @@ public final class ContainerClientMetrics {
private final Map writeChunksCallsByLeaders;
private final MetricsRegistry registry;
+ /**
+ * Handle for one ContainerClientMetrics acquisition.
+ */
+ public static final class Handle implements AutoCloseable {
+ private final ContainerClientMetrics metrics;
+ private final AtomicBoolean closed = new AtomicBoolean(false);
+
+ private Handle(ContainerClientMetrics metrics) {
+ this.metrics = metrics;
+ }
+
+ public ContainerClientMetrics metrics() {
+ return metrics;
+ }
+
+ @Override
+ public void close() {
+ if (closed.compareAndSet(false, true)) {
+ release();
+ }
+ }
+ }
+
public static synchronized ContainerClientMetrics acquire() {
if (instance == null) {
instanceCount++;
@@ -90,6 +114,10 @@ public static synchronized ContainerClientMetrics acquire() {
return instance;
}
+ public static Handle acquireHandle() {
+ return new Handle(acquire());
+ }
+
public static synchronized void release() {
if (instance == null) {
throw new IllegalStateException("This metrics class is not used.");
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/OzoneClientConfig.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/OzoneClientConfig.java
index dba10525d2bd..824f2674f55a 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/OzoneClientConfig.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/OzoneClientConfig.java
@@ -36,7 +36,35 @@
public class OzoneClientConfig {
private static final Logger LOG = LoggerFactory.getLogger(OzoneClientConfig.class);
+ public static final String OZONE_READ_SHORT_CIRCUIT = "ozone.client.read.short-circuit";
+ public static final boolean OZONE_READ_SHORT_CIRCUIT_DEFAULT = false;
+ public static final String OZONE_DOMAIN_SOCKET_PATH = "ozone.domain.socket.path";
+ public static final String SHORT_CIRCUIT_PREFIX = OZONE_READ_SHORT_CIRCUIT + ".";
+ public static final short DATA_TRANSFER_VERSION = 28;
+ public static final byte DATA_TRANSFER_MAGIC_CODE = 99;
+
+ @Config(key = "ozone.client.read.short-circuit",
+ defaultValue = "false",
+ type = ConfigType.BOOLEAN,
+ description = "Whether read short-circuit is enabled or not",
+ tags = { ConfigTag.CLIENT, ConfigTag.DATANODE })
+ private boolean shortCircuitEnabled = OZONE_READ_SHORT_CIRCUIT_DEFAULT;
+
+ @Config(key = SHORT_CIRCUIT_PREFIX + "buffer.size",
+ defaultValue = "128KB",
+ type = ConfigType.SIZE,
+ description = "Buffer size of reader/writer.",
+ tags = { ConfigTag.CLIENT, ConfigTag.DATANODE })
+ private int shortCircuitBufferSize = 128 * 1024;
+ @Config(key = SHORT_CIRCUIT_PREFIX + "disable.interval",
+ defaultValue = "600",
+ type = ConfigType.LONG,
+ description = "If some unknown IO error happens on Domain socket read, short circuit read will be disabled " +
+ "temporarily for this period of time(seconds).",
+ tags = { ConfigTag.CLIENT })
+ private long shortCircuitReadDisableInterval = 60 * 10;
+
@Config(key = "ozone.client.stream.buffer.flush.size",
defaultValue = "16MB",
type = ConfigType.SIZE,
@@ -289,6 +317,14 @@ public class OzoneClientConfig {
tags = ConfigTag.CLIENT)
private boolean enablePutblockPiggybacking = false;
+ @Config(key = "ozone.client.datastream.putblock.on.close.enabled",
+ defaultValue = "false",
+ type = ConfigType.BOOLEAN,
+ description = "When enabled, use StreamInitWithPutBlock so datanodes commit PutBlock " +
+ "when the Ratis data stream closes instead of via a separate WriteAsync PutBlock.",
+ tags = ConfigTag.CLIENT)
+ private boolean datastreamPutBlockOnCloseEnabled = false;
+
@Config(key = "ozone.client.key.write.concurrency",
defaultValue = "1",
description = "Maximum concurrent writes allowed on each key. " +
@@ -402,6 +438,30 @@ public void validate() {
}
}
+ public boolean isShortCircuitEnabled() {
+ return shortCircuitEnabled;
+ }
+
+ public void setShortCircuit(boolean enabled) {
+ shortCircuitEnabled = enabled;
+ }
+
+ public int getShortCircuitBufferSize() {
+ return shortCircuitBufferSize;
+ }
+
+ public void setShortCircuitBufferSize(int size) {
+ this.shortCircuitBufferSize = size;
+ }
+
+ public long getShortCircuitReadDisableInterval() {
+ return shortCircuitReadDisableInterval;
+ }
+
+ public void setShortCircuitReadDisableInterval(long value) {
+ shortCircuitReadDisableInterval = value;
+ }
+
public long getStreamBufferFlushSize() {
return streamBufferFlushSize;
}
@@ -646,6 +706,14 @@ public void setStreamReadTimeout(Duration streamReadTimeout) {
this.streamReadTimeout = streamReadTimeout;
}
+ public boolean isDatastreamPutBlockOnCloseEnabled() {
+ return datastreamPutBlockOnCloseEnabled;
+ }
+
+ public void setDatastreamPutBlockOnCloseEnabled(boolean datastreamPutBlockOnCloseEnabled) {
+ this.datastreamPutBlockOnCloseEnabled = datastreamPutBlockOnCloseEnabled;
+ }
+
/**
* Enum for indicating what mode to use when combining chunk and block
* checksums to define an aggregate FileChecksum. This should be considered
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientCreator.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientCreator.java
index ce3404ae5b79..d68836bd3fe7 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientCreator.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientCreator.java
@@ -20,8 +20,10 @@
import java.io.IOException;
import java.util.Objects;
import org.apache.hadoop.hdds.conf.ConfigurationSource;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
import org.apache.hadoop.hdds.scm.client.ClientTrustManager;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.hadoop.hdds.scm.storage.DomainSocketFactory;
import org.apache.hadoop.hdds.utils.IOUtils;
import org.apache.hadoop.ozone.OzoneConfigKeys;
import org.apache.hadoop.ozone.OzoneSecurityUtil;
@@ -36,6 +38,8 @@ public class XceiverClientCreator implements XceiverClientFactory {
private final boolean topologyAwareRead;
private final ClientTrustManager trustManager;
private final boolean securityEnabled;
+ private boolean shortCircuitEnabled;
+ private DomainSocketFactory domainSocketFactory;
public XceiverClientCreator(ConfigurationSource conf) {
this(conf, null);
@@ -51,6 +55,10 @@ public XceiverClientCreator(ConfigurationSource conf, ClientTrustManager trustMa
if (securityEnabled) {
Objects.requireNonNull(trustManager, "trustManager == null");
}
+ shortCircuitEnabled = conf.getObject(OzoneClientConfig.class).isShortCircuitEnabled();
+ if (shortCircuitEnabled) {
+ domainSocketFactory = DomainSocketFactory.getInstance(conf);
+ }
}
public static void enableErrorInjection(ErrorInjector injector) {
@@ -61,14 +69,27 @@ public boolean isSecurityEnabled() {
return securityEnabled;
}
+ @Override
+ public boolean isShortCircuitEnabled() {
+ return shortCircuitEnabled && domainSocketFactory.isServiceReady();
+ }
+
protected XceiverClientSpi newClient(Pipeline pipeline) throws IOException {
+ return newClient(pipeline, null);
+ }
+
+ protected XceiverClientSpi newClient(Pipeline pipeline, DatanodeDetails dn) throws IOException {
XceiverClientSpi client;
switch (pipeline.getType()) {
case RATIS:
client = XceiverClientRatis.newXceiverClientRatis(pipeline, conf, trustManager, errorInjector);
break;
case STAND_ALONE:
- client = new XceiverClientGrpc(pipeline, conf, trustManager);
+ if (dn != null) {
+ client = new XceiverClientShortCircuit(pipeline, conf, dn);
+ } else {
+ client = new XceiverClientGrpc(pipeline, conf, trustManager);
+ }
break;
case EC:
client = new ECXceiverClientGrpc(pipeline, conf, trustManager);
@@ -96,7 +117,14 @@ public void releaseClient(XceiverClientSpi xceiverClient, boolean invalidateClie
}
@Override
- public XceiverClientSpi acquireClientForReadData(Pipeline pipeline) throws IOException {
+ public XceiverClientSpi acquireClientForReadData(Pipeline pipeline, boolean allowShortCircuit)
+ throws IOException {
+ return acquireClient(pipeline, false, allowShortCircuit);
+ }
+
+ @Override
+ public XceiverClientSpi acquireClient(Pipeline pipeline, boolean topologyAware, boolean allowShortCircuit)
+ throws IOException {
return acquireClient(pipeline);
}
@@ -116,7 +144,10 @@ public void releaseClient(XceiverClientSpi xceiverClient, boolean invalidateClie
}
@Override
- public void close() throws Exception {
+ public void close() {
// clients are not tracked, closing each client is the responsibility of users of this class
+ if (domainSocketFactory != null) {
+ domainSocketFactory.close();
+ }
}
}
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientFactory.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientFactory.java
index a46090545904..00e680307dc3 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientFactory.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientFactory.java
@@ -52,8 +52,10 @@ public interface XceiverClientFactory extends AutoCloseable {
* @return XceiverClientSpi connected to a container
* @throws IOException if a XceiverClientSpi cannot be acquired
*/
- XceiverClientSpi acquireClientForReadData(Pipeline pipeline)
- throws IOException;
+ default XceiverClientSpi acquireClientForReadData(Pipeline pipeline)
+ throws IOException {
+ return acquireClientForReadData(pipeline, false);
+ }
/**
* Releases a read XceiverClientSpi after use.
@@ -72,10 +74,17 @@ void releaseClientForReadData(XceiverClientSpi client,
* @return XceiverClientSpi connected to a container
* @throws IOException if a XceiverClientSpi cannot be acquired
*/
- XceiverClientSpi acquireClient(Pipeline pipeline, boolean topologyAware)
+ XceiverClientSpi acquireClient(Pipeline pipeline, boolean topologyAware) throws IOException;
+
+ XceiverClientSpi acquireClientForReadData(Pipeline pipeline, boolean allowShortCircuit)
+ throws IOException;
+
+ XceiverClientSpi acquireClient(Pipeline pipeline, boolean topologyAware, boolean allowShortCircuit)
throws IOException;
- void releaseClient(XceiverClientSpi xceiverClient, boolean invalidateClient,
- boolean topologyAware);
+ void releaseClient(XceiverClientSpi xceiverClient, boolean invalidateClient, boolean topologyAware);
+ default boolean isShortCircuitEnabled() {
+ return false;
+ }
}
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientGrpc.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientGrpc.java
index 1f9ac0a12267..bf8374f5e3a4 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientGrpc.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientGrpc.java
@@ -30,6 +30,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -37,7 +38,7 @@
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.stream.Collectors;
+import java.util.concurrent.locks.LockSupport;
import org.apache.hadoop.hdds.HddsConfigKeys;
import org.apache.hadoop.hdds.HddsUtils;
import org.apache.hadoop.hdds.client.BlockID;
@@ -63,6 +64,7 @@
import org.apache.hadoop.ozone.OzoneConfigKeys;
import org.apache.hadoop.ozone.OzoneConsts;
import org.apache.hadoop.util.Time;
+import org.apache.ratis.protocol.exceptions.TimeoutIOException;
import org.apache.ratis.thirdparty.com.google.protobuf.TextFormat;
import org.apache.ratis.thirdparty.io.grpc.ManagedChannel;
import org.apache.ratis.thirdparty.io.grpc.Status;
@@ -88,14 +90,14 @@
* how it works, and how it is integrated with the Ozone client.
*/
public class XceiverClientGrpc extends XceiverClientSpi {
- private static final Logger LOG = LoggerFactory.getLogger(XceiverClientGrpc.class);
- private static final int SHUTDOWN_WAIT_INTERVAL_MILLIS = 100;
+ public static final Logger LOG = LoggerFactory.getLogger(XceiverClientGrpc.class);
private static final int SHUTDOWN_WAIT_MAX_SECONDS = 5;
private final Pipeline pipeline;
private final ConfigurationSource config;
private final XceiverClientMetrics metrics;
private final Semaphore semaphore;
private long timeout;
+ private final long streamReadTimeoutNanos;
private final SecurityConfig secConfig;
private final boolean topologyAwareRead;
private final ClientTrustManager trustManager;
@@ -121,6 +123,8 @@ public XceiverClientGrpc(Pipeline pipeline, ConfigurationSource config,
Objects.requireNonNull(config, "config == null");
setTimeout(config.getTimeDuration(OzoneConfigKeys.OZONE_CLIENT_READ_TIMEOUT,
OzoneConfigKeys.OZONE_CLIENT_READ_TIMEOUT_DEFAULT, TimeUnit.SECONDS));
+ this.streamReadTimeoutNanos = config.getObject(OzoneClientConfig.class)
+ .getStreamReadTimeout().toNanos();
this.pipeline = pipeline;
this.config = config;
this.secConfig = new SecurityConfig(config);
@@ -133,6 +137,7 @@ public XceiverClientGrpc(Pipeline pipeline, ConfigurationSource config,
OzoneConfigKeys.OZONE_NETWORK_TOPOLOGY_AWARE_READ_DEFAULT);
this.trustManager = trustManager;
this.getBlockDNcache = new ConcurrentHashMap<>();
+ LOG.info("{} is created for pipeline {}", XceiverClientGrpc.class.getSimpleName(), pipeline);
}
/**
@@ -252,7 +257,7 @@ public boolean isConnected(DatanodeDetails details) {
/**
* Closes all the communication channels of the client one-by-one.
* When a channel is closed, no further requests can be sent via the channel,
- * and the method waits to finish all ongoing communication.
+ * and any in-flight RPCs are cancelled immediately (shutdownNow semantics).
*/
@Override
public void close() {
@@ -261,42 +266,33 @@ public void close() {
return;
}
+ // Use shutdownNow() (not the graceful shutdown()) so in-flight RPCs are
+ // cancelled and the channel terminates immediately. close() is frequently
+ // invoked from cache eviction while the XceiverClientManager clientCache
+ // monitor is held (HDDS-15849); a blocking graceful drain there serializes
+ // every concurrent acquireClient()/releaseClient() call.
for (ChannelInfo channelInfo : dnChannelInfoMap.values()) {
- channelInfo.getChannel().shutdown();
- }
-
- final long maxWaitNanos = TimeUnit.SECONDS.toNanos(SHUTDOWN_WAIT_MAX_SECONDS);
- long deadline = System.nanoTime() + maxWaitNanos;
- List nonTerminatedChannels = dnChannelInfoMap.values()
- .stream()
- .map(ChannelInfo::getChannel)
- .filter(Objects::nonNull)
- .collect(Collectors.toList());
-
- while (!nonTerminatedChannels.isEmpty() && System.nanoTime() < deadline) {
- nonTerminatedChannels.removeIf(ManagedChannel::isTerminated);
+ ManagedChannel channel = channelInfo.getChannel();
+ channel.shutdownNow();
try {
- Thread.sleep(SHUTDOWN_WAIT_INTERVAL_MILLIS);
+ if (!channel.awaitTermination(SHUTDOWN_WAIT_MAX_SECONDS, TimeUnit.SECONDS)) {
+ LOG.warn("Channel {} did not terminate within {}s.", channel, SHUTDOWN_WAIT_MAX_SECONDS);
+ }
} catch (InterruptedException e) {
- LOG.error("Interrupted while waiting for channels to terminate", e);
+ LOG.error("Interrupted while waiting for channel termination", e);
Thread.currentThread().interrupt();
break;
}
}
- List failedChannels = dnChannelInfoMap.entrySet()
- .stream()
- .filter(e -> !e.getValue().getChannel().isTerminated())
- .map(Map.Entry::getKey)
- .collect(Collectors.toList());
-
- if (!failedChannels.isEmpty()) {
- LOG.warn("Channels {} did not terminate within timeout.", failedChannels);
- }
-
dnChannelInfoMap.clear();
}
+ @Override
+ public boolean isClosed() {
+ return isClosed.get();
+ }
+
@Override
public Pipeline getPipeline() {
return pipeline;
@@ -542,6 +538,9 @@ private XceiverClientReply sendCommandWithRetry(
} catch (InterruptedException e) {
LOG.error("Command execution was interrupted ", e);
Thread.currentThread().interrupt();
+ throw (IOException) new InterruptedIOException(
+ "Command " + processForDebug(request) + " was interrupted.")
+ .initCause(e);
}
}
@@ -564,19 +563,53 @@ private XceiverClientReply sendCommandWithRetry(
@Override
public void streamRead(ContainerCommandRequestProto request,
- StreamingReadResponse streamObserver) {
+ StreamingReadResponse streamObserver) throws IOException {
+ final ClientCallStreamObserver obs = streamObserver.getRequestObserver();
+
+ if (!obs.isReady()) {
+ LOG.debug("->{}: flow control stall (isReady=false) for block={} offset={} length={}. Waiting.",
+ streamObserver,
+ request.getReadBlock().getBlockID().getLocalID(),
+ request.getReadBlock().getOffset(),
+ request.getReadBlock().getLength());
+ final long deadlineNs = System.nanoTime() + streamReadTimeoutNanos;
+ while (!obs.isReady() && System.nanoTime() - deadlineNs < 0) {
+ LockSupport.parkNanos(10_000_000L);
+ if (Thread.currentThread().isInterrupted()) {
+ Thread.currentThread().interrupt();
+ throw new InterruptedIOException("Interrupted while waiting for stream to become ready: " + streamObserver);
+ }
+ }
+ if (!obs.isReady()) {
+ throw new TimeoutIOException("Timed out waiting for stream to become ready: " + streamObserver);
+ }
+ }
+
if (LOG.isDebugEnabled()) {
LOG.debug("->{}, send onNext request {}",
streamObserver, TextFormat.shortDebugString(request.getReadBlock()));
}
- streamObserver.getRequestObserver().onNext(request);
+ obs.onNext(request);
}
@Override
public void initStreamRead(BlockID blockID, StreamingReaderSpi streamObserver) throws IOException {
+ initStreamRead(blockID, streamObserver, Collections.emptySet());
+ }
+
+ /**
+ * Start a streaming read, skipping datanodes that previously failed for this block stream.
+ */
+ public void initStreamRead(BlockID blockID, StreamingReaderSpi streamObserver,
+ Set excludedDatanodes) throws IOException {
final List datanodeList = sortDatanodes(null, ContainerProtos.Type.ReadBlock);
IOException lastException = null;
for (DatanodeDetails dn : datanodeList) {
+ if (excludedDatanodes.contains(dn.getID())) {
+ LOG.debug("Skipping excluded datanode {} (uuid={}) for initStreamRead {}",
+ dn, dn.getUuidString(), blockID.getContainerBlockID());
+ continue;
+ }
try {
checkOpen(dn);
semaphore.acquire();
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientManager.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientManager.java
index 629932b0d371..221cae5f62e7 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientManager.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientManager.java
@@ -18,6 +18,7 @@
package org.apache.hadoop.hdds.scm;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.apache.hadoop.hdds.DatanodeVersion.SHORT_CIRCUIT_READS;
import static org.apache.hadoop.hdds.conf.ConfigTag.OZONE;
import static org.apache.hadoop.hdds.conf.ConfigTag.PERFORMANCE;
import static org.apache.hadoop.hdds.scm.exceptions.SCMException.ResultCodes.NO_REPLICA_FOUND;
@@ -29,7 +30,9 @@
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import java.io.IOException;
+import java.net.InetSocketAddress;
import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.apache.hadoop.hdds.conf.Config;
import org.apache.hadoop.hdds.conf.ConfigGroup;
@@ -39,7 +42,10 @@
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
import org.apache.hadoop.hdds.scm.client.ClientTrustManager;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.hadoop.hdds.scm.storage.DomainSocketFactory;
+import org.apache.hadoop.net.NetUtils;
import org.apache.hadoop.ozone.util.CacheMetrics;
+import org.apache.hadoop.ozone.util.OzoneNetUtils;
import org.apache.hadoop.security.UserGroupInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -62,8 +68,8 @@ public class XceiverClientManager extends XceiverClientCreator {
private final Cache clientCache;
private final CacheMetrics cacheMetrics;
-
private static XceiverClientMetrics metrics;
+ private final ConcurrentHashMap localDNCache;
/**
* Creates a new XceiverClientManager for non secured ozone cluster.
@@ -103,6 +109,7 @@ public void onRemoval(
}).build();
cacheMetrics = CacheMetrics.create(clientCache, this);
+ this.localDNCache = new ConcurrentHashMap<>();
}
@VisibleForTesting
@@ -111,26 +118,53 @@ public Cache getClientCache() {
}
/**
- * {@inheritDoc}
+ * Acquires a XceiverClientSpi connected to a container for read.
+ *
+ * If there is already a cached XceiverClientSpi, simply return
+ * the cached otherwise create a new one.
+ *
+ * @param pipeline the container pipeline for the client connection
+ * @param allowShortCircuit create a short-circuit read client or not if applicable
+ * @return XceiverClientSpi connected to a container
+ * @throws IOException if a XceiverClientSpi cannot be acquired
+ */
+ @Override
+ public XceiverClientSpi acquireClientForReadData(Pipeline pipeline, boolean allowShortCircuit)
+ throws IOException {
+ return acquireClient(pipeline, false, allowShortCircuit);
+ }
+
+ /**
+ * Acquires a XceiverClientSpi connected to a container capable of
+ * storing the specified key.
*
* If there is already a cached XceiverClientSpi, simply return
* the cached otherwise create a new one.
+ *
+ * @param pipeline the container pipeline for the client connection
+ * @return XceiverClientSpi connected to a container
+ * @throws IOException if a XceiverClientSpi cannot be acquired
*/
@Override
public XceiverClientSpi acquireClient(Pipeline pipeline,
- boolean topologyAware) throws IOException {
+ boolean topologyAware, boolean allowShortCircuit) throws IOException {
Objects.requireNonNull(pipeline, "pipeline == null");
Preconditions.checkArgument(pipeline.getNodes() != null);
Preconditions.checkArgument(!pipeline.getNodes().isEmpty(),
NO_REPLICA_FOUND);
synchronized (clientCache) {
- XceiverClientSpi info = getClient(pipeline, topologyAware);
+ XceiverClientSpi info = getClient(pipeline, topologyAware, allowShortCircuit);
info.incrementReference();
return info;
}
}
+ @Override
+ public XceiverClientSpi acquireClient(Pipeline pipeline, boolean topologyAware) throws IOException {
+ return acquireClient(pipeline, topologyAware, false);
+ }
+
@Override
public void releaseClient(XceiverClientSpi client, boolean invalidateClient,
boolean topologyAware) {
@@ -139,7 +173,7 @@ public void releaseClient(XceiverClientSpi client, boolean invalidateClient,
client.decrementReference();
if (invalidateClient) {
Pipeline pipeline = client.getPipeline();
- String key = getPipelineCacheKey(pipeline, topologyAware);
+ String key = getPipelineCacheKey(pipeline, topologyAware, client instanceof XceiverClientShortCircuit);
XceiverClientSpi cachedClient = clientCache.getIfPresent(key);
if (cachedClient == client) {
clientCache.invalidate(key);
@@ -148,24 +182,47 @@ public void releaseClient(XceiverClientSpi client, boolean invalidateClient,
}
}
- protected XceiverClientSpi getClient(Pipeline pipeline, boolean topologyAware)
+ protected XceiverClientSpi getClient(Pipeline pipeline, boolean topologyAware, boolean allowShortCircuit)
throws IOException {
try {
- // create different client different pipeline node based on
- // network topology
- String key = getPipelineCacheKey(pipeline, topologyAware);
- return clientCache.get(key, () -> newClient(pipeline));
+ // create different client different pipeline node based on network topology
+ String key = getPipelineCacheKey(pipeline, topologyAware, allowShortCircuit);
+ return clientCache.get(key, () -> newClient(pipeline, localDNCache.get(key)));
} catch (Exception e) {
throw new IOException(
"Exception getting XceiverClient: " + e, e);
}
}
- private String getPipelineCacheKey(Pipeline pipeline,
- boolean topologyAware) {
- String key = pipeline.getId().getId().toString() + pipeline.getType();
+ private String getPipelineCacheKey(Pipeline pipeline, boolean topologyAware, boolean allowShortCircuit) {
+ StringBuilder key = new StringBuilder()
+ .append(pipeline.getId().getId()).append('-').append(pipeline.getType());
boolean isEC = pipeline.getType() == HddsProtos.ReplicationType.EC;
- if (topologyAware || isEC) {
+ DatanodeDetails localDN = null;
+ boolean shortCircuitPipeline = false;
+
+ if ((!isEC) && allowShortCircuit && isShortCircuitEnabled()) {
+ int port = 0;
+ InetSocketAddress localAddr = null;
+ for (DatanodeDetails dn : pipeline.getNodes()) {
+ // read port from the data node, on failure use default configured port.
+ port = dn.getPort(DatanodeDetails.Port.Name.STANDALONE).getValue();
+ InetSocketAddress addr = NetUtils.createSocketAddr(dn.getIpAddress(), port);
+ if (OzoneNetUtils.isAddressLocal(addr) &&
+ dn.getCurrentVersion() >= SHORT_CIRCUIT_READS.toProtoValue()) {
+ localAddr = addr;
+ localDN = dn;
+ break;
+ }
+ }
+ if (localAddr != null) {
+ // Find a local DN and short circuit read is enabled
+ key.append('@').append(localAddr.getHostName()).append(':').append(port);
+ shortCircuitPipeline = true;
+ }
+ }
+
+ if (localDN == null && (topologyAware || isEC)) {
try {
DatanodeDetails closestNode = pipeline.getClosestNode();
// Pipeline cache key uses host:port suffix to handle
@@ -183,7 +240,8 @@ private String getPipelineCacheKey(Pipeline pipeline,
// Standalone port is chosen since all datanodes should have a
// standalone port regardless of version and this port should not
// have any collisions.
- key += closestNode.getHostName() + closestNode.getStandalonePort();
+ key.append('@').append(closestNode.getHostName())
+ .append(':').append(closestNode.getStandalonePort());
} catch (IOException e) {
LOG.error("Failed to get closest node to create pipeline cache key:" +
e.getMessage());
@@ -194,13 +252,22 @@ private String getPipelineCacheKey(Pipeline pipeline,
// Append user short name to key to prevent a different user
// from using same instance of xceiverClient.
try {
- key += UserGroupInformation.getCurrentUser().getShortUserName();
+ key.append('|').append(UserGroupInformation.getCurrentUser().getShortUserName());
} catch (IOException e) {
LOG.error("Failed to get current user to create pipeline cache key:" +
e.getMessage());
}
}
- return key;
+
+ if (shortCircuitPipeline) {
+ key.append('|').append(DomainSocketFactory.FEATURE_FLAG);
+ }
+
+ String keyString = key.toString();
+ if (localDN != null) {
+ localDNCache.put(keyString, localDN);
+ }
+ return keyString;
}
/**
@@ -208,12 +275,14 @@ private String getPipelineCacheKey(Pipeline pipeline,
*/
@Override
public void close() {
+ super.close();
//closing is done through RemovalListener
clientCache.invalidateAll();
clientCache.cleanUp();
if (LOG.isDebugEnabled()) {
LOG.debug("XceiverClient cache stats: {}", clientCache.stats());
}
+ localDNCache.clear();
cacheMetrics.unregister();
if (metrics != null) {
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientShortCircuit.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientShortCircuit.java
new file mode 100644
index 000000000000..e2fc70aae982
--- /dev/null
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientShortCircuit.java
@@ -0,0 +1,623 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hadoop.hdds.scm;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.hadoop.hdds.HddsUtils.processForDebug;
+import static org.apache.hadoop.hdds.scm.OzoneClientConfig.DATA_TRANSFER_MAGIC_CODE;
+import static org.apache.hadoop.hdds.scm.OzoneClientConfig.DATA_TRANSFER_VERSION;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.BufferedOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.net.InetSocketAddress;
+import java.net.SocketTimeoutException;
+import java.nio.channels.ClosedChannelException;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import org.apache.hadoop.hdds.conf.ConfigurationSource;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ContainerCommandRequestProto;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ContainerCommandResponseProto;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.DatanodeBlockID;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.hadoop.hdds.scm.storage.DomainSocketFactory;
+import org.apache.hadoop.hdds.security.exception.SCMSecurityException;
+import org.apache.hadoop.hdds.tracing.TracingUtil;
+import org.apache.hadoop.net.NetUtils;
+import org.apache.hadoop.net.unix.DomainSocket;
+import org.apache.hadoop.ozone.OzoneConfigKeys;
+import org.apache.hadoop.util.Daemon;
+import org.apache.hadoop.util.LimitInputStream;
+import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
+import org.apache.ratis.thirdparty.com.google.protobuf.CodedInputStream;
+import org.apache.ratis.thirdparty.io.grpc.Status;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link XceiverClientSpi} implementation, the client to read local replica through short circuit.
+ */
+public class XceiverClientShortCircuit extends XceiverClientSpi {
+ public static final Logger LOG =
+ LoggerFactory.getLogger(XceiverClientShortCircuit.class);
+ private final Pipeline pipeline;
+ private final ConfigurationSource config;
+ private final XceiverClientMetrics metrics;
+ private int readTimeoutMs;
+ private int writeTimeoutMs;
+ // Cache the stream of blocks
+ private final Map blockStreamCache;
+ private final Map sentRequests;
+ private final Daemon readDaemon;
+ private Timer timer;
+
+ private boolean closed = false;
+ private final DatanodeDetails dn;
+ private final InetSocketAddress dnAddr;
+ private final DomainSocketFactory domainSocketFactory;
+ private DomainSocket domainSocket;
+ private AtomicBoolean isDomainSocketOpen = new AtomicBoolean(false);
+ private Lock lock = new ReentrantLock();
+ private final int bufferSize;
+ private final ByteString clientId = ByteString.copyFrom(UUID.randomUUID().toString().getBytes(UTF_8));
+ private final AtomicLong callId = new AtomicLong(0);
+ private long requestSent = 0;
+ private long responseReceived = 0;
+ private String prefix;
+
+ /**
+ * Constructs a client that can communicate with the Container framework on local datanode through DomainSocket.
+ */
+ public XceiverClientShortCircuit(Pipeline pipeline, ConfigurationSource config, DatanodeDetails dn) {
+ super();
+ Objects.requireNonNull(config);
+ this.readTimeoutMs = (int) config.getTimeDuration(OzoneConfigKeys.OZONE_CLIENT_READ_TIMEOUT,
+ OzoneConfigKeys.OZONE_CLIENT_READ_TIMEOUT_DEFAULT, TimeUnit.MILLISECONDS);
+ this.writeTimeoutMs = (int) config.getTimeDuration(OzoneConfigKeys.OZONE_CLIENT_WRITE_TIMEOUT,
+ OzoneConfigKeys.OZONE_CLIENT_WRITE_TIMEOUT_DEFAULT, TimeUnit.MILLISECONDS);
+
+ this.pipeline = pipeline;
+ this.dn = dn;
+ this.domainSocketFactory = DomainSocketFactory.getInstance(config);
+ this.config = config;
+ this.metrics = XceiverClientManager.getXceiverClientMetrics();
+ this.blockStreamCache = new ConcurrentHashMap<>();
+ this.sentRequests = new ConcurrentHashMap<>();
+ int port = dn.getPort(DatanodeDetails.Port.Name.STANDALONE).getValue();
+ this.dnAddr = NetUtils.createSocketAddr(dn.getIpAddress(), port);
+ this.bufferSize = config.getObject(OzoneClientConfig.class).getShortCircuitBufferSize();
+ this.readDaemon = new Daemon(new ReceiveResponseTask());
+ LOG.info("{} is created for pipeline {}", XceiverClientShortCircuit.class.getSimpleName(), pipeline);
+ }
+
+ /**
+ * Create the DomainSocket to connect to the local DataNode.
+ */
+ @Override
+ public void connect() throws IOException {
+ // Even the in & out stream has returned EOFException, domainSocket.isOpen() is still true.
+ if (domainSocket != null && domainSocket.isOpen() && isDomainSocketOpen.get()) {
+ return;
+ }
+ domainSocket = domainSocketFactory.createSocket(readTimeoutMs, writeTimeoutMs, dnAddr);
+ isDomainSocketOpen.set(true);
+ prefix = XceiverClientShortCircuit.class.getSimpleName() + "-" + domainSocket.toString();
+ timer = new Timer(prefix + "-Timer");
+ readDaemon.start();
+ LOG.info("{} is started", prefix);
+ }
+
+ /**
+ * Close the DomainSocket.
+ */
+ @Override
+ public synchronized void close() {
+ closed = true;
+ timer.cancel();
+ if (domainSocket != null) {
+ try {
+ isDomainSocketOpen.set(false);
+ domainSocket.close();
+ LOG.info("{} is closed for {} with {} requests sent and {} responses received",
+ domainSocket.toString(), dn, requestSent, responseReceived);
+ } catch (IOException e) {
+ LOG.warn("Failed to close domain socket for datanode {}", dn, e);
+ }
+ }
+ readDaemon.interrupt();
+ try {
+ readDaemon.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Override
+ public boolean isClosed() {
+ return closed;
+ }
+
+ @Override
+ public Pipeline getPipeline() {
+ return pipeline;
+ }
+
+ public DatanodeDetails getDn() {
+ return this.dn;
+ }
+
+ public ByteString getClientId() {
+ return clientId;
+ }
+
+ public long getCallId() {
+ return callId.incrementAndGet();
+ }
+
+ @Override
+ public ContainerCommandResponseProto sendCommand(ContainerCommandRequestProto request) throws IOException {
+ try {
+ return sendCommandWithTraceID(request, null).getResponse().get();
+ } catch (ExecutionException e) {
+ throw getIOExceptionForSendCommand(request, e);
+ } catch (InterruptedException e) {
+ LOG.error("Command execution was interrupted.");
+ Thread.currentThread().interrupt();
+ throw (IOException) new InterruptedIOException(
+ "Command " + processForDebug(request) + " was interrupted.")
+ .initCause(e);
+ }
+ }
+
+ @Override
+ public Map
+ sendCommandOnAllNodes(
+ ContainerCommandRequestProto request) throws IOException {
+ throw new UnsupportedOperationException("Operation Not supported for " +
+ DomainSocketFactory.FEATURE + " client");
+ }
+
+ @Override
+ public ContainerCommandResponseProto sendCommand(
+ ContainerCommandRequestProto request, List validators)
+ throws IOException {
+ try {
+ XceiverClientReply reply;
+ reply = sendCommandWithTraceID(request, validators);
+ return reply.getResponse().get();
+ } catch (ExecutionException e) {
+ throw getIOExceptionForSendCommand(request, e);
+ } catch (InterruptedException e) {
+ LOG.error("Command execution was interrupted.");
+ Thread.currentThread().interrupt();
+ throw (IOException) new InterruptedIOException(
+ "Command " + processForDebug(request) + " was interrupted.")
+ .initCause(e);
+ }
+ }
+
+ private XceiverClientReply sendCommandWithTraceID(
+ ContainerCommandRequestProto request, List validators)
+ throws IOException {
+ String spanName = "XceiverClientShortCircuit." + request.getCmdType().name();
+ return TracingUtil.executeInNewSpan(spanName,
+ () -> {
+ ContainerCommandRequestProto finalPayload =
+ ContainerCommandRequestProto.newBuilder(request)
+ .setTraceID(TracingUtil.exportCurrentSpan()).build();
+ ContainerCommandResponseProto responseProto = null;
+ IOException ioException = null;
+ XceiverClientReply reply = new XceiverClientReply(null);
+
+ if (request.getCmdType() != ContainerProtos.Type.GetBlock &&
+ request.getCmdType() != ContainerProtos.Type.Echo) {
+ throw new UnsupportedOperationException("Command " + request.getCmdType() +
+ " is not supported for " + DomainSocketFactory.FEATURE + " client");
+ }
+
+ try {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Executing command {} on datanode {}", request, dn);
+ }
+ reply.addDatanode(dn);
+ responseProto = sendCommandInternal(finalPayload).getResponse().get();
+ if (validators != null && !validators.isEmpty()) {
+ for (Validator validator : validators) {
+ validator.accept(request, responseProto);
+ }
+ }
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("request {} {} {} finished", request.getCmdType(),
+ request.getClientId().toStringUtf8(), request.getCallId());
+ }
+ } catch (IOException e) {
+ ioException = e;
+ responseProto = null;
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Failed to execute command {} on datanode {}", request, dn, e);
+ }
+ } catch (ExecutionException e) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Failed to execute command {} on datanode {}", request, dn, e);
+ }
+ if (Status.fromThrowable(e.getCause()).getCode()
+ == Status.UNAUTHENTICATED.getCode()) {
+ throw new SCMSecurityException("Failed to authenticate with "
+ + "datanode DomainSocket XceiverServer with Ozone block token.");
+ }
+ ioException = new IOException(e);
+ } catch (InterruptedException e) {
+ LOG.error("Command execution was interrupted ", e);
+ Thread.currentThread().interrupt();
+ }
+
+ if (responseProto != null) {
+ reply.setResponse(CompletableFuture.completedFuture(responseProto));
+ return reply;
+ } else {
+ Objects.requireNonNull(ioException);
+ String message = "Failed to execute command {}";
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(message + " on the datanode {} {}.", request, dn, domainSocket, ioException);
+ }
+ throw ioException;
+ }
+ });
+ }
+
+ @VisibleForTesting
+ public XceiverClientReply sendCommandInternal(ContainerCommandRequestProto request)
+ throws IOException, InterruptedException {
+ checkOpen();
+ final CompletableFuture replyFuture =
+ new CompletableFuture<>();
+ RequestEntry entry = new RequestEntry(request, replyFuture);
+ sendRequest(entry);
+ return new XceiverClientReply(replyFuture);
+ }
+
+ @Override
+ public XceiverClientReply sendCommandAsync(
+ ContainerCommandRequestProto request)
+ throws IOException, ExecutionException, InterruptedException {
+ throw new UnsupportedOperationException("Operation Not supported for " + DomainSocketFactory.FEATURE + " client");
+ }
+
+ public synchronized void checkOpen() throws IOException {
+ if (closed) {
+ throw new IOException("DomainSocket is not connected.");
+ }
+
+ if (!isDomainSocketOpen.get()) {
+ throw new IOException(domainSocket.toString() + " is not open.");
+ }
+ }
+
+ @Override
+ public CompletableFuture watchForCommit(long index) {
+ // there is no notion of watch for commit index in short-circuit local reads
+ return null;
+ }
+
+ @Override
+ public long getReplicatedMinCommitIndex() {
+ return 0;
+ }
+
+ public FileInputStream getFileInputStream(long id, DatanodeBlockID blockID) {
+ return blockStreamCache.remove(getFileInputStreamMapKey(id, blockID));
+ }
+
+ private String getFileInputStreamMapKey(long id, DatanodeBlockID blockID) {
+ return id + "-" + blockID.getLocalID();
+ }
+
+ @Override
+ public HddsProtos.ReplicationType getPipelineType() {
+ return HddsProtos.ReplicationType.STAND_ALONE;
+ }
+
+ public ConfigurationSource getConfig() {
+ return config;
+ }
+
+ @VisibleForTesting
+ public static Logger getLogger() {
+ return LOG;
+ }
+
+ public void setReadTimeout(int timeout) {
+ this.readTimeoutMs = timeout;
+ }
+
+ public int getReadTimeout() {
+ return this.readTimeoutMs;
+ }
+
+ String getRequestUniqueID(ContainerCommandRequestProto request) {
+ return request.getClientId().toStringUtf8() + request.getCallId();
+ }
+
+ String getRequestUniqueID(ContainerCommandResponseProto response) {
+ return response.getClientId().toStringUtf8() + response.getCallId();
+ }
+
+ void requestTimeout(String requestId) {
+ final RequestEntry entry = sentRequests.remove(requestId);
+ if (entry != null) {
+ LOG.warn("Timeout to receive response for command {}", entry.getRequest());
+ ContainerProtos.Type type = entry.getRequest().getCmdType();
+ metrics.decrPendingContainerOpsMetrics(type);
+ entry.getFuture().completeExceptionally(new TimeoutException("Timeout to receive response"));
+ }
+ }
+
+ public void sendRequest(RequestEntry entry) {
+ ContainerCommandRequestProto request = entry.getRequest();
+ try {
+ String key = getRequestUniqueID(request);
+ TimerTask task = new TimerTask() {
+ @Override
+ public void run() {
+ requestTimeout(key);
+ }
+ };
+ entry.setTimerTask(task);
+ timer.schedule(task, readTimeoutMs);
+ sentRequests.put(key, entry);
+ ContainerProtos.Type type = request.getCmdType();
+ metrics.incrPendingContainerOpsMetrics(type);
+ byte[] bytes = request.toByteArray();
+ if (bytes.length != request.getSerializedSize()) {
+ throw new IOException("Serialized request " + request.getCmdType()
+ + " size mismatch, byte array size " + bytes.length +
+ ", serialized size " + request.getSerializedSize());
+ }
+
+ lock.lock();
+ try {
+ DataOutputStream dataOut =
+ new DataOutputStream(new BufferedOutputStream(domainSocket.getOutputStream(), bufferSize));
+ // send version number
+ dataOut.writeShort(DATA_TRANSFER_VERSION);
+ // send command type
+ dataOut.writeShort(type.getNumber());
+ // send request body
+ request.writeDelimitedTo(dataOut);
+ dataOut.flush();
+ } finally {
+ lock.unlock();
+ entry.setSentTimeNs();
+ requestSent++;
+ }
+ } catch (IOException e) {
+ LOG.error("Failed to send command {}", request, e);
+ entry.getFuture().completeExceptionally(e);
+ metrics.decrPendingContainerOpsMetrics(request.getCmdType());
+ metrics.addContainerOpsLatency(request.getCmdType(), System.nanoTime() - entry.getCreateTimeNs());
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder b =
+ new StringBuilder(getClass().getSimpleName())
+ .append('[').append(" DomainSocket: ").append(domainSocket.toString())
+ .append(" Pipeline: ").append(pipeline.toString())
+ .append(" ]");
+ return b.toString();
+ }
+
+ /**
+ * Task to receive responses from server.
+ */
+ public class ReceiveResponseTask implements Runnable {
+ @Override
+ public void run() {
+ long timerTaskCancelledCount = 0;
+ do {
+ Thread.currentThread().setName(prefix + "-ReceiveResponse");
+ RequestEntry entry = null;
+ try {
+ DataInputStream dataIn = new DataInputStream(domainSocket.getInputStream());
+ final short version = dataIn.readShort();
+ if (version != DATA_TRANSFER_VERSION) {
+ throw new IOException("Version Mismatch (Expected: " +
+ DATA_TRANSFER_VERSION + ", Received: " + version + ")");
+ }
+ long receiveStartTime = System.nanoTime();
+ final short typeNumber = dataIn.readShort();
+ ContainerProtos.Type type = ContainerProtos.Type.forNumber(typeNumber);
+ ContainerCommandResponseProto responseProto =
+ ContainerCommandResponseProto.parseFrom(vintPrefixed(dataIn));
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("received response {} callId {}", type, responseProto.getCallId());
+ }
+ String key = getRequestUniqueID(responseProto);
+ entry = sentRequests.remove(key);
+ if (entry == null) {
+ // This could be two cases
+ // 1. there is bug in the code
+ // 2. the response is too late, the request is removed from sentRequests after it is timeout.
+ throw new IOException("Failed to find request for response, type " + type +
+ ", clientId " + responseProto.getClientId().toStringUtf8() + ", callId " + responseProto.getCallId());
+ }
+
+ // cancel timeout timer task
+ if (entry.getTimerTask().cancel()) {
+ timerTaskCancelledCount++;
+ // purge timer every 1000 cancels
+ if (timerTaskCancelledCount == 1000) {
+ timer.purge();
+ timerTaskCancelledCount = 0;
+ }
+ }
+
+ long processStartTime = System.nanoTime();
+ ContainerProtos.Result result = responseProto.getResult();
+ if (result == ContainerProtos.Result.SUCCESS) {
+ if (type == ContainerProtos.Type.GetBlock) {
+ try {
+ ContainerProtos.GetBlockResponseProto getBlockResponse = responseProto.getGetBlock();
+ if (!getBlockResponse.getShortCircuitAccessGranted()) {
+ throw new IOException("Short-circuit access is denied on " + dn);
+ }
+ // read FS from domainSocket
+ FileInputStream[] fis = new FileInputStream[1];
+ byte[] buf = new byte[1];
+ int ret = domainSocket.recvFileInputStreams(fis, buf, 0, buf.length);
+ if (ret == -1) {
+ throw new IOException("failed to get a file descriptor from datanode " + dn +
+ " for peer is shutdown.");
+ }
+ if (fis[0] == null) {
+ throw new IOException("the datanode " + dn + " failed to " +
+ "pass a file descriptor (might have reached open file limit).");
+ }
+ if (buf[0] != DATA_TRANSFER_MAGIC_CODE) {
+ throw new IOException("Magic Code Mismatch (Expected: " +
+ DATA_TRANSFER_MAGIC_CODE + ", Received: " + buf[0] + ")");
+ }
+ DatanodeBlockID blockID = getBlockResponse.getBlockData().getBlockID();
+ blockStreamCache.put(getFileInputStreamMapKey(responseProto.getCallId(), blockID), fis[0]);
+ } catch (IOException e) {
+ LOG.warn("Failed to handle short-circuit information exchange", e);
+ // disable docket socket for a while
+ domainSocketFactory.disableShortCircuit();
+ entry.getFuture().completeExceptionally(e);
+ continue;
+ }
+ }
+ entry.getFuture().complete(responseProto);
+ } else {
+ // response result is not SUCCESS
+ entry.getFuture().complete(responseProto);
+ }
+ long currentTime = System.nanoTime();
+ long endToEndCost = currentTime - entry.getCreateTimeNs();
+ long sentCost = entry.getSentTimeNs() - entry.getCreateTimeNs();
+ long receiveCost = processStartTime - receiveStartTime;
+ long processCost = currentTime - processStartTime;
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Executed command {} {}:{} on datanode {}, end-to-end {} ns, sent {} ns, receive {} ns, " +
+ "process {} ns", type, entry.getRequest().getClientId().toStringUtf8(),
+ entry.getRequest().getCallId(), dn, endToEndCost, sentCost, receiveCost, processCost);
+ }
+ responseReceived++;
+ metrics.decrPendingContainerOpsMetrics(type);
+ metrics.addContainerOpsLatency(type, endToEndCost);
+ } catch (SocketTimeoutException | EOFException | ClosedChannelException e) {
+ isDomainSocketOpen.set(false);
+ LOG.info("{} receiveResponseTask is closed after send {} requests and received {} responses, due to {}",
+ domainSocket.toString(), requestSent, responseReceived, e.getClass().getName(), e);
+ // fail all requests pending responses
+ sentRequests.values().forEach(i -> i.fail(e));
+ } catch (Throwable e) {
+ isDomainSocketOpen.set(false);
+ LOG.error("{} failed after send {} requests and received {} responses",
+ domainSocket.toString(), requestSent, responseReceived, e);
+ if (entry != null) {
+ entry.getFuture().completeExceptionally(e);
+ }
+ sentRequests.values().forEach(i -> i.fail(e));
+ break;
+ }
+ } while (isDomainSocketOpen.get());
+ }
+ }
+
+ public static InputStream vintPrefixed(final DataInputStream input) throws IOException {
+ final int firstByte = input.read();
+ int size = CodedInputStream.readRawVarint32(firstByte, input);
+ assert size >= 0;
+ return new LimitInputStream(input, size);
+ }
+
+ /**
+ * Class wraps a container command request.
+ */
+ public static class RequestEntry {
+ private ContainerCommandRequestProto request;
+ private CompletableFuture future;
+ private long createTimeNs;
+ private long sentTimeNs;
+ private TimerTask timerTask;
+
+ RequestEntry(ContainerCommandRequestProto requestProto,
+ CompletableFuture future) {
+ this.request = requestProto;
+ this.future = future;
+ this.createTimeNs = System.nanoTime();
+ }
+
+ public ContainerCommandRequestProto getRequest() {
+ return request;
+ }
+
+ public CompletableFuture getFuture() {
+ return future;
+ }
+
+ public long getCreateTimeNs() {
+ return createTimeNs;
+ }
+
+ public long getSentTimeNs() {
+ return sentTimeNs;
+ }
+
+ public void setSentTimeNs() {
+ sentTimeNs = System.nanoTime();
+ }
+
+ public void setTimerTask(TimerTask task) {
+ timerTask = task;
+ }
+
+ public TimerTask getTimerTask() {
+ return timerTask;
+ }
+
+ public void fail(Throwable e) {
+ timerTask.cancel();
+ future.completeExceptionally(e);
+ }
+ }
+}
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/client/HddsClientUtils.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/client/HddsClientUtils.java
index cc61b5371c60..efd7ea500ded 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/client/HddsClientUtils.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/client/HddsClientUtils.java
@@ -29,8 +29,8 @@
import org.apache.hadoop.hdds.conf.ConfigurationSource;
import org.apache.hadoop.hdds.ratis.conf.RatisClientConfig;
import org.apache.hadoop.hdds.scm.container.common.helpers.StorageContainerException;
-import org.apache.hadoop.io.retry.RetryPolicies;
import org.apache.hadoop.io.retry.RetryPolicy;
+import org.apache.hadoop.io_.retry.RetryPolicies;
import org.apache.hadoop.ozone.OzoneConfigKeys;
import org.apache.hadoop.ozone.OzoneConsts;
import org.apache.ratis.protocol.exceptions.AlreadyClosedException;
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockDataStreamOutput.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockDataStreamOutput.java
index 1ababc7a1d76..e3fff7528d4c 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockDataStreamOutput.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockDataStreamOutput.java
@@ -211,9 +211,12 @@ private DataStreamOutput setupStream(Pipeline pipeline) throws IOException {
// TODO: The datanode UUID is not used meaningfully, consider deprecating
// it or remove it completely if possible
String id = pipeline.getFirstNode().getUuidString();
+ ContainerProtos.Type streamInitType = config.isDatastreamPutBlockOnCloseEnabled()
+ ? ContainerProtos.Type.StreamInitWithPutBlock
+ : ContainerProtos.Type.StreamInit;
ContainerProtos.ContainerCommandRequestProto.Builder builder =
ContainerProtos.ContainerCommandRequestProto.newBuilder()
- .setCmdType(ContainerProtos.Type.StreamInit)
+ .setCmdType(streamInitType)
.setContainerID(blockID.get().getContainerID())
.setDatanodeUuid(id).setWriteChunk(writeChunkRequest);
@@ -416,6 +419,10 @@ public void executePutBlock(boolean close,
byteBufferList = null;
}
waitFuturesComplete();
+ if (close && config.isDatastreamPutBlockOnCloseEnabled()) {
+ // Wait for boundary PutBlock(s) before appending the stream-close PutBlock.
+ waitPutBlockFuturesComplete();
+ }
final BlockData blockData = containerBlockData.build();
if (close) {
// HDDS-12007 changed datanodes to ignore the following PutBlock request.
@@ -437,8 +444,12 @@ public void executePutBlock(boolean close,
}
}
});
+ if (config.isDatastreamPutBlockOnCloseEnabled()) {
+ // PutBlock is supposed to be committed after the data stream close so there
+ // is no need to continue.
+ return;
+ }
}
-
try {
XceiverClientReply asyncReply =
putBlockAsync(xceiverClient, blockData, close, tokenString);
@@ -545,6 +556,19 @@ public void waitFuturesComplete() throws IOException {
}
}
+ private void waitPutBlockFuturesComplete() throws IOException {
+ if (putBlockFutures.isEmpty()) {
+ return;
+ }
+ try {
+ CompletableFuture.allOf(putBlockFutures.toArray(EMPTY_FUTURE_ARRAY)).get();
+ checkOpen();
+ } catch (Exception e) {
+ LOG.warn("Failed to commit PutBlock before stream close: " + e);
+ throw new IOException(e);
+ }
+ }
+
/**
* @param close whether the flush is happening as part of closing the stream
*/
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockInputStream.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockInputStream.java
index 6f6b513422f7..9dcf7f66c26b 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockInputStream.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockInputStream.java
@@ -20,14 +20,17 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import java.io.EOFException;
+import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import org.apache.hadoop.hdds.client.BlockID;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.BlockData;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChunkInfo;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ContainerCommandResponseProto;
@@ -35,6 +38,7 @@
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.GetBlockResponseProto;
import org.apache.hadoop.hdds.scm.OzoneClientConfig;
import org.apache.hadoop.hdds.scm.XceiverClientFactory;
+import org.apache.hadoop.hdds.scm.XceiverClientShortCircuit;
import org.apache.hadoop.hdds.scm.XceiverClientSpi;
import org.apache.hadoop.hdds.scm.XceiverClientSpi.Validator;
import org.apache.hadoop.hdds.scm.container.common.helpers.StorageContainerException;
@@ -54,7 +58,7 @@
*/
public class BlockInputStream extends BlockExtendedInputStream {
- private static final Logger LOG = LoggerFactory.getLogger(BlockInputStream.class);
+ public static final Logger LOG = LoggerFactory.getLogger(BlockInputStream.class);
private static final List VALIDATORS =
ContainerProtocolCalls.toValidatorList((request, response) -> validate(response));
@@ -68,7 +72,10 @@ public class BlockInputStream extends BlockExtendedInputStream {
new AtomicReference<>();
private final boolean verifyChecksum;
private XceiverClientFactory xceiverClientFactory;
- private XceiverClientSpi xceiverClient;
+ private XceiverClientSpi xceiverClientGrpc;
+ private XceiverClientShortCircuit xceiverClientShortCircuit;
+ private final AtomicBoolean fallbackToGrpc = new AtomicBoolean(false);
+ private volatile FileInputStream blockFileInputStream;
private boolean initialized = false;
// TODO: do we need to change retrypolicy based on exception.
private final RetryPolicy retryPolicy;
@@ -230,14 +237,85 @@ protected BlockData getBlockData() throws IOException {
* @return BlockData.
*/
protected BlockData getBlockDataUsingClient() throws IOException {
- Pipeline pipeline = pipelineRef.get();
+ if (xceiverClientShortCircuit != null) {
+ try {
+ return getBlockDataUsingSCClient();
+ } catch (IOException e) {
+ if (e instanceof StorageContainerException) {
+ // getBlock may return exceptions like
+ // "StorageContainerException: Unable to find the block with bcsID 3275. Container 1 bcsId is 3261."
+ // when local datanode is not the leader of pipeline and hasn't finished the putBlock execution
+ // with the expected bcsID
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Failed to get blockData using short-circuit client", e);
+ }
+ } else {
+ LOG.warn("Failed to get blockData using short-circuit client", e);
+ }
+ // acquire client again if xceiverClientGrpc is not acquired.
+ acquireClient();
+ }
+ }
+ return getBlockDataUsingGRPCClient();
+ }
+
+ @VisibleForTesting
+ protected BlockData getBlockDataUsingSCClient() throws IOException {
+ final Pipeline pipeline = pipelineRef.get();
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Initializing BlockInputStream for get key to access {}",
+ blockID.getContainerID());
+ }
+
+ DatanodeBlockID.Builder blkIDBuilder =
+ DatanodeBlockID.newBuilder().setContainerID(blockID.getContainerID())
+ .setLocalID(blockID.getLocalID())
+ .setBlockCommitSequenceId(blockID.getBlockCommitSequenceId());
+
+ int replicaIndex = pipeline.getReplicaIndex(xceiverClientShortCircuit.getDn());
+ if (replicaIndex > 0) {
+ blkIDBuilder.setReplicaIndex(replicaIndex);
+ }
+ DatanodeBlockID datanodeBlockID = blkIDBuilder.build();
+ ContainerProtos.GetBlockRequestProto.Builder readBlockRequest =
+ ContainerProtos.GetBlockRequestProto.newBuilder().setBlockID(datanodeBlockID)
+ .setRequestShortCircuitAccess(true);
+ ContainerProtos.ContainerCommandRequestProto.Builder builder =
+ ContainerProtos.ContainerCommandRequestProto.newBuilder()
+ .setCmdType(ContainerProtos.Type.GetBlock)
+ .setContainerID(datanodeBlockID.getContainerID())
+ .setGetBlock(readBlockRequest)
+ .setClientId(xceiverClientShortCircuit.getClientId())
+ .setCallId(xceiverClientShortCircuit.getCallId());
+ if (tokenRef.get() != null) {
+ builder.setEncodedToken(tokenRef.get().encodeToUrlString());
+ }
+ GetBlockResponseProto response = ContainerProtocolCalls.getBlock(xceiverClientShortCircuit,
+ VALIDATORS, builder, xceiverClientShortCircuit.getDn());
+
+ blockFileInputStream = xceiverClientShortCircuit.getFileInputStream(builder.getCallId(), datanodeBlockID);
+ if (blockFileInputStream == null) {
+ throw new IOException("Failed to get file InputStream for block " + datanodeBlockID);
+ } else {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Get the FileInputStream of block {}", datanodeBlockID);
+ }
+ }
+ return response.getBlockData();
+ }
+
+ @VisibleForTesting
+ protected BlockData getBlockDataUsingGRPCClient() throws IOException {
+ final Pipeline pipeline = pipelineRef.get();
+
if (LOG.isDebugEnabled()) {
LOG.debug("Initializing BlockInputStream for get key to access block {}",
blockID);
}
GetBlockResponseProto response = ContainerProtocolCalls.getBlock(
- xceiverClient, VALIDATORS, blockID, tokenRef.get(), pipeline.getReplicaIndexes());
+ xceiverClientGrpc, VALIDATORS, blockID, tokenRef.get(), pipeline.getReplicaIndexes());
return response.getBlockData();
}
@@ -266,13 +344,35 @@ private static void validate(ContainerCommandResponseProto response)
}
private void acquireClient() throws IOException {
- if (xceiverClientFactory != null && xceiverClient == null) {
- final Pipeline pipeline = pipelineRef.get();
+ final Pipeline pipeline = pipelineRef.get();
+ // xceiverClientGrpc not-null indicates there is fall back to GRPC reads
+ if (xceiverClientFactory != null && xceiverClientFactory.isShortCircuitEnabled() && !fallbackToGrpc.get()
+ && xceiverClientShortCircuit == null) {
+ try {
+ XceiverClientSpi newClient = xceiverClientFactory.acquireClientForReadData(pipeline, true);
+ if (newClient instanceof XceiverClientShortCircuit) {
+ xceiverClientShortCircuit = (XceiverClientShortCircuit) newClient;
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("acquired short-circuit client {} for block {}", xceiverClientShortCircuit.toString(), blockID);
+ }
+ } else {
+ xceiverClientGrpc = newClient;
+ fallbackToGrpc.set(true);
+ }
+ return;
+ } catch (Exception e) {
+ LOG.warn("Failed to acquire {} client for pipeline {}, block {}. Fallback to Grpc client.",
+ DomainSocketFactory.FEATURE, pipeline, blockID, e);
+ fallbackToGrpc.set(true);
+ }
+ }
+
+ // fall back to acquire GRPC client
+ if (xceiverClientFactory != null && xceiverClientGrpc == null) {
try {
- xceiverClient = xceiverClientFactory.acquireClientForReadData(pipeline);
+ xceiverClientGrpc = xceiverClientFactory.acquireClientForReadData(pipeline);
} catch (IOException ioe) {
- LOG.warn("Failed to acquire client for pipeline {}, block {}",
- pipeline, blockID);
+ LOG.warn("Failed to acquire client for pipeline {}, block {}", pipeline, blockID);
throw ioe;
}
}
@@ -288,8 +388,14 @@ protected synchronized void addStream(ChunkInfo chunkInfo) {
}
protected ChunkInputStream createChunkInputStream(ChunkInfo chunkInfo) {
- return new ChunkInputStream(chunkInfo, blockID,
- xceiverClientFactory, pipelineRef::get, verifyChecksum, tokenRef::get);
+ if (blockFileInputStream != null) {
+ // a non-empty blockFileInputStream means we have a direct local block replica to read from
+ return new LocalChunkInputStream(chunkInfo, blockID, xceiverClientFactory,
+ pipelineRef::get, verifyChecksum, tokenRef::get, xceiverClientShortCircuit, blockFileInputStream);
+ } else {
+ return new ChunkInputStream(chunkInfo, blockID,
+ xceiverClientFactory, pipelineRef::get, verifyChecksum, tokenRef::get);
+ }
}
@Override
@@ -461,12 +567,23 @@ public synchronized void close() {
is.close();
}
}
+ if (blockFileInputStream != null) {
+ try {
+ blockFileInputStream.close();
+ } catch (IOException e) {
+ LOG.error("Failed to close file InputStream for block " + blockID, e);
+ }
+ }
}
private void releaseClient() {
- if (xceiverClientFactory != null && xceiverClient != null) {
- xceiverClientFactory.releaseClientForReadData(xceiverClient, false);
- xceiverClient = null;
+ if (xceiverClientFactory != null && xceiverClientGrpc != null) {
+ xceiverClientFactory.releaseClientForReadData(xceiverClientGrpc, false);
+ xceiverClientGrpc = null;
+ }
+ if (xceiverClientFactory != null && xceiverClientShortCircuit != null) {
+ xceiverClientFactory.releaseClientForReadData(xceiverClientShortCircuit, false);
+ xceiverClientShortCircuit = null;
}
}
@@ -500,6 +617,10 @@ synchronized long getBlockPosition() {
return blockPosition;
}
+ public FileInputStream getBlockFileInputStream() {
+ return blockFileInputStream;
+ }
+
@Override
public synchronized void unbuffer() {
storePosition();
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockOutputStream.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockOutputStream.java
index 432fee81d193..77a667259a77 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockOutputStream.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockOutputStream.java
@@ -597,7 +597,7 @@ CompletableFuture executePutBlock(boolean close,
// if block is full, send the eof
boolean isBlockFull = (blockSize != -1 && flushPos == blockSize);
- asyncReply = putBlockAsync(xceiverClient, blockData, close || isBlockFull, tokenString);
+ asyncReply = putBlockAsync(xceiverClient, blockData, close || isBlockFull, tokenString, containerAutoCreate());
CompletableFuture future = asyncReply.getResponse();
flushFuture = future.thenApplyAsync(e -> {
try {
@@ -967,7 +967,7 @@ private CompletableFuture writeChunkToContainer(
// once that context is wired through BlockOutputStream. Null preserves
// the current any-volume behavior.
asyncReply = writeChunkAsync(xceiverClient, chunkInfo,
- blockID.get(), data, tokenString, replicationIndex, blockData, close, null);
+ blockID.get(), data, tokenString, replicationIndex, blockData, close, null, containerAutoCreate());
CompletableFuture
respFuture = asyncReply.getResponse();
validateFuture = respFuture.thenApplyAsync(e -> {
@@ -1163,6 +1163,18 @@ private ChunkInfo createChunkInfo(long lastPartialChunkOffset)
return revisedChunkInfo.build();
}
+ /**
+ * @return true when the DataNode may auto-create a missing container for this write.
+ */
+ protected boolean containerAutoCreate() {
+ return true;
+ }
+
+ @VisibleForTesting
+ public boolean isContainerAutoCreate() {
+ return containerAutoCreate();
+ }
+
private boolean isFullChunk(ChunkInfo chunkInfo) {
Preconditions.checkState(
chunkInfo.getLen() <= config.getStreamBufferSize());
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ChunkInputStream.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ChunkInputStream.java
index 22917ce4b6c7..34ef7a71bd3f 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ChunkInputStream.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ChunkInputStream.java
@@ -412,7 +412,7 @@ private synchronized void readChunkFromContainer(int len) throws IOException {
adjustBufferPosition(startByteIndex - bufferOffsetWrtChunkData);
}
- private void readChunkDataIntoBuffers(ChunkInfo readChunkInfo)
+ protected void readChunkDataIntoBuffers(ChunkInfo readChunkInfo)
throws IOException {
buffers = readChunk(readChunkInfo);
buffersSize = readChunkInfo.getLen();
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/DomainPeer.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/DomainPeer.java
new file mode 100644
index 000000000000..9a173046ea9b
--- /dev/null
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/DomainPeer.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hadoop.hdds.scm.storage;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.ReadableByteChannel;
+import org.apache.hadoop.net.unix.DomainSocket;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Represents a peer that we communicate with by using blocking I/O
+ * on a UNIX domain socket.
+ */
+public class DomainPeer implements Closeable {
+ private final DomainSocket socket;
+ private final OutputStream out;
+ private final InputStream in;
+ private final ReadableByteChannel channel;
+ public static final Logger LOG = LoggerFactory.getLogger(DomainPeer.class);
+
+ public DomainPeer(DomainSocket socket) {
+ this.socket = socket;
+ this.out = socket.getOutputStream();
+ this.in = socket.getInputStream();
+ this.channel = socket.getChannel();
+ }
+
+ public ReadableByteChannel getInputStreamChannel() {
+ return channel;
+ }
+
+ public void setReadTimeout(int timeoutMs) throws IOException {
+ socket.setAttribute(DomainSocket.RECEIVE_TIMEOUT, timeoutMs);
+ }
+
+ public int getReceiveBufferSize() throws IOException {
+ return socket.getAttribute(DomainSocket.RECEIVE_BUFFER_SIZE);
+ }
+
+ public void setWriteTimeout(int timeoutMs) throws IOException {
+ socket.setAttribute(DomainSocket.SEND_TIMEOUT, timeoutMs);
+ }
+
+ public boolean isClosed() {
+ return !socket.isOpen();
+ }
+
+ @Override
+ public void close() throws IOException {
+ socket.close();
+ LOG.info("{} is closed", socket);
+ }
+
+ public String getRemoteAddressString() {
+ return "unix:{" + socket.toString() + "}";
+ }
+
+ public String getLocalAddressString() {
+ return "";
+ }
+
+ public InputStream getInputStream() throws IOException {
+ return in;
+ }
+
+ public OutputStream getOutputStream() throws IOException {
+ return out;
+ }
+
+ @Override
+ public String toString() {
+ return "DomainPeer(" + getRemoteAddressString() + ")";
+ }
+
+ public DomainSocket getDomainSocket() {
+ return socket;
+ }
+
+ public boolean hasSecureChannel() {
+ //
+ // Communication over domain sockets is assumed to be secure, since it
+ // doesn't pass over any network. We also carefully control the privileges
+ // that can be used on the domain socket inode and its parent directories.
+ // See #{java.org.apache.hadoop.net.unix.DomainSocket#validateSocketPathSecurity0}
+ // for details.
+ //
+ // So unless you are running as root or the user launches the service, you cannot
+ // launch a man-in-the-middle attach on UNIX domain socket traffic.
+ //
+ return true;
+ }
+}
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/DomainSocketFactory.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/DomainSocketFactory.java
new file mode 100644
index 000000000000..6ccaffe0fe7e
--- /dev/null
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/DomainSocketFactory.java
@@ -0,0 +1,276 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hadoop.hdds.scm.storage;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.SystemUtils;
+import org.apache.hadoop.hdds.conf.ConfigurationSource;
+import org.apache.hadoop.hdds.scm.OzoneClientConfig;
+import org.apache.hadoop.net.unix.DomainSocket;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A factory to help create DomainSocket.
+ */
+public final class DomainSocketFactory {
+ private static final Logger LOG = LoggerFactory.getLogger(
+ DomainSocketFactory.class);
+ public static final String FEATURE = "short-circuit reads";
+ public static final String FEATURE_FLAG = "SC";
+ private static boolean nativeLibraryLoaded = false;
+ private static String nativeLibraryLoadFailureReason;
+ private long pathExpireMills;
+ private final ConcurrentHashMap pathMap;
+ private Timer timer;
+ private boolean isEnabled = false;
+ private String domainSocketPath;
+ private static volatile DomainSocketFactory instance = null;
+
+ /**
+ * Domain socket path state.
+ */
+ public enum PathState {
+ NOT_CONFIGURED(false),
+ DISABLED(false),
+ VALID(true);
+
+ private final boolean usableForShortCircuit;
+
+ PathState(boolean usableForShortCircuit) {
+ this.usableForShortCircuit = usableForShortCircuit;
+ }
+
+ public boolean getUsableForShortCircuit() {
+ return usableForShortCircuit;
+ }
+ }
+
+ /**
+ * Domain socket path.
+ */
+ public static class PathInfo {
+ private static final PathInfo NOT_CONFIGURED = new PathInfo("", PathState.NOT_CONFIGURED);
+ private static final PathInfo DISABLED = new PathInfo("", PathState.DISABLED);
+ private static final PathInfo VALID = new PathInfo("", PathState.VALID);
+
+ private final String path;
+ private final PathState state;
+
+ PathInfo(String path, PathState state) {
+ this.path = path;
+ this.state = state;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public PathState getPathState() {
+ return state;
+ }
+
+ @Override
+ public String toString() {
+ return "PathInfo{path=" + path + ", state=" + state + "}";
+ }
+ }
+
+ static {
+ // Try to load native hadoop library and set fallback flag appropriately
+ if (SystemUtils.IS_OS_WINDOWS) {
+ nativeLibraryLoadFailureReason = "UNIX Domain sockets are not available on Windows.";
+ } else {
+ LOG.info("Trying to load the custom-built native-hadoop library...");
+ try {
+ System.loadLibrary("hadoop");
+ LOG.info("Loaded the native-hadoop library");
+ nativeLibraryLoaded = true;
+ } catch (Throwable t) {
+ // Ignore failure to continue
+ LOG.info("Failed to load native-hadoop with error: " + t);
+ LOG.info("java.library.path=" + System.getProperty("java.library.path"));
+ nativeLibraryLoadFailureReason = "libhadoop cannot be loaded.";
+ }
+
+ if (!nativeLibraryLoaded) {
+ LOG.warn("Unable to load native-hadoop library for your platform... " +
+ "using builtin-java classes where applicable");
+ }
+ }
+ }
+
+ public static DomainSocketFactory getInstance(ConfigurationSource conf) {
+ if (instance == null) {
+ synchronized (DomainSocketFactory.class) {
+ if (instance == null) {
+ instance = new DomainSocketFactory(conf);
+ }
+ }
+ }
+ return instance;
+ }
+
+ private DomainSocketFactory(ConfigurationSource conf) {
+ OzoneClientConfig clientConfig = conf.getObject(OzoneClientConfig.class);
+ boolean shortCircuitEnabled = clientConfig.isShortCircuitEnabled();
+ domainSocketPath = conf.get(OzoneClientConfig.OZONE_DOMAIN_SOCKET_PATH);
+ PathInfo pathInfo;
+ long startTime = System.nanoTime();
+ if (!shortCircuitEnabled) {
+ LOG.info(FEATURE + " is disabled.");
+ pathInfo = PathInfo.NOT_CONFIGURED;
+ domainSocketPath = Strings.isNullOrEmpty(domainSocketPath) ? "" : domainSocketPath;
+ } else {
+ if (Strings.isNullOrEmpty(domainSocketPath)) {
+ throw new IllegalArgumentException(FEATURE + " is enabled but "
+ + OzoneClientConfig.OZONE_DOMAIN_SOCKET_PATH + " is not set.");
+ } else if (!nativeLibraryLoaded) {
+ LOG.warn(FEATURE + " cannot be used because " + nativeLibraryLoadFailureReason);
+ pathInfo = PathInfo.DISABLED;
+ } else {
+ pathInfo = PathInfo.VALID;
+ isEnabled = true;
+ timer = new Timer(DomainSocketFactory.class.getSimpleName() + "-Timer");
+ LOG.info(FEATURE + " is enabled within {} ns.", System.nanoTime() - startTime);
+ }
+ }
+ pathExpireMills = clientConfig.getShortCircuitReadDisableInterval() * 1000;
+ pathMap = new ConcurrentHashMap<>();
+ pathMap.put(domainSocketPath, pathInfo);
+ }
+
+ public boolean isServiceEnabled() {
+ return isEnabled;
+ }
+
+ public boolean isServiceReady() {
+ if (isEnabled) {
+ PathInfo status = pathMap.get(domainSocketPath);
+ return status.getPathState() == PathState.VALID;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get information about a domain socket path. Caller must make sure that addr is a local address.
+ *
+ * @param addr The local inet address to use.
+ * @return Information about the socket path.
+ */
+ public PathInfo getPathInfo(InetSocketAddress addr) {
+ if (!isEnabled) {
+ return PathInfo.NOT_CONFIGURED;
+ }
+
+ if (!isServiceReady()) {
+ return PathInfo.DISABLED;
+ }
+
+ String escapedPath = DomainSocket.getEffectivePath(domainSocketPath, addr.getPort());
+ PathInfo status = pathMap.get(escapedPath);
+ if (status == null) {
+ PathInfo pathInfo = new PathInfo(escapedPath, PathState.VALID);
+ pathMap.putIfAbsent(escapedPath, pathInfo);
+ return pathInfo;
+ } else {
+ return status;
+ }
+ }
+
+ /**
+ * Create DomainSocket for addr. Caller must make sure that addr is a local address.
+ */
+ public DomainSocket createSocket(int readTimeoutMs, int writeTimeoutMs, InetSocketAddress addr) throws IOException {
+ if (!isEnabled || !isServiceReady()) {
+ return null;
+ }
+ boolean success = false;
+ DomainSocket sock = null;
+ String escapedPath = null;
+ long startTime = System.nanoTime();
+ try {
+ escapedPath = DomainSocket.getEffectivePath(domainSocketPath, addr.getPort());
+ sock = DomainSocket.connect(escapedPath);
+ sock.setAttribute(DomainSocket.RECEIVE_TIMEOUT, readTimeoutMs);
+ sock.setAttribute(DomainSocket.SEND_TIMEOUT, writeTimeoutMs);
+ success = true;
+ LOG.info("{} is created within {} ns", sock, System.nanoTime() - startTime);
+ } catch (IOException e) {
+ LOG.error("Failed to create DomainSocket", e);
+ throw e;
+ } finally {
+ if (!success) {
+ if (sock != null) {
+ IOUtils.closeQuietly(sock);
+ }
+ if (escapedPath != null) {
+ pathMap.put(escapedPath, PathInfo.DISABLED);
+ LOG.error("{} is disabled for {} ms due to current failure", escapedPath, pathExpireMills);
+ schedulePathEnable(escapedPath, pathExpireMills);
+ }
+ sock = null;
+ }
+ }
+ return sock;
+ }
+
+ public void disableShortCircuit() {
+ pathMap.put(domainSocketPath, PathInfo.DISABLED);
+ schedulePathEnable(domainSocketPath, pathExpireMills);
+ }
+
+ private void schedulePathEnable(String path, long delayMills) {
+ timer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ pathMap.put(path, PathInfo.VALID);
+ }
+ }, delayMills);
+ }
+
+ @VisibleForTesting
+ public void clearPathMap() {
+ pathMap.clear();
+ }
+
+ public long getPathExpireMills() {
+ return pathExpireMills;
+ }
+
+ public Timer getTimer() {
+ return timer;
+ }
+
+ public static synchronized void close() {
+ if (instance != null) {
+ if (instance.getTimer() != null) {
+ instance.getTimer().cancel();
+ }
+ DomainSocketFactory.instance = null;
+ }
+ }
+}
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ECBlockOutputStream.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ECBlockOutputStream.java
index d798b3a9385a..5f33e02f4d50 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ECBlockOutputStream.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ECBlockOutputStream.java
@@ -59,6 +59,7 @@
public class ECBlockOutputStream extends BlockOutputStream {
private final DatanodeDetails datanodeDetails;
+ private final boolean containerAutoCreate;
private CompletableFuture
currentChunkRspFuture = null;
@@ -83,11 +84,33 @@ public ECBlockOutputStream(
Token extends TokenIdentifier> token,
ContainerClientMetrics clientMetrics, StreamBufferArgs streamBufferArgs,
Supplier executorServiceSupplier
+ ) throws IOException {
+ this(blockID, xceiverClientManager, pipeline, bufferPool, config, token, clientMetrics,
+ streamBufferArgs, executorServiceSupplier, true);
+ }
+
+ @SuppressWarnings("checkstyle:ParameterNumber")
+ public ECBlockOutputStream(
+ BlockID blockID,
+ XceiverClientFactory xceiverClientManager,
+ Pipeline pipeline,
+ BufferPool bufferPool,
+ OzoneClientConfig config,
+ Token extends TokenIdentifier> token,
+ ContainerClientMetrics clientMetrics, StreamBufferArgs streamBufferArgs,
+ Supplier executorServiceSupplier,
+ boolean containerAutoCreate
) throws IOException {
super(blockID, -1, xceiverClientManager,
pipeline, bufferPool, config, token, clientMetrics, streamBufferArgs, executorServiceSupplier);
// In EC stream, there will be only one node in pipeline.
this.datanodeDetails = pipeline.getClosestNode();
+ this.containerAutoCreate = containerAutoCreate;
+ }
+
+ @Override
+ protected boolean containerAutoCreate() {
+ return containerAutoCreate;
}
@Override
@@ -272,7 +295,7 @@ public CompletableFuture executePutBlock(boolean close,
try {
ContainerProtos.BlockData blockData = getContainerBlockData().build();
XceiverClientReply asyncReply =
- putBlockAsync(getXceiverClient(), blockData, close, getTokenString());
+ putBlockAsync(getXceiverClient(), blockData, close, getTokenString(), containerAutoCreate());
CompletableFuture future =
asyncReply.getResponse();
flushFuture = future.thenApplyAsync(e -> {
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/LocalChunkInputStream.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/LocalChunkInputStream.java
new file mode 100644
index 000000000000..9de58ac7fe9a
--- /dev/null
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/LocalChunkInputStream.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hadoop.hdds.scm.storage;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Supplier;
+import org.apache.hadoop.fs.ByteBufferReadable;
+import org.apache.hadoop.fs.CanUnbuffer;
+import org.apache.hadoop.fs.Seekable;
+import org.apache.hadoop.hdds.client.BlockID;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChunkInfo;
+import org.apache.hadoop.hdds.scm.XceiverClientFactory;
+import org.apache.hadoop.hdds.scm.XceiverClientShortCircuit;
+import org.apache.hadoop.hdds.scm.XceiverClientSpi.ShortCircuitValidator;
+import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.hadoop.ozone.common.Checksum;
+import org.apache.hadoop.ozone.common.ChecksumData;
+import org.apache.hadoop.ozone.common.OzoneChecksumException;
+import org.apache.hadoop.ozone.common.utils.BufferUtils;
+import org.apache.hadoop.security.token.Token;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An {@link InputStream} called from BlockInputStream to read a chunk from the local
+ * block replica directly. Each chunk may contain multiple underlying {@link ByteBuffer}
+ * instances.
+ */
+public class LocalChunkInputStream extends ChunkInputStream
+ implements Seekable, CanUnbuffer, ByteBufferReadable {
+
+ private final ChunkInfo chunkInfo;
+ private final FileChannel dataIn;
+ private final ShortCircuitValidator validator;
+ private final boolean verifyChecksum;
+ public static final Logger LOG =
+ LoggerFactory.getLogger(LocalChunkInputStream.class);
+
+ @SuppressWarnings("checkstyle:parameternumber")
+ LocalChunkInputStream(ChunkInfo chunkInfo, BlockID blockId, XceiverClientFactory xceiverClientFactory,
+ Supplier pipelineSupplier, boolean verifyChecksum, Supplier> tokenSupplier,
+ XceiverClientShortCircuit xceiverClientShortCircuit, FileInputStream blockInputStream) {
+ super(chunkInfo, blockId, xceiverClientFactory, pipelineSupplier, verifyChecksum, tokenSupplier);
+ this.chunkInfo = chunkInfo;
+ this.dataIn = blockInputStream.getChannel();
+ this.validator = this::validateChunk;
+ this.verifyChecksum = verifyChecksum;
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("{} is created for {}", LocalChunkInputStream.class.getSimpleName(), blockId);
+ }
+ }
+
+ /**
+ * Get the chunk from the local block replica.
+ */
+ @VisibleForTesting
+ @Override
+ protected ByteBuffer[] readChunk(ChunkInfo readChunkInfo)
+ throws IOException {
+ int bytesPerChecksum = chunkInfo.getChecksumData().getBytesPerChecksum();
+ final ByteBuffer[] buffers = BufferUtils.assignByteBuffers(readChunkInfo.getLen(),
+ bytesPerChecksum);
+ dataIn.position(readChunkInfo.getOffset()).read(buffers);
+ Arrays.stream(buffers).forEach(ByteBuffer::flip);
+ validator.accept(Arrays.asList(buffers), readChunkInfo);
+ return buffers;
+ }
+
+ private void validateChunk(List bufferList, ChunkInfo readChunkInfo)
+ throws OzoneChecksumException {
+ if (verifyChecksum) {
+ ChecksumData checksumData = ChecksumData.getFromProtoBuf(
+ chunkInfo.getChecksumData());
+
+ // ChecksumData stores checksum for each 'numBytesPerChecksum'
+ // number of bytes in a list. Compute the index of the first
+ // checksum to match with the read data
+
+ long relativeOffset = readChunkInfo.getOffset() -
+ chunkInfo.getOffset();
+ int bytesPerChecksum = checksumData.getBytesPerChecksum();
+ int startIndex = (int) (relativeOffset / bytesPerChecksum);
+ Checksum.verifyChecksum(bufferList, startIndex, checksumData);
+ }
+ }
+
+ /**
+ * Acquire short-circuit local read client.
+ */
+ @Override
+ protected synchronized void acquireClient() throws IOException {
+ // do nothing, read data doesn't need short-circuit client
+ }
+}
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/MultipartInputStream.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/MultipartInputStream.java
index 221a48be828d..075ab08fe5a6 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/MultipartInputStream.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/MultipartInputStream.java
@@ -196,6 +196,10 @@ public boolean readFully(long position, ByteBuffer buffer) throws IOException {
final long oldPos = getPos();
seek(position);
try {
+ int remainingBeforeRead = buffer.remaining();
+ if (remainingBeforeRead == 0) {
+ return true;
+ }
read(new ByteBufferReader(buffer) {
@Override
int readImpl(InputStream inputStream) throws IOException {
@@ -203,6 +207,10 @@ int readImpl(InputStream inputStream) throws IOException {
.readFully(getBuffer(), false);
}
});
+ if (remainingBeforeRead - buffer.remaining() == 0) {
+ throw new EOFException("EOF encountered at pos: " + position +
+ " for key: " + key);
+ }
} finally {
seek(oldPos);
}
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/StreamBlockInputStream.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/StreamBlockInputStream.java
index 8a029f871719..c15cd338908d 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/StreamBlockInputStream.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/StreamBlockInputStream.java
@@ -23,6 +23,8 @@
import java.io.IOException;
import java.nio.ByteBuffer;
import java.time.Duration;
+import java.util.HashSet;
+import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@@ -36,6 +38,8 @@
import org.apache.hadoop.fs.FSExceptionMessages;
import org.apache.hadoop.hdds.StringUtils;
import org.apache.hadoop.hdds.client.BlockID;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.DatanodeID;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ReadBlockResponseProto;
import org.apache.hadoop.hdds.scm.OzoneClientConfig;
@@ -47,6 +51,7 @@
import org.apache.hadoop.hdds.scm.container.common.helpers.StorageContainerException;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
import org.apache.hadoop.hdds.security.token.OzoneBlockTokenIdentifier;
+import org.apache.hadoop.hdds.utils.ConnectionFailureUtils;
import org.apache.hadoop.io.retry.RetryPolicy;
import org.apache.hadoop.ozone.common.Checksum;
import org.apache.hadoop.ozone.common.ChecksumData;
@@ -91,6 +96,7 @@ public class StreamBlockInputStream extends BlockExtendedInputStream {
private final Function refreshFunction;
private final RetryPolicy retryPolicy;
private int retries = 0;
+ private final Set failedStreamingDatanodes = new HashSet<>();
public StreamBlockInputStream(
BlockID blockID, long length, Pipeline pipeline,
@@ -171,13 +177,19 @@ private synchronized boolean dataAvailableToRead(int length, boolean preRead) th
if (position >= blockLength) {
return false;
}
- initialize();
-
- if (bufferHasRemaining()) {
- return true;
+ while (true) {
+ try {
+ initialize();
+ if (bufferHasRemaining()) {
+ return true;
+ }
+ buffer = streamingReader.read(length, preRead);
+ retries = 0;
+ return bufferHasRemaining();
+ } catch (IOException ex) {
+ handleExceptions(ex);
+ }
}
- buffer = streamingReader.read(length, preRead);
- return bufferHasRemaining();
}
private synchronized void advancePosition(long delta) {
@@ -293,7 +305,7 @@ private synchronized void initialize() throws IOException {
try {
acquireClient();
final StreamingReader reader = new StreamingReader();
- xceiverClient.initStreamRead(blockID, reader);
+ xceiverClient.initStreamRead(blockID, reader, failedStreamingDatanodes);
streamingReader = reader;
} catch (IOException ioe) {
handleExceptions(ioe);
@@ -327,10 +339,14 @@ synchronized void readBlockImpl(long length) throws IOException {
}
private void handleExceptions(IOException cause) throws IOException {
- if (cause instanceof StorageContainerException || isConnectivityIssue(cause)) {
- if (shouldRetryRead(cause, retryPolicy, retries++)) {
+ IOException root = ConnectionFailureUtils.unwrapCause(cause);
+ if (root instanceof StorageContainerException || isConnectivityIssue(root) ||
+ root instanceof TimeoutIOException) {
+ if (shouldRetryRead(root, retryPolicy, retries++)) {
+ recordFailedStreamingDatanode();
releaseClient();
- refreshBlockInfo(cause);
+ refreshBlockInfo(root);
+ requestedLength = position;
LOG.warn("Refreshing block data to read block {} due to {}", blockID, cause.getMessage());
} else {
throw cause;
@@ -340,6 +356,21 @@ private void handleExceptions(IOException cause) throws IOException {
}
}
+ private void recordFailedStreamingDatanode() {
+ if (streamingReader == null) {
+ return;
+ }
+ final StreamingReadResponse response = streamingReader.getResponse();
+ if (response == null) {
+ return;
+ }
+ final DatanodeDetails dn = response.getDatanodeDetails();
+ if (failedStreamingDatanodes.add(dn.getID())) {
+ LOG.warn("Excluding DataNode {} from streaming read retries for block {}",
+ dn, blockID);
+ }
+ }
+
protected synchronized void releaseClient() {
if (xceiverClientFactory != null && xceiverClient != null) {
closeStream();
@@ -411,9 +442,6 @@ ReadBlockResponseProto poll() throws IOException {
while (true) {
checkError();
- if (future.isDone()) {
- return null; // Stream ended
- }
final ReadBlockResponseProto proto;
try {
@@ -426,6 +454,13 @@ ReadBlockResponseProto poll() throws IOException {
return proto;
}
+ // Check isDone only after confirming the queue is empty. If isDone() were
+ // checked first, an item delivered by onNext() just before onCompleted()
+ // fired would be silently dropped, causing data corruption.
+ if (future.isDone()) {
+ return null; // Stream ended, queue is empty
+ }
+
final long elapsedNanos = System.nanoTime() - startTime;
if (elapsedNanos >= readTimeoutNanos) {
setFailedAndThrow(new TimeoutIOException(
@@ -438,24 +473,34 @@ ReadBlockResponseProto poll() throws IOException {
private ByteBuffer read(int length, boolean preRead) throws IOException {
checkError();
if (future.isDone()) {
- return null; // Stream ended
+ // Don't return null while items remain in the queue. onNext() may have delivered items just before
+ // onCompleted() fired.
+ return responseQueue.isEmpty() ? null : readFromQueue();
}
readBlock(length, preRead);
while (true) {
final ByteBuffer buf = readFromQueue();
- if (buf != null && buf.hasRemaining()) {
+ if (buf == null) {
+ return null; // Stream ended
+ }
+ if (buf.hasRemaining()) {
return buf;
}
+ // buf is empty: the server aligned its response to a checksum boundary
+ // before our current position and all bytes were skipped. Fetch the next
+ // response, which should start at or after our position.
}
}
ByteBuffer readFromQueue() throws IOException {
final ReadBlockResponseProto readBlock = poll();
+ if (readBlock == null) {
+ return null; // Stream ended
+ }
// The server always returns data starting from the last checksum boundary. Therefore if the reader position is
// ahead of the position we received from the server, we need to adjust the buffer position accordingly.
- // If the reader position is behind
final ByteString data = readBlock.getData();
final ByteBuffer dataBuffer = data.asReadOnlyByteBuffer();
final long blockOffset = readBlock.getOffset();
@@ -491,15 +536,18 @@ public void onNext(ContainerProtos.ContainerCommandResponseProto containerComman
}
offerToQueue(readBlock);
} catch (Exception e) {
+ // Record the failure first: the log and observer calls below must not mask it.
+ setFailed(e);
final ByteString data = readBlock.getData();
final long offset = readBlock.getOffset();
final StreamingReadResponse r = getResponse();
LOG.warn("Failed to process block {} response at offset={}, size={}: {}, {}",
getBlockID().getContainerBlockID(),
- offset, data.size(), StringUtils.bytes2Hex(data.substring(0, 10).asReadOnlyByteBuffer()),
+ offset, data.size(), StringUtils.bytes2Hex(data.asReadOnlyByteBuffer(), 10),
readBlock.getChecksumData(), e);
- setFailed(e);
- r.getRequestObserver().onError(e);
+ if (r != null) {
+ r.getRequestObserver().onError(e);
+ }
releaseResources();
}
}
diff --git a/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockReconstructedStripeInputStream.java b/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockReconstructedStripeInputStream.java
index 73eaa7b74468..c71db0e41ed4 100644
--- a/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockReconstructedStripeInputStream.java
+++ b/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockReconstructedStripeInputStream.java
@@ -597,13 +597,13 @@ protected void loadDataBuffersFromStream()
} catch (ExecutionException ee) {
boolean added = failedDataIndexes.add(index);
Throwable t = ee.getCause() != null ? ee.getCause() : ee;
- String msg = "{}: error reading [{}]";
+ StringBuilder msg = new StringBuilder("{}: error reading [{}]");
if (added) {
- msg += ", marked as failed";
+ msg.append(", marked as failed");
} else {
- msg += ", already had failed"; // should not really happen
+ msg.append(", already had failed"); // should not really happen
}
- LOG.info(msg, this, index, t);
+ LOG.info(msg.toString(), this, index, t);
exceptionOccurred = true;
} catch (InterruptedException ie) {
diff --git a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/TestOzoneClientConfig.java b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/TestOzoneClientConfig.java
index 5c1eeccff919..cb05d31a2d6e 100644
--- a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/TestOzoneClientConfig.java
+++ b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/TestOzoneClientConfig.java
@@ -91,4 +91,21 @@ public void testStreamReadConfigParsing() {
assertEquals(2 << 20, clientConfig.getStreamReadResponseDataSize());
assertEquals(Duration.ofSeconds(5), clientConfig.getStreamReadTimeout());
}
+
+ @Test
+ void testDatastreamPutBlockOnCloseEnabledDefault() {
+ OzoneClientConfig subject = new OzoneConfiguration()
+ .getObject(OzoneClientConfig.class);
+ assertFalse(subject.isDatastreamPutBlockOnCloseEnabled());
+ }
+
+ @Test
+ void testDatastreamPutBlockOnCloseConfigParsing() {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.setBoolean("ozone.client.datastream.putblock.on.close.enabled", true);
+
+ OzoneClientConfig subject = conf.getObject(OzoneClientConfig.class);
+
+ assertTrue(subject.isDatastreamPutBlockOnCloseEnabled());
+ }
}
diff --git a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/TestXceiverClientGrpcChannel.java b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/TestXceiverClientGrpcChannel.java
new file mode 100644
index 000000000000..7b84dff590d8
--- /dev/null
+++ b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/TestXceiverClientGrpcChannel.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hadoop.hdds.scm;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+import org.apache.hadoop.hdds.HddsConfigKeys;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.DatanodeID;
+import org.apache.hadoop.hdds.protocol.MockDatanodeDetails;
+import org.apache.hadoop.hdds.scm.pipeline.MockPipeline;
+import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.ratis.thirdparty.io.grpc.ManagedChannel;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class TestXceiverClientGrpcChannel {
+
+ private static final int PORT = 9882;
+ private static final String HOSTNAME = "dn-host.example.com";
+ private static final String IP_ADDRESS = "192.168.1.100";
+
+ @ParameterizedTest(name = "useDatanodeHostname={0}")
+ @ValueSource(booleans = {false, true})
+ void createChannelUsesConfiguredAddress(boolean useHostname) throws IOException, InterruptedException {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.setBoolean(HddsConfigKeys.HDDS_DATANODE_USE_DN_HOSTNAME, useHostname);
+
+ DatanodeDetails dn = datanodeWithDistinctHostAndIp();
+ Pipeline pipeline = MockPipeline.createPipeline(Collections.singletonList(dn));
+
+ try (XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf)) {
+ ManagedChannel channel = client.createChannel(dn, PORT).build();
+ try {
+ String expectedHost = useHostname ? HOSTNAME : IP_ADDRESS;
+ assertThat(channel.authority()).isEqualTo(expectedHost + ":" + PORT);
+ } finally {
+ channel.shutdownNow();
+ channel.awaitTermination(5, TimeUnit.SECONDS);
+ }
+ }
+ }
+
+ private static DatanodeDetails datanodeWithDistinctHostAndIp() {
+ return MockDatanodeDetails.createDatanodeDetails(
+ DatanodeID.randomID(), HOSTNAME, IP_ADDRESS, "/rack");
+ }
+
+}
diff --git a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/TestXceiverClientManagerCloseContention.java b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/TestXceiverClientManagerCloseContention.java
new file mode 100644
index 000000000000..b817f73cb76d
--- /dev/null
+++ b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/TestXceiverClientManagerCloseContention.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hadoop.hdds.scm;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.apache.hadoop.hdds.conf.ConfigurationSource;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.scm.XceiverClientManager.ScmClientConfig;
+import org.apache.hadoop.hdds.scm.XceiverClientManager.XceiverClientManagerConfigBuilder;
+import org.apache.hadoop.hdds.scm.client.ClientTrustManager;
+import org.apache.hadoop.hdds.scm.pipeline.MockPipeline;
+import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Regression test for HDDS-15849.
+ *
+ *
HDDS-14571 changed {@link XceiverClientGrpc#close()} from a forced
+ * {@code shutdownNow()} (which cancels in-flight RPCs and terminates in ~1ms)
+ * to a graceful {@code shutdown()} followed by a polling loop that always
+ * sleeps at least {@code SHUTDOWN_WAIT_INTERVAL_MILLIS} (100ms) before it can
+ * observe channel termination. Every {@code close()} therefore costs ≥100ms
+ * instead of ~1ms.
+ *
+ *
The damage is amplified by where {@code close()} runs. Clients
+ * are torn down through {@link XceiverClientManager}'s cache eviction, and the
+ * whole acquire / evict / close sequence executes under the {@code clientCache}
+ * monitor:
+ *
+ * So the blocking graceful-shutdown wait runs while the {@code clientCache}
+ * monitor is held, and every other thread in {@code acquireClient()} /
+ * {@code releaseClient()} serializes behind it.
+ *
+ *
This test exercises the real {@code XceiverClientManager} and
+ * {@code XceiverClientGrpc.close()} — nothing is emulated. Real (lazily
+ * connected, plaintext) gRPC channels are created against random local
+ * datanode addresses; no live datanode is required because
+ * {@code close()} only shuts the channels down. {@code maxCacheSize == 1} plus
+ * a distinct pipeline per iteration forces the previously cached client to be
+ * evicted and closed on essentially every acquisition — mirroring the per-file
+ * client churn of EC checksum collection.
+ *
+ *
Run it against the pre-fix {@code close()} and the wall-clock is dominated
+ * by serialized 100ms sleeps ({@code ~ evictions * 100ms}); run it against the
+ * fix (immediate {@code shutdownNow()}) and per-close cost collapses. The
+ * assertion below encodes the fixed expectation, so this test FAILS on the
+ * regression and PASSES once HDDS-15849 restores immediate termination.
+ */
+class TestXceiverClientManagerCloseContention {
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(TestXceiverClientManagerCloseContention.class);
+
+ private static final int THREADS = 8;
+ private static final int CYCLES_PER_THREAD = 8;
+
+ /**
+ * Upper bound on acceptable average wall-clock cost per eviction-driven
+ * close(). The pre-fix code has a hard ≥100ms floor per close (the
+ * mandatory Thread.sleep in the termination polling loop); immediate
+ * shutdownNow() is well under a millisecond. 40ms sits safely between the
+ * two so the assertion is not timing-flaky.
+ */
+ private static final long MAX_MILLIS_PER_CLOSE = 40;
+
+ @Test
+ void evictionDrivenCloseDoesNotSerializeAcquisition() throws Exception {
+ ConfigurationSource conf = new OzoneConfiguration();
+ // maxCacheSize == 1 => each acquisition of a new pipeline evicts (and thus
+ // closes) the previously cached client.
+ ScmClientConfig clientConf = new XceiverClientManagerConfigBuilder()
+ .setMaxCacheSize(1)
+ .setStaleThresholdMs(TimeUnit.SECONDS.toMillis(30))
+ .build();
+
+ try (XceiverClientManager manager =
+ new XceiverClientManager(conf, clientConf, (ClientTrustManager) null)) {
+
+ ExecutorService pool = Executors.newFixedThreadPool(THREADS);
+ CountDownLatch start = new CountDownLatch(1);
+ long wallStartNanos;
+ try {
+ List> futures = IntStream.range(0, THREADS)
+ .mapToObj(t -> pool.submit(() -> {
+ start.await();
+ for (int i = 0; i < CYCLES_PER_THREAD; i++) {
+ // Distinct pipeline per cycle => distinct cache key => the
+ // previously cached client is evicted and closed.
+ Pipeline pipeline = MockPipeline.createPipeline(1);
+ XceiverClientSpi client = manager.acquireClient(pipeline);
+ manager.releaseClient(client, false);
+ }
+ return null;
+ }))
+ .collect(Collectors.toList());
+
+ wallStartNanos = System.nanoTime();
+ start.countDown();
+ for (Future> f : futures) {
+ f.get(120, TimeUnit.SECONDS);
+ }
+ } finally {
+ pool.shutdownNow();
+ }
+ long wallMillis =
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - wallStartNanos);
+
+ // recordStats() is enabled on the cache; each eviction drives one close().
+ long evictions = manager.getClientCache().stats().evictionCount();
+ long millisPerClose = evictions == 0 ? 0 : wallMillis / evictions;
+
+ LOG.info("threads={} cyclesPerThread={} acquisitions={} evictions(closes)={}",
+ THREADS, CYCLES_PER_THREAD, THREADS * CYCLES_PER_THREAD, evictions);
+ LOG.info("wall={}ms ms-per-close={}ms (regression floor is ~100ms/close)",
+ wallMillis, millisPerClose);
+
+ assertThat(evictions)
+ .as("cache eviction should have driven close() calls")
+ .isPositive();
+
+ assertThat(millisPerClose)
+ .as("HDDS-15849: eviction-driven close() must not serialize "
+ + "acquisition under the clientCache monitor; the pre-fix "
+ + "graceful-shutdown wait forces a ~100ms floor per close")
+ .isLessThan(MAX_MILLIS_PER_CLOSE);
+ }
+ }
+}
diff --git a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/client/TestHddsClientUtils.java b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/client/TestHddsClientUtils.java
index a18d008ab8eb..647ae727465a 100644
--- a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/client/TestHddsClientUtils.java
+++ b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/client/TestHddsClientUtils.java
@@ -127,6 +127,13 @@ private void checkAddr(OzoneConfiguration conf, String address, int port) {
assertEquals(port, scmAddr.getPort());
}
+ private void checkScmClientAddr(String confKey, String value,
+ String expectedHost, int expectedPort) {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.set(confKey, value);
+ checkAddr(conf, expectedHost, expectedPort);
+ }
+
@Test
public void testBlockClientFallbackToClientNoPort() {
// When OZONE_SCM_BLOCK_CLIENT_ADDRESS_KEY is undefined it should
@@ -175,6 +182,36 @@ public void testClientFallbackToScmNamesWithPort() {
assertEquals(OZONE_SCM_CLIENT_PORT_DEFAULT, socketAddress.getPort());
}
+ @Test
+ public void testClientAddressIPv6() {
+ // Bare IPv6 literal without port: port falls back to the default and the
+ // host must be re-bracketed before the address string is parsed.
+ checkScmClientAddr(OZONE_SCM_CLIENT_ADDRESS_KEY, "2001:db8::1",
+ "2001:db8:0:0:0:0:0:1", OZONE_SCM_CLIENT_PORT_DEFAULT);
+
+ // Bracketed IPv6 literal with explicit port.
+ checkScmClientAddr(OZONE_SCM_CLIENT_ADDRESS_KEY, "[2001:db8::1]:9876",
+ "2001:db8:0:0:0:0:0:1", 9876);
+
+ // Bracketed IPv6 literal without port (host:port documents port as
+ // optional).
+ checkScmClientAddr(OZONE_SCM_CLIENT_ADDRESS_KEY, "[2001:db8::1]",
+ "2001:db8:0:0:0:0:0:1", OZONE_SCM_CLIENT_PORT_DEFAULT);
+ }
+
+ @Test
+ public void testClientFallbackToScmNamesIPv6() {
+ // Bare IPv6 literal in ozone.scm.names.
+ checkScmClientAddr(OZONE_SCM_NAMES, "2001:db8::1",
+ "2001:db8:0:0:0:0:0:1", OZONE_SCM_CLIENT_PORT_DEFAULT);
+
+ // On the ozone.scm.names fallback path an inline port is ignored and the
+ // default client port is used instead (same semantics as
+ // testClientFallbackToScmNamesWithPort).
+ checkScmClientAddr(OZONE_SCM_NAMES, "[2001:db8::1]:300",
+ "2001:db8:0:0:0:0:0:1", OZONE_SCM_CLIENT_PORT_DEFAULT);
+ }
+
@Test
@SuppressWarnings("StringSplitter")
public void testBlockClientFallbackToClientWithPort() {
diff --git a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/MockDatanodePipeline.java b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/MockDatanodePipeline.java
new file mode 100644
index 000000000000..c1c11b0d1a83
--- /dev/null
+++ b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/MockDatanodePipeline.java
@@ -0,0 +1,313 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hadoop.hdds.scm.storage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Supplier;
+import org.apache.hadoop.hdds.client.BlockID;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ContainerCommandRequestProto;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ContainerCommandResponseProto;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.GetCommittedBlockLengthResponseProto;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.PutBlockResponseProto;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.Result;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.Type;
+import org.apache.hadoop.hdds.ratis.ContainerCommandRequestMessage;
+import org.apache.hadoop.hdds.scm.XceiverClientFactory;
+import org.apache.hadoop.hdds.scm.XceiverClientManager;
+import org.apache.hadoop.hdds.scm.XceiverClientRatis;
+import org.apache.hadoop.hdds.scm.XceiverClientReply;
+import org.apache.hadoop.hdds.scm.pipeline.MockPipeline;
+import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.ratis.client.api.DataStreamApi;
+import org.apache.ratis.client.api.DataStreamOutput;
+import org.apache.ratis.io.FilePositionCount;
+import org.apache.ratis.io.StandardWriteOption;
+import org.apache.ratis.io.WriteOption;
+import org.apache.ratis.protocol.DataStreamReply;
+import org.apache.ratis.protocol.RoutingTable;
+import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
+
+/**
+ * A stateful test harness that simulates a datanode pipeline for {@link BlockDataStreamOutput} unit tests.
+ * Replaces a real Ratis pipeline with mocked {@link XceiverClientRatis} and a concrete {@link DataStreamOutput}
+ * implementation.
+ *
+ *
Tracks all chunks written, putBlock calls, and watchForCommit calls.
+ * Configurable failure injection for each operation.
+ */
+public class MockDatanodePipeline {
+
+ private final Pipeline pipeline;
+ private final XceiverClientRatis xceiverClient;
+ private final XceiverClientFactory clientFactory;
+ private final BlockID blockID;
+
+ // Recorded state
+ private final List receivedChunks = Collections.synchronizedList(new ArrayList<>());
+ private final List receivedPutBlocks = Collections.synchronizedList(new ArrayList<>());
+ private final AtomicInteger watchForCommitCount = new AtomicInteger(0);
+ private volatile Type streamInitType;
+
+ // Commit tracking
+ private final AtomicLong nextLogIndex = new AtomicLong(1);
+
+ // Failure injection
+ private volatile Supplier chunkFailure = null;
+ private volatile int chunkFailAfter = Integer.MAX_VALUE;
+ private final AtomicInteger chunkCount = new AtomicInteger(0);
+
+ private volatile Supplier putBlockFailure = null;
+ private volatile int putBlockFailAfter = Integer.MAX_VALUE;
+ private final AtomicInteger putBlockCount = new AtomicInteger(0);
+
+ private volatile Supplier watchFailure = null;
+ private volatile int watchFailAfter = Integer.MAX_VALUE;
+
+ public MockDatanodePipeline() throws IOException {
+ this(new BlockID(1, 1));
+ }
+
+ public MockDatanodePipeline(BlockID blockID) throws IOException {
+ this.blockID = blockID;
+ this.pipeline = MockPipeline.createRatisPipeline();
+
+ // Ensure metrics are initialized
+ XceiverClientManager.getXceiverClientMetrics();
+
+ // Create concrete DataStreamOutput
+ DataStreamOutput mockDataStreamOutput = spy(DataStreamOutput.class);
+ doThrow(new UnsupportedOperationException()).
+ when(mockDataStreamOutput).writeAsync(any(FilePositionCount.class), any(WriteOption[].class));
+ doThrow(new UnsupportedOperationException()).when(mockDataStreamOutput).getRaftClientReplyFuture();
+ doThrow(new UnsupportedOperationException()).when(mockDataStreamOutput).getWritableByteChannel();
+ doReturn(CompletableFuture.completedFuture(dataStreamReply(0))).when(mockDataStreamOutput).closeAsync();
+
+ // Mock XceiverClientRatis
+ this.xceiverClient = mock(XceiverClientRatis.class);
+ when(xceiverClient.getPipeline()).thenReturn(pipeline);
+ doReturn(0L).when(xceiverClient).getReplicatedMinCommitIndex();
+
+ // Mock DataStreamApi to return our concrete DataStreamOutput
+ // Both overloads must be stubbed: stream(ByteBuffer) and stream(ByteBuffer, RoutingTable) — the pipeline-mode
+ // default is true, so the 2-arg overload is what BlockDataStreamOutput.setupStream calls.
+ DataStreamApi dataStreamApi = mock(DataStreamApi.class);
+ doAnswer(invocation -> {
+ captureStreamInitType(invocation.getArgument(0));
+ return mockDataStreamOutput;
+ }).when(dataStreamApi).stream(any(ByteBuffer.class));
+ doAnswer(invocation -> {
+ captureStreamInitType(invocation.getArgument(0));
+ return mockDataStreamOutput;
+ }).when(dataStreamApi).stream(any(ByteBuffer.class), any(RoutingTable.class));
+ doReturn(dataStreamApi).when(xceiverClient).getDataStreamApi();
+
+ // Setup sendCommandAsync (putBlock) behavior
+ doAnswer(invocation -> {
+ ContainerCommandRequestProto request = invocation.getArgument(0);
+ if (request.getCmdType() == Type.PutBlock) {
+ receivedPutBlocks.add(request);
+ int count = putBlockCount.incrementAndGet();
+ CompletableFuture f = new CompletableFuture<>();
+ if (count > putBlockFailAfter && putBlockFailure != null) {
+ f.completeExceptionally(putBlockFailure.get());
+ } else {
+ ContainerCommandResponseProto response = buildPutBlockResponse(blockID);
+ f.complete(response);
+ }
+ XceiverClientReply reply = new XceiverClientReply(f);
+ reply.setLogIndex(nextLogIndex.getAndIncrement());
+ return reply;
+ }
+ // Default: return success
+ ContainerCommandResponseProto response =
+ ContainerCommandResponseProto.newBuilder()
+ .setCmdType(request.getCmdType())
+ .setResult(Result.SUCCESS)
+ .build();
+ XceiverClientReply reply = new XceiverClientReply(CompletableFuture.completedFuture(response));
+ reply.setLogIndex(0);
+ return reply;
+ }).when(xceiverClient).sendCommandAsync(any());
+
+ // Setup watchForCommit behavior
+ doAnswer(invocation -> {
+ long index = invocation.getArgument(0);
+ int count = watchForCommitCount.incrementAndGet();
+ CompletableFuture f = new CompletableFuture<>();
+ if (count > watchFailAfter && watchFailure != null) {
+ f.completeExceptionally(watchFailure.get());
+ } else {
+ XceiverClientReply watchReply = new XceiverClientReply(null);
+ watchReply.setLogIndex(index);
+ f.complete(watchReply);
+ }
+ return f;
+ }).when(xceiverClient).watchForCommit(anyLong());
+
+ // Setup updateCommitInfosMap — no-op
+ doAnswer(invocation -> {
+ ByteBuffer src = invocation.getArgument(0);
+ Iterable options = invocation.getArgument(1);
+ int size = src.remaining();
+ for (WriteOption option : options) {
+ if (option == StandardWriteOption.CLOSE) {
+ if (!receivedChunks.isEmpty()) {
+ receivedChunks.remove(receivedChunks.size() - 1);
+ }
+ src.position(src.limit());
+ return CompletableFuture.completedFuture(dataStreamReply(size));
+ }
+ }
+ int count = chunkCount.incrementAndGet();
+ if (count > chunkFailAfter && chunkFailure != null) {
+ CompletableFuture failed = new CompletableFuture<>();
+ failed.completeExceptionally(chunkFailure.get());
+ return failed;
+ }
+ byte[] data = new byte[size];
+ src.get(data);
+ receivedChunks.add(data);
+ return CompletableFuture.completedFuture(dataStreamReply(data.length));
+ }).when(mockDataStreamOutput).writeAsync(any(ByteBuffer.class), any(Iterable.class));
+
+ // Mock XceiverClientFactory
+ this.clientFactory = mock(XceiverClientFactory.class);
+ doReturn(xceiverClient).when(clientFactory).acquireClient(any(Pipeline.class), anyBoolean());
+ doReturn(xceiverClient).when(clientFactory).acquireClient(any(Pipeline.class));
+ }
+
+ // --- Accessors ---
+
+ public Pipeline getPipeline() {
+ return pipeline;
+ }
+
+ public XceiverClientRatis getXceiverClient() {
+ return xceiverClient;
+ }
+
+ public XceiverClientFactory getClientFactory() {
+ return clientFactory;
+ }
+
+ public BlockID getBlockID() {
+ return blockID;
+ }
+
+ public List getReceivedChunks() {
+ return receivedChunks;
+ }
+
+ public List getReceivedPutBlocks() {
+ return receivedPutBlocks;
+ }
+
+ public int getWatchForCommitCount() {
+ return watchForCommitCount.get();
+ }
+
+ public Type getStreamInitType() {
+ return streamInitType;
+ }
+
+ /** Concatenate all received chunks into a single byte array. */
+ public byte[] getAllReceivedData() {
+ int total = receivedChunks.stream().mapToInt(c -> c.length).sum();
+ byte[] result = new byte[total];
+ int pos = 0;
+ for (byte[] chunk : receivedChunks) {
+ System.arraycopy(chunk, 0, result, pos, chunk.length);
+ pos += chunk.length;
+ }
+ return result;
+ }
+
+ // --- Failure injection ---
+
+ public MockDatanodePipeline failChunkAfter(int n, Supplier err) {
+ this.chunkFailAfter = n;
+ this.chunkFailure = err;
+ return this;
+ }
+
+ public MockDatanodePipeline failPutBlockAfter(int n, Supplier err) {
+ this.putBlockFailAfter = n;
+ this.putBlockFailure = err;
+ return this;
+ }
+
+ public MockDatanodePipeline failWatchAfter(int n, Supplier err) {
+ this.watchFailAfter = n;
+ this.watchFailure = err;
+ return this;
+ }
+
+ // --- Helpers ---
+
+ private void captureStreamInitType(ByteBuffer buffer) {
+ ByteBuffer dup = buffer.duplicate();
+ byte[] bytes = new byte[dup.remaining()];
+ dup.get(bytes);
+ try {
+ streamInitType = ContainerCommandRequestMessage.toProto(
+ ByteString.copyFrom(bytes), null).getCmdType();
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to decode stream init request", e);
+ }
+ }
+
+ private static ContainerCommandResponseProto buildPutBlockResponse(BlockID blockID) {
+ return ContainerCommandResponseProto.newBuilder()
+ .setCmdType(Type.PutBlock)
+ .setResult(Result.SUCCESS)
+ .setPutBlock(PutBlockResponseProto.newBuilder()
+ .setCommittedBlockLength(
+ GetCommittedBlockLengthResponseProto.newBuilder()
+ .setBlockID(blockID.getDatanodeBlockIDProtobuf())
+ .setBlockLength(0)
+ .build())
+ .build())
+ .build();
+ }
+
+ private static DataStreamReply dataStreamReply(long bytesWritten) {
+ DataStreamReply reply = mock(DataStreamReply.class);
+ when(reply.isSuccess()).thenReturn(true);
+ when(reply.getBytesWritten()).thenReturn(bytesWritten);
+ when(reply.getDataLength()).thenReturn(bytesWritten);
+ when(reply.getCommitInfos()).thenReturn(Collections.emptyList());
+ return reply;
+ }
+}
diff --git a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockDataStreamOutput.java b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockDataStreamOutput.java
new file mode 100644
index 000000000000..8f51e3c9a417
--- /dev/null
+++ b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockDataStreamOutput.java
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hadoop.hdds.scm.storage;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletionException;
+import org.apache.commons.lang3.RandomUtils;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.Type;
+import org.apache.hadoop.hdds.scm.OzoneClientConfig;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Unit tests for {@link BlockDataStreamOutput} exercised through the {@link ByteBufferStreamOutput} interface with a
+ * mocked datanode pipeline.
+ */
+class TestBlockDataStreamOutput {
+
+ // Config: CHUNK=100, flush boundary=4 chunks (400B), window=5 chunks (500B)
+ private static final int CHUNK_SIZE = 100;
+ private static final long DS_FLUSH_SIZE = 400;
+ private static final long STREAM_WINDOW = 500;
+
+ private static OzoneClientConfig createConfig() {
+ OzoneClientConfig config = new OzoneClientConfig();
+ config.setDataStreamMinPacketSize(CHUNK_SIZE);
+ config.setDataStreamBufferFlushSize(DS_FLUSH_SIZE);
+ config.setStreamWindowSize(STREAM_WINDOW);
+ config.setStreamBufferSize(CHUNK_SIZE);
+ config.setStreamBufferFlushSize(DS_FLUSH_SIZE);
+ config.setStreamBufferMaxSize(2 * DS_FLUSH_SIZE);
+ config.setStreamBufferFlushDelay(false);
+ config.setChecksumType(ContainerProtos.ChecksumType.NONE);
+ config.setBytesPerChecksum(CHUNK_SIZE);
+ return config;
+ }
+
+ private BlockDataStreamOutput createStream(MockDatanodePipeline pipeline) throws IOException {
+ return createStream(pipeline, createConfig());
+ }
+
+ private BlockDataStreamOutput createStream(
+ MockDatanodePipeline pipeline, OzoneClientConfig config) throws IOException {
+ List bufferList = new ArrayList<>();
+ return new BlockDataStreamOutput(
+ pipeline.getBlockID(),
+ pipeline.getClientFactory(),
+ pipeline.getPipeline(),
+ config,
+ null, // no token
+ bufferList);
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void streamInitTypeFollowsClientConfig(boolean putBlockOnCloseEnabled) throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ OzoneClientConfig config = createConfig();
+ config.setDatastreamPutBlockOnCloseEnabled(putBlockOnCloseEnabled);
+ try (BlockDataStreamOutput stream = createStream(pipeline, config)) {
+ Type expected = putBlockOnCloseEnabled ? Type.StreamInitWithPutBlock : Type.StreamInit;
+ assertEquals(expected, pipeline.getStreamInitType());
+ }
+ }
+
+ @Test
+ void writeSubChunkThenClose() throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ try (BlockDataStreamOutput stream = createStream(pipeline)) {
+ byte[] data = randomBytes(50);
+ stream.write(ByteBuffer.wrap(data), 0, data.length);
+ // No chunk shipped yet — data is in currentBuffer
+ assertEquals(0, pipeline.getReceivedChunks().size(),
+ "No chunk should be shipped before close for sub-chunk write");
+ }
+ // After close: 1 chunk flushed + 1 putBlock
+ assertEquals(1, pipeline.getReceivedChunks().size());
+ assertEquals(1, pipeline.getReceivedPutBlocks().size());
+ }
+
+ @Test
+ void writeExactChunkThenClose() throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ byte[] data = randomBytes(CHUNK_SIZE);
+ try (BlockDataStreamOutput stream = createStream(pipeline)) {
+ stream.write(ByteBuffer.wrap(data), 0, data.length);
+ // Exact chunk → shipped immediately (currentBuffer full)
+ assertEquals(1, pipeline.getReceivedChunks().size());
+ }
+ assertEquals(1, pipeline.getReceivedPutBlocks().size());
+ assertArrayEquals(data, pipeline.getAllReceivedData());
+ }
+
+ @Test
+ void writeFlushBoundaryTriggersPutBlock() throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ byte[] data = randomBytes(400);
+ try (BlockDataStreamOutput stream = createStream(pipeline)) {
+ stream.write(ByteBuffer.wrap(data), 0, data.length);
+ // 4 chunks of 100B each → hits flush boundary → 1 putBlock
+ assertEquals(4, pipeline.getReceivedChunks().size());
+ assertEquals(1, pipeline.getReceivedPutBlocks().size(), "PutBlock should trigger at flush boundary (400B)");
+ }
+ // Close adds another putBlock
+ assertEquals(2, pipeline.getReceivedPutBlocks().size());
+ }
+
+ @Test
+ void writeAcrossStreamWindowTriggersBackPressure() throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ // Window = 500B = 5 chunks. Writing 500B should trigger back-pressure.
+ byte[] data = randomBytes(500);
+ try (BlockDataStreamOutput stream = createStream(pipeline)) {
+ stream.write(ByteBuffer.wrap(data), 0, data.length);
+ // 5 chunks written, putBlock at 400B boundary, back-pressure at 500B
+ // should have triggered watchForCommit
+ assertEquals(1, pipeline.getWatchForCommitCount(), "watchForCommit should be called for back-pressure");
+ }
+ assertArrayEquals(data, pipeline.getAllReceivedData());
+ }
+
+ @Test
+ void hsyncFlushesAndWaitsForCommit() throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ try (BlockDataStreamOutput stream = createStream(pipeline)) {
+ byte[] data = randomBytes(200);
+ stream.write(ByteBuffer.wrap(data), 0, data.length);
+ stream.hsync();
+ // 2 chunks, 1 putBlock (from hsync), watch called
+ assertEquals(2, pipeline.getReceivedChunks().size());
+ assertEquals(1, pipeline.getReceivedPutBlocks().size());
+ assertEquals(1, pipeline.getWatchForCommitCount());
+ }
+ }
+
+ //@Test - skipped as it fails now.
+ void hsyncPropagatesIOException() throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ // Fail the first putBlock
+ pipeline.failPutBlockAfter(0, () -> new IOException("simulated putBlock fail"));
+
+ BlockDataStreamOutput stream = createStream(pipeline);
+ byte[] data = randomBytes(200);
+ stream.write(ByteBuffer.wrap(data), 0, data.length);
+
+ // hsync should propagate the IOException from the failed putBlock
+ assertThrows(IOException.class, stream::hsync, "hsync() must propagate IOException from failed putBlock");
+ stream.close();
+ }
+
+ //@Test - skipped as it fails now
+ void hsyncPropagatesWatchFailure() throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ // Fail the first watchForCommit
+ pipeline.failWatchAfter(0,
+ () -> new IOException("simulated watch timeout"));
+
+ BlockDataStreamOutput stream = createStream(pipeline);
+ byte[] data = randomBytes(200);
+ stream.write(ByteBuffer.wrap(data), 0, data.length);
+
+ // hsync should propagate the watch failure
+ assertThrows(IOException.class, stream::hsync, "hsync() must propagate IOException from failed watchForCommit");
+ stream.close();
+ }
+
+ @Test
+ void closeAfterWriteFailureThrows() throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ // Fail on the 2nd chunk write
+ pipeline.failChunkAfter(1, () -> new IOException("chunk write failed due to injected failure"));
+
+ BlockDataStreamOutput stream = createStream(pipeline);
+ byte[] data = randomBytes(50);
+ stream.write(ByteBuffer.wrap(data), 0, data.length); // ok, stays in buffer
+
+ byte[] data2 = randomBytes(CHUNK_SIZE + 50);
+ // This write will fill the buffer and trigger a chunk write that may fail.
+ // Close will surface the exception, which will be caused by the injected exception,
+ // however based on thread scheduling, the actual exception we get back from the stream
+ // is either a CompletionException caused by the injected exception or the
+ // injected exception itself so check for both.
+ stream.write(ByteBuffer.wrap(data2), 0, data2.length);
+ Throwable e = assertThrows(IOException.class, () -> stream.close());
+ if (e instanceof CompletionException) {
+ assertThat(e.getMessage()).contains("Failed to write chunk ");
+ boolean foundExpectedCause = false;
+ while (e.getCause() != null) {
+ e = e.getCause();
+ if (e instanceof IOException && e.getMessage().contains("chunk write failed due to injected failure")) {
+ foundExpectedCause = true;
+ }
+ }
+ assertTrue(foundExpectedCause);
+ } else {
+ assertThat(e.getMessage().contains("chunk write failed due to injected failure"));
+ }
+ }
+
+ @Test
+ void writeAfterCloseThrows() throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ BlockDataStreamOutput stream = createStream(pipeline);
+ byte[] data = randomBytes(CHUNK_SIZE);
+ stream.write(ByteBuffer.wrap(data), 0, data.length);
+ stream.close();
+
+ IOException e = assertThrows(IOException.class,
+ () -> stream.write(ByteBuffer.wrap(data), 0, data.length),
+ "write() after close() should throw IOException");
+ assertThat(e.getMessage()).contains("has been closed");
+ }
+
+ @Test
+ void ackDataLengthTracksCommittedData() throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ BlockDataStreamOutput stream = createStream(pipeline);
+ byte[] data = randomBytes(400);
+ stream.write(ByteBuffer.wrap(data), 0, data.length);
+ stream.close();
+
+ assertEquals(400, stream.getTotalAckDataLength(), "After close, all written data should be acknowledged");
+ }
+
+ @Test
+ void chunkDataIntegrity() throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ byte[] data = randomBytes(350);
+ try (BlockDataStreamOutput stream = createStream(pipeline)) {
+ stream.write(ByteBuffer.wrap(data), 0, data.length);
+ }
+ // 3 full chunks of 100B + 1 partial chunk of 50B
+ assertEquals(4, pipeline.getReceivedChunks().size());
+ assertArrayEquals(data, pipeline.getAllReceivedData(), "Concatenated chunk data must match original input");
+ }
+
+ @Test
+ void putBlockContainsAllChunkMetadata() throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ byte[] data = randomBytes(300);
+ try (BlockDataStreamOutput stream = createStream(pipeline)) {
+ stream.write(ByteBuffer.wrap(data), 0, data.length);
+ }
+
+ // One putBlock should have been sent
+ assertEquals(1, pipeline.getReceivedPutBlocks().size());
+
+ // 3 chunks with 300 bytes were sent and all chunk file name is correct.
+ List chunksList =
+ pipeline.getReceivedPutBlocks().get(0).getPutBlock().getBlockData().getChunksList();
+
+ assertEquals(3, chunksList.size());
+ assertEquals(300, chunksList.stream().mapToLong(ContainerProtos.ChunkInfo::getLen).sum());
+ assertTrue(chunksList.stream().allMatch(c -> c.getChunkName().contains("_chunk_")));
+ }
+
+ private static byte[] randomBytes(int length) {
+ return RandomUtils.secure().randomBytes(length);
+ }
+}
diff --git a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockOutputStreamCorrectness.java b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockOutputStreamCorrectness.java
index 440b5b3d4d52..81deaaf36bb1 100644
--- a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockOutputStreamCorrectness.java
+++ b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockOutputStreamCorrectness.java
@@ -19,6 +19,8 @@
import static java.util.concurrent.Executors.newFixedThreadPool;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -132,6 +134,46 @@ public void testMissingStripeChecksumDoesNotMakeExecutePutBlockFailDuringECRecon
}
}
+ @Test
+ public void testEcReconstructionStreamDisablesContainerAutoCreate() throws IOException {
+ OzoneClientConfig config = new OzoneClientConfig();
+ ECReplicationConfig replicationConfig = new ECReplicationConfig(3, 2);
+ BlockID blockID = new BlockID(1, 1);
+ DatanodeDetails datanodeDetails = MockDatanodeDetails.randomDatanodeDetails();
+ Pipeline pipeline = Pipeline.newBuilder()
+ .setId(datanodeDetails.getID())
+ .setReplicationConfig(replicationConfig)
+ .setNodes(ImmutableList.of(datanodeDetails))
+ .setState(Pipeline.PipelineState.CLOSED)
+ .setReplicaIndexes(ImmutableMap.of(datanodeDetails, 2))
+ .build();
+
+ try (ECBlockOutputStream ecBlockOutputStream = createECBlockOutputStream(config, replicationConfig,
+ blockID, pipeline, false)) {
+ assertFalse(ecBlockOutputStream.isContainerAutoCreate());
+ }
+ }
+
+ @Test
+ public void testEcClientStreamAllowsContainerAutoCreate() throws IOException {
+ OzoneClientConfig config = new OzoneClientConfig();
+ ECReplicationConfig replicationConfig = new ECReplicationConfig(3, 2);
+ BlockID blockID = new BlockID(1, 1);
+ DatanodeDetails datanodeDetails = MockDatanodeDetails.randomDatanodeDetails();
+ Pipeline pipeline = Pipeline.newBuilder()
+ .setId(datanodeDetails.getID())
+ .setReplicationConfig(replicationConfig)
+ .setNodes(ImmutableList.of(datanodeDetails))
+ .setState(Pipeline.PipelineState.CLOSED)
+ .setReplicaIndexes(ImmutableMap.of(datanodeDetails, 2))
+ .build();
+
+ try (ECBlockOutputStream ecBlockOutputStream = createECBlockOutputStream(config, replicationConfig,
+ blockID, pipeline)) {
+ assertTrue(ecBlockOutputStream.isContainerAutoCreate());
+ }
+ }
+
/**
* Creates a BlockData array with {@link ECReplicationConfig#getRequiredNodes()} number of elements.
*/
@@ -183,7 +225,8 @@ private BlockOutputStream createBlockOutputStream(BufferPool bufferPool)
}
private ECBlockOutputStream createECBlockOutputStream(OzoneClientConfig clientConfig,
- ECReplicationConfig repConfig, BlockID blockID, Pipeline pipeline) throws IOException {
+ ECReplicationConfig repConfig, BlockID blockID, Pipeline pipeline,
+ boolean containerAutoCreate) throws IOException {
final XceiverClientManager xcm = mock(XceiverClientManager.class);
when(xcm.acquireClient(any()))
.thenReturn(new MockXceiverClientSpi(pipeline));
@@ -193,7 +236,12 @@ private ECBlockOutputStream createECBlockOutputStream(OzoneClientConfig clientCo
StreamBufferArgs.getDefaultStreamBufferArgs(repConfig, clientConfig);
return new ECBlockOutputStream(blockID, xcm, pipeline, BufferPool.empty(), clientConfig, null,
- clientMetrics, streamBufferArgs, () -> newFixedThreadPool(2));
+ clientMetrics, streamBufferArgs, () -> newFixedThreadPool(2), containerAutoCreate);
+ }
+
+ private ECBlockOutputStream createECBlockOutputStream(OzoneClientConfig clientConfig,
+ ECReplicationConfig repConfig, BlockID blockID, Pipeline pipeline) throws IOException {
+ return createECBlockOutputStream(clientConfig, repConfig, blockID, pipeline, true);
}
/**
diff --git a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestStreamBlockInputStream.java b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestStreamBlockInputStream.java
index 9c77ae19f207..cded373e7c81 100644
--- a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestStreamBlockInputStream.java
+++ b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestStreamBlockInputStream.java
@@ -17,7 +17,11 @@
package org.apache.hadoop.hdds.scm.storage;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
@@ -29,13 +33,17 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.io.IOException;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Collections;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import org.apache.hadoop.hdds.client.BlockID;
import org.apache.hadoop.hdds.protocol.DatanodeDetails;
import org.apache.hadoop.hdds.protocol.DatanodeID;
+import org.apache.hadoop.hdds.protocol.MockDatanodeDetails;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChecksumData;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ContainerCommandRequestProto;
@@ -50,6 +58,7 @@
import org.apache.hadoop.hdds.scm.XceiverClientGrpc;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
import org.apache.hadoop.hdds.security.token.OzoneBlockTokenIdentifier;
+import org.apache.hadoop.ozone.common.OzoneChecksumException;
import org.apache.hadoop.security.token.Token;
import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
import org.apache.ratis.thirdparty.io.grpc.stub.ClientCallStreamObserver;
@@ -188,6 +197,74 @@ public void testCloseDoesNotFailWhenOnCompletedAndCancelThrow() throws Exception
verify(xceiverClient, times(1)).completeStreamRead();
}
+ /**
+ * Reproduces Bug 2: poll() checks future.isDone() before draining the queue.
+ *
+ * When the server delivers a response (onNext) and immediately closes the stream
+ * (onCompleted) — which can happen on the same gRPC thread in rapid succession —
+ * the item is in the queue and the future is already complete by the time poll()
+ * first runs. poll() sees isDone()==true and returns null without ever checking
+ * the queue, so readFromQueue() throws NullPointerException on the null proto.
+ *
+ * This test will FAIL with NullPointerException on the current code and should
+ * PASS once the bug is fixed (poll must drain the queue before checking isDone).
+ */
+ @Test
+ public void testPollDoesNotDropQueuedItemWhenFutureCompletesFirst() throws Exception {
+ OzoneClientConfig clientConfig = newStreamReadConfig();
+ BlockID blockID = new BlockID(1L, 10L);
+ byte[] data = {1, 2, 3, 4};
+ Pipeline pipeline = mockStandalonePipeline();
+ ClientCallStreamObserver requestObserver =
+ mock(ClientCallStreamObserver.class);
+ StreamingReadResponse streamingReadResponse = mock(StreamingReadResponse.class);
+ when(streamingReadResponse.getRequestObserver()).thenReturn(requestObserver);
+
+ // Capture the StreamingReaderSpi during initStreamRead so we can drive
+ // its callbacks from the streamRead mock below.
+ AtomicReference readerRef = new AtomicReference<>();
+ XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = inv.getArgument(1);
+ reader.setStreamingReadResponse(streamingReadResponse);
+ readerRef.set(reader);
+ return null;
+ }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+
+ // Simulate the race: when the client sends a ReadBlock request, the server
+ // responds with data (onNext) and closes the stream (onCompleted) before
+ // poll() has had a chance to run — both callbacks fire on the same call stack
+ // before streamRead() returns. This means when poll() is entered, the queue
+ // already has the response item AND future.isDone() is already true.
+ // poll() checks isDone() first and returns null, dropping the queued item.
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = readerRef.get();
+ reader.onNext(ContainerCommandResponseProto.newBuilder()
+ .setCmdType(Type.ReadBlock)
+ .setResult(ContainerProtos.Result.SUCCESS)
+ .setReadBlock(buildReadBlockResponse(data))
+ .build());
+ reader.onCompleted(); // future is now done; item is already in the queue
+ return null;
+ }).when(xceiverClient).streamRead(any(), any());
+
+ XceiverClientFactory xceiverClientFactory = mock(XceiverClientFactory.class);
+ when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class)))
+ .thenReturn(xceiverClient);
+
+ try (StreamBlockInputStream sbis = new StreamBlockInputStream(
+ blockID, data.length, pipeline, null, xceiverClientFactory,
+ NO_REFRESH, clientConfig)) {
+
+ ByteBuffer buf = ByteBuffer.allocate(data.length);
+ // With the bug: poll() returns null (future done, queue unchecked) and
+ // readFromQueue() throws NullPointerException.
+ // After the fix: all 4 bytes are returned successfully.
+ assertDoesNotThrow(() -> sbis.read(buf), "should not NPE when onCompleted fires before poll");
+ assertEquals(data.length, buf.position(), "all bytes should be read");
+ }
+ }
+
private OzoneClientConfig newStreamReadConfig() {
OzoneClientConfig clientConfig = new OzoneClientConfig();
clientConfig.setChecksumVerify(false);
@@ -232,11 +309,441 @@ private XceiverClientGrpc mockStreamingReadClient(byte[] data,
.setReadBlock(readBlock)
.build());
return null;
- }).when(xceiverClient).initStreamRead(any(BlockID.class), any());
+ }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
return xceiverClient;
}
+ /**
+ * Realistic test for the checksum-alignment skip path.
+ *
+ * After a seek, the server aligns its response to the nearest checksum boundary,
+ * which may be before the client's current position. With a small responseDataSize
+ * (4 bytes), the server sends two 4-byte chunks:
+ * chunk 1: blockOffset=0, data=[0,1,2,3] — entirely before seek position 4
+ * chunk 2: blockOffset=4, data=[4,5,6,7] — starts at seek position
+ *
+ * The while(true) loop in read() must:
+ * iteration 1: receive chunk 1, skip all 4 bytes (pos-blockOffset=4 == data.size()),
+ * empty buffer → continue
+ * iteration 2: receive chunk 2, no skip needed → return buffer with [4,5,6,7]
+ *
+ * This was an infinite loop or MPE before fixes in the PR that added this test.
+ */
+ @Test
+ public void testSeekReadsCorrectBytesWhenFirstResponseIsFullyBeforePosition() throws Exception {
+ OzoneClientConfig clientConfig = newStreamReadConfig();
+ clientConfig.setStreamReadResponseDataSize(4); // 4-byte chunks match the test data
+ BlockID blockID = new BlockID(1L, 12L);
+ long length = 8;
+ Pipeline pipeline = mockStandalonePipeline();
+ ClientCallStreamObserver requestObserver =
+ mock(ClientCallStreamObserver.class);
+ StreamingReadResponse streamingReadResponse = mock(StreamingReadResponse.class);
+ when(streamingReadResponse.getRequestObserver()).thenReturn(requestObserver);
+
+ AtomicReference readerRef = new AtomicReference<>();
+ XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = inv.getArgument(1);
+ reader.setStreamingReadResponse(streamingReadResponse);
+ readerRef.set(reader);
+ return null;
+ }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+
+ // Server aligns to checksum boundary 0 and sends two 4-byte responses.
+ // The first chunk (bytes 0–3) is entirely before seek position 4 and will be
+ // fully skipped. The second chunk (bytes 4–7) starts at our position.
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = readerRef.get();
+ reader.onNext(buildResponseProto(new byte[]{0, 1, 2, 3}, 0)); // fully skipped
+ reader.onNext(buildResponseProto(new byte[]{4, 5, 6, 7}, 4)); // has our data
+ reader.onCompleted();
+ return null;
+ }).when(xceiverClient).streamRead(any(), any());
+
+ XceiverClientFactory xceiverClientFactory = mock(XceiverClientFactory.class);
+ when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class)))
+ .thenReturn(xceiverClient);
+
+ try (StreamBlockInputStream sbis = new StreamBlockInputStream(
+ blockID, length, pipeline, null, xceiverClientFactory,
+ NO_REFRESH, clientConfig)) {
+
+ sbis.seek(4);
+
+ byte[] out = new byte[4];
+ int bytesRead = sbis.read(out, 0, 4);
+ assertEquals(4, bytesRead);
+ assertArrayEquals(new byte[]{4, 5, 6, 7}, out,
+ "should return bytes starting from seek position, skipping the checksum-aligned preamble");
+ }
+ }
+
+ /**
+ * Defensive test for readFromQueue() which NPE'ed when poll() returns null.
+ *
+ * This tests a server-error / edge-case scenario: the server sends only a
+ * single response whose data ends before the client's seek position, then
+ * immediately completes the stream. The while(true) loop in read() skips
+ * all bytes in the response (empty buffer), then calls poll() again. poll()
+ * finds the queue empty and isDone()==true and returns null.
+ *
+ * A well-behaved server would never complete the stream without covering the
+ * client's position, so this scenario represents a protocol violation rather
+ * than normal operation, but it serves to reproduce the NPE exception before
+ * fixing the code.
+ */
+ @Test
+ public void testReadFromQueueNpeWhenStreamCompletesWithoutCoveringSeekPosition() throws Exception {
+ OzoneClientConfig clientConfig = newStreamReadConfig();
+ // Short timeout so the test completes quickly rather than waiting 5 s.
+ clientConfig.setStreamReadTimeout(Duration.ofMillis(200));
+
+ BlockID blockID = new BlockID(1L, 13L);
+ long length = 8;
+ Pipeline pipeline = mockStandalonePipeline();
+ ClientCallStreamObserver requestObserver =
+ mock(ClientCallStreamObserver.class);
+ StreamingReadResponse streamingReadResponse = mock(StreamingReadResponse.class);
+ when(streamingReadResponse.getRequestObserver()).thenReturn(requestObserver);
+
+ AtomicReference readerRef = new AtomicReference<>();
+ XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = inv.getArgument(1);
+ reader.setStreamingReadResponse(streamingReadResponse);
+ readerRef.set(reader);
+ return null;
+ }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+
+ // Server only sends bytes 0–3 (before seek position 4) then completes —
+ // simulating a protocol violation or a truncated/corrupt response.
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = readerRef.get();
+ reader.onNext(buildResponseProto(new byte[]{0, 1, 2, 3}, 0));
+ reader.onCompleted();
+ return null;
+ }).when(xceiverClient).streamRead(any(), any());
+
+ XceiverClientFactory xceiverClientFactory = mock(XceiverClientFactory.class);
+ when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class)))
+ .thenReturn(xceiverClient);
+
+ try (StreamBlockInputStream sbis = new StreamBlockInputStream(
+ blockID, length, pipeline, null, xceiverClientFactory,
+ NO_REFRESH, clientConfig)) {
+
+ sbis.seek(4);
+
+ ByteBuffer buf = ByteBuffer.allocate(4);
+ // Before the fixes: threw NullPointerException (Bug 1) or looped forever (Bug 3).
+ // After the fixes: returns gracefully with 0 / EOF rather than crashing.
+ int bytesRead = sbis.read(buf);
+ assertEquals(-1, bytesRead, "should reach EOF when the stream completes before the seek position");
+ assertEquals(0, buf.position(), "no bytes should be produced");
+ }
+ }
+
+ /**
+ * When the server delivers multiple responses plus onCompleted() inside a
+ * single streamRead() call (all on the same call stack), the first response
+ * is consumed correctly, but by the time read() is invoked again for the
+ * second chunk, future.isDone() is already true. read() sees isDone() and
+ * returns null immediately without checking the queue, so the second (and
+ * any further) queued responses are silently dropped.
+ */
+ @Test
+ public void testReadDoesNotDropQueuedItemsWhenFutureIsDoneOnSecondCall() throws Exception {
+ OzoneClientConfig clientConfig = newStreamReadConfig();
+ BlockID blockID = new BlockID(1L, 11L);
+ byte[] firstChunk = {1, 2, 3, 4};
+ byte[] secondChunk = {5, 6, 7, 8};
+ long length = firstChunk.length + secondChunk.length; // 8 bytes total
+
+ Pipeline pipeline = mockStandalonePipeline();
+ ClientCallStreamObserver requestObserver =
+ mock(ClientCallStreamObserver.class);
+ StreamingReadResponse streamingReadResponse = mock(StreamingReadResponse.class);
+ when(streamingReadResponse.getRequestObserver()).thenReturn(requestObserver);
+
+ AtomicReference readerRef = new AtomicReference<>();
+ XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = inv.getArgument(1);
+ reader.setStreamingReadResponse(streamingReadResponse);
+ readerRef.set(reader);
+ return null;
+ }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+
+ // Server delivers both 4-byte chunks plus onCompleted() in one synchronous
+ // call. After streamRead() returns: queue=[chunk1, chunk2], isDone=true.
+ // read() correctly returns chunk1 on the first call, but on the second call
+ // it sees isDone()==true and returns null before draining chunk2.
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = readerRef.get();
+ reader.onNext(buildResponseProto(firstChunk, 0));
+ reader.onNext(buildResponseProto(secondChunk, firstChunk.length));
+ reader.onCompleted(); // future done; both items still in queue
+ return null;
+ }).when(xceiverClient).streamRead(any(), any());
+
+ XceiverClientFactory xceiverClientFactory = mock(XceiverClientFactory.class);
+ when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class)))
+ .thenReturn(xceiverClient);
+
+ try (StreamBlockInputStream sbis = new StreamBlockInputStream(
+ blockID, length, pipeline, null, xceiverClientFactory,
+ NO_REFRESH, clientConfig)) {
+ ByteBuffer buf = ByteBuffer.allocate((int) length);
+ // With the bug: read() returns null on the second call (isDone is true),
+ // so only 4 bytes are read and buf.position() == 4.
+ // After the fix: all 8 bytes are read and buf.position() == 8.
+ int bytesRead = sbis.read(buf);
+ assertEquals(length, bytesRead, "expected all bytes to be read");
+ assertEquals(length, buf.position(), "buffer position should be at end of block");
+ }
+ }
+
+ @Test
+ public void testReadGetsFreshResponseTimeoutAfterStreamReadWait() throws Exception {
+ OzoneClientConfig clientConfig = newStreamReadConfig();
+ clientConfig.setStreamReadTimeout(Duration.ofMillis(500));
+ BlockID blockID = new BlockID(1L, 12L);
+ Pipeline pipeline = mockStandalonePipeline();
+ ClientCallStreamObserver requestObserver =
+ mock(ClientCallStreamObserver.class);
+ StreamingReadResponse streamingReadResponse = new StreamingReadResponse(
+ MockDatanodeDetails.randomDatanodeDetails(), requestObserver);
+
+ XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+ AtomicReference readerRef = new AtomicReference<>();
+ AtomicReference responseThreadRef = new AtomicReference<>();
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = inv.getArgument(1);
+ reader.setStreamingReadResponse(streamingReadResponse);
+ readerRef.set(reader);
+ return null;
+ }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+ doAnswer(inv -> {
+ Thread.sleep(450);
+ Thread responseThread = new Thread(() -> {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ }
+ readerRef.get().onNext(buildResponseProto(new byte[] {1}, 0));
+ });
+ responseThreadRef.set(responseThread);
+ responseThread.start();
+ return null;
+ }).when(xceiverClient).streamRead(any(), any());
+
+ XceiverClientFactory xceiverClientFactory = mock(XceiverClientFactory.class);
+ when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class)))
+ .thenReturn(xceiverClient);
+
+ try (StreamBlockInputStream sbis = new StreamBlockInputStream(
+ blockID, 1L, pipeline, null, xceiverClientFactory,
+ NO_REFRESH, clientConfig)) {
+ ByteBuffer buf = ByteBuffer.allocate(1);
+ assertEquals(1, sbis.read(buf));
+ responseThreadRef.get().join();
+ }
+ }
+
+ @Test
+ public void testReadWithoutNewRequestGetsFreshTimeoutBudget() throws Exception {
+ OzoneClientConfig clientConfig = newStreamReadConfig();
+ clientConfig.setStreamReadPreReadSize(10);
+ clientConfig.setStreamReadTimeout(Duration.ofMillis(500));
+ BlockID blockID = new BlockID(1L, 13L);
+ Pipeline pipeline = mockStandalonePipeline();
+ ClientCallStreamObserver requestObserver =
+ mock(ClientCallStreamObserver.class);
+ StreamingReadResponse streamingReadResponse = new StreamingReadResponse(
+ MockDatanodeDetails.randomDatanodeDetails(), requestObserver);
+
+ AtomicReference readerRef = new AtomicReference<>();
+ AtomicInteger streamReads = new AtomicInteger();
+ XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = inv.getArgument(1);
+ reader.setStreamingReadResponse(streamingReadResponse);
+ readerRef.set(reader);
+ return null;
+ }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+ doAnswer(inv -> {
+ streamReads.incrementAndGet();
+ readerRef.get().onNext(buildResponseProto(new byte[] {1}, 0));
+ return null;
+ }).when(xceiverClient).streamRead(any(), any());
+
+ XceiverClientFactory xceiverClientFactory = mock(XceiverClientFactory.class);
+ when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class)))
+ .thenReturn(xceiverClient);
+
+ try (StreamBlockInputStream sbis = new StreamBlockInputStream(
+ blockID, 2L, pipeline, null, xceiverClientFactory,
+ NO_REFRESH, clientConfig)) {
+ ByteBuffer first = ByteBuffer.allocate(1);
+ assertEquals(1, sbis.read(first));
+ Thread.sleep(600);
+
+ Thread delayedResponse = new Thread(() -> {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ }
+ readerRef.get().onNext(buildResponseProto(new byte[] {2}, 1));
+ });
+ delayedResponse.start();
+
+ ByteBuffer second = ByteBuffer.allocate(1);
+ assertEquals(1, sbis.readFully(second, false));
+ delayedResponse.join();
+ assertEquals(1, streamReads.get(), "second read should use data from the existing request");
+ }
+ }
+
+ /**
+ * The server may end the stream (onCompleted) after a request has been sent but before any
+ * data is delivered, e.g. when the datanode shuts down gracefully. poll() then returns null
+ * (stream done, queue drained) while readFromQueue() is waiting for data. The premature end
+ * of the stream must surface as EOF, not as a NullPointerException that bypasses
+ * handleExceptions() retry handling.
+ */
+ @Test
+ public void testStreamCompletedMidReadWithEmptyQueueSurfacesEof() throws Exception {
+ OzoneClientConfig clientConfig = newStreamReadConfig();
+ BlockID blockID = new BlockID(1L, 14L);
+ Pipeline pipeline = mockStandalonePipeline();
+ ClientCallStreamObserver requestObserver =
+ mock(ClientCallStreamObserver.class);
+ StreamingReadResponse streamingReadResponse = mock(StreamingReadResponse.class);
+ when(streamingReadResponse.getRequestObserver()).thenReturn(requestObserver);
+
+ AtomicReference readerRef = new AtomicReference<>();
+ XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = inv.getArgument(1);
+ reader.setStreamingReadResponse(streamingReadResponse);
+ readerRef.set(reader);
+ return null;
+ }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+
+ // The server closes the stream without delivering any of the requested data, so the
+ // reader ends up polling a drained queue with the future already completed.
+ doAnswer(inv -> {
+ readerRef.get().onCompleted();
+ return null;
+ }).when(xceiverClient).streamRead(any(), any());
+
+ XceiverClientFactory xceiverClientFactory = mock(XceiverClientFactory.class);
+ when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class)))
+ .thenReturn(xceiverClient);
+
+ try (StreamBlockInputStream sbis = new StreamBlockInputStream(
+ blockID, 4L, pipeline, null, xceiverClientFactory,
+ NO_REFRESH, clientConfig)) {
+ ByteBuffer buf = ByteBuffer.allocate(4);
+ int bytesRead = assertDoesNotThrow(() -> sbis.read(buf),
+ "premature stream completion must not throw NullPointerException");
+ assertEquals(-1, bytesRead, "premature stream completion should surface as EOF");
+ assertEquals(0, buf.position());
+ }
+ }
+
+ /**
+ * A truncated payload fails checksum verification in onNext(). The failure handling must
+ * record the real failure via setFailed() and propagate it, even though the payload is
+ * shorter than the 10-byte hex preview included in the warning log.
+ */
+ @Test
+ public void testChecksumFailureOnShortPayloadSurfacesRealError() throws Exception {
+ OzoneClientConfig clientConfig = newStreamReadConfig();
+ clientConfig.setChecksumVerify(true);
+ BlockID blockID = new BlockID(1L, 15L);
+ byte[] data = {1, 2, 3, 4};
+ Pipeline pipeline = mockStandalonePipeline();
+ ClientCallStreamObserver requestObserver =
+ mock(ClientCallStreamObserver.class);
+ StreamingReadResponse streamingReadResponse = mock(StreamingReadResponse.class);
+ when(streamingReadResponse.getRequestObserver()).thenReturn(requestObserver);
+
+ AtomicReference readerRef = new AtomicReference<>();
+ XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = inv.getArgument(1);
+ reader.setStreamingReadResponse(streamingReadResponse);
+ readerRef.set(reader);
+ return null;
+ }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+
+ // Deliver a payload shorter than 10 bytes whose checksum does not match the data.
+ doAnswer(inv -> {
+ readerRef.get().onNext(buildCorruptResponseProto(data, 0));
+ return null;
+ }).when(xceiverClient).streamRead(any(), any());
+
+ XceiverClientFactory xceiverClientFactory = mock(XceiverClientFactory.class);
+ when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class)))
+ .thenReturn(xceiverClient);
+
+ try (StreamBlockInputStream sbis = new StreamBlockInputStream(
+ blockID, data.length, pipeline, null, xceiverClientFactory,
+ NO_REFRESH, clientConfig)) {
+ ByteBuffer buf = ByteBuffer.allocate(data.length);
+ IOException thrown = assertThrows(IOException.class, () -> sbis.read(buf),
+ "checksum failure should surface as an IOException");
+ assertThat(thrown).hasRootCauseInstanceOf(OzoneChecksumException.class);
+ }
+ verify(requestObserver, times(1)).onError(any(OzoneChecksumException.class));
+ }
+
+ /**
+ * onNext() may fail before XceiverClientGrpc has registered the StreamingReadResponse, so
+ * getResponse() is still null while the failure is reported. The real failure must still be
+ * recorded and surfaced to the reader.
+ */
+ @Test
+ public void testChecksumFailureBeforeResponseRegistered() throws Exception {
+ OzoneClientConfig clientConfig = newStreamReadConfig();
+ clientConfig.setChecksumVerify(true);
+ BlockID blockID = new BlockID(1L, 16L);
+ byte[] data = {1, 2, 3};
+ Pipeline pipeline = mockStandalonePipeline();
+ ClientCallStreamObserver requestObserver =
+ mock(ClientCallStreamObserver.class);
+ StreamingReadResponse streamingReadResponse = mock(StreamingReadResponse.class);
+ when(streamingReadResponse.getRequestObserver()).thenReturn(requestObserver);
+
+ XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = inv.getArgument(1);
+ // The corrupt response arrives before setStreamingReadResponse() has been called.
+ reader.onNext(buildCorruptResponseProto(data, 0));
+ reader.setStreamingReadResponse(streamingReadResponse);
+ return null;
+ }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+
+ XceiverClientFactory xceiverClientFactory = mock(XceiverClientFactory.class);
+ when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class)))
+ .thenReturn(xceiverClient);
+
+ try (StreamBlockInputStream sbis = new StreamBlockInputStream(
+ blockID, data.length, pipeline, null, xceiverClientFactory,
+ NO_REFRESH, clientConfig)) {
+ ByteBuffer buf = ByteBuffer.allocate(data.length);
+ IOException thrown = assertThrows(IOException.class, () -> sbis.read(buf),
+ "checksum failure should surface as an IOException");
+ assertThat(thrown).hasRootCauseInstanceOf(OzoneChecksumException.class);
+ }
+ verify(requestObserver, never()).onError(any());
+ }
+
private ReadBlockResponseProto buildReadBlockResponse(byte[] data) {
return ReadBlockResponseProto.newBuilder()
.setOffset(0)
@@ -247,4 +754,35 @@ private ReadBlockResponseProto buildReadBlockResponse(byte[] data) {
.build())
.build();
}
+
+ private ContainerCommandResponseProto buildResponseProto(byte[] data, long offset) {
+ return ContainerCommandResponseProto.newBuilder()
+ .setCmdType(Type.ReadBlock)
+ .setResult(ContainerProtos.Result.SUCCESS)
+ .setReadBlock(ReadBlockResponseProto.newBuilder()
+ .setOffset(offset)
+ .setData(ByteString.copyFrom(data))
+ .setChecksumData(ChecksumData.newBuilder()
+ .setType(ContainerProtos.ChecksumType.NONE)
+ .setBytesPerChecksum(data.length)
+ .build())
+ .build())
+ .build();
+ }
+
+ private ContainerCommandResponseProto buildCorruptResponseProto(byte[] data, long offset) {
+ return ContainerCommandResponseProto.newBuilder()
+ .setCmdType(Type.ReadBlock)
+ .setResult(ContainerProtos.Result.SUCCESS)
+ .setReadBlock(ReadBlockResponseProto.newBuilder()
+ .setOffset(offset)
+ .setData(ByteString.copyFrom(data))
+ .setChecksumData(ChecksumData.newBuilder()
+ .setType(ContainerProtos.ChecksumType.CRC32)
+ .setBytesPerChecksum(data.length)
+ .addChecksums(ByteString.copyFrom(new byte[4]))
+ .build())
+ .build())
+ .build();
+ }
}
diff --git a/hadoop-hdds/common/dev-support/findbugsExcludeFile.xml b/hadoop-hdds/common/dev-support/findbugsExcludeFile.xml
index b80471fe376e..bf9af0c6cf76 100644
--- a/hadoop-hdds/common/dev-support/findbugsExcludeFile.xml
+++ b/hadoop-hdds/common/dev-support/findbugsExcludeFile.xml
@@ -28,9 +28,4 @@
-
-
-
-
-
diff --git a/hadoop-hdds/common/pom.xml b/hadoop-hdds/common/pom.xml
index daf7008fa83b..eedb7ac058ff 100644
--- a/hadoop-hdds/common/pom.xml
+++ b/hadoop-hdds/common/pom.xml
@@ -17,11 +17,11 @@
org.apache.ozonehdds-hadoop-dependency-client
- 2.2.0-SNAPSHOT
+ 2.3.0-SNAPSHOT../hadoop-dependency-clienthdds-common
- 2.2.0-SNAPSHOT
+ 2.3.0-SNAPSHOTjarApache Ozone HDDS CommonApache Ozone Distributed Data Store Common
@@ -174,6 +174,12 @@
commons-iotest
+
+ org.apache.hadoop
+ hadoop-common
+ test-jar
+ test
+ org.apache.ozonehdds-config
diff --git a/hadoop-hdds/common/src/main/conf/ozone-env.sh b/hadoop-hdds/common/src/main/conf/ozone-env.sh
index dd98331d78db..ce7d094887c2 100644
--- a/hadoop-hdds/common/src/main/conf/ozone-env.sh
+++ b/hadoop-hdds/common/src/main/conf/ozone-env.sh
@@ -86,11 +86,11 @@ fi
# memory size.
# export OZONE_HEAPSIZE_MIN=
-# Extra Java runtime options for all Ozone commands. We don't support
-# IPv6 yet/still, so by default the preference is set to IPv4.
+# Extra Java runtime options for all Ozone commands. Ozone does not override
+# the JVM's IP stack preference. To force IPv4-only networking:
# export OZONE_OPTS="-Djava.net.preferIPv4Stack=true"
-# For Kerberos debugging, an extended option set logs more information
-# export OZONE_OPTS="-Djava.net.preferIPv4Stack=true -Dsun.security.krb5.debug=true -Dsun.security.spnego.debug"
+# For Kerberos debugging, an extended option set logs more information:
+# export OZONE_OPTS="-Dsun.security.krb5.debug=true -Dsun.security.spnego.debug"
# Some parts of the shell code may do special things dependent upon
# the operating system. We have to set this here. See the next
@@ -154,6 +154,10 @@ export OZONE_OS_TYPE=${OZONE_OS_TYPE:-$(uname -s)}
# helper scripts # such as workers.sh, start-ozone.sh, etc.
# export OZONE_WORKERS="${OZONE_CONF_DIR}/workers"
+# A space-separated list of worker host names, used as an alternative to the
+# OZONE_WORKERS file. Only one of OZONE_WORKERS or OZONE_WORKER_NAMES may be set.
+# export OZONE_WORKER_NAMES=""
+
###
# Options for all daemons
###
@@ -168,6 +172,15 @@ export OZONE_OS_TYPE=${OZONE_OS_TYPE:-$(uname -s)}
# non-secure)
#
+# Extra Java runtime options for all Ozone server daemons (OM, SCM, DataNode,
+# S3 Gateway, Recon, HttpFS). These get appended to OZONE_OPTS for such
+# daemons and are a convenient way to apply common options to all of them.
+# export OZONE_SERVER_OPTS=""
+
+# Simple override of the default log level used to build OZONE_ROOT_LOGGER and
+# OZONE_DAEMON_ROOT_LOGGER. Defaults to INFO.
+# export OZONE_LOGLEVEL=INFO
+
# Where (primarily) daemon log files are stored.
# ${OZONE_HOME}/logs by default.
# Java property: hadoop.log.dir
@@ -193,6 +206,15 @@ export OZONE_OS_TYPE=${OZONE_OS_TYPE:-$(uname -s)}
# Java property: hadoop.root.logger
# export OZONE_DAEMON_ROOT_LOGGER=INFO,RFA
+# Default log4j setting for the HTTP request log of interactive commands
+# Java property: ozone.http.request.logger
+# export OZONE_HTTP_REQUEST_LOGGER=INFO,console
+
+# Default log4j setting for the HTTP request log of daemons spawned explicitly by
+# --daemon option of ozone command.
+# Java property: ozone.http.request.logger
+# export OZONE_DAEMON_HTTP_REQUEST_LOGGER=INFO,HttpAccess
+
# Default log level and output location for security-related messages.
# You will almost certainly want to change this on a per-daemon basis via
# the Java property (i.e., -Dhadoop.security.logger=foo).
@@ -207,16 +229,6 @@ export OZONE_OS_TYPE=${OZONE_OS_TYPE:-$(uname -s)}
# Java property: hadoop.policy.file
# export OZONE_POLICYFILE="hadoop-policy.xml"
-#
-# NOTE: this is not used by default! <-----
-# You can define variables right here and then re-use them later on.
-# For example, it is common to use the same garbage collection settings
-# for all the daemons. So one could define:
-#
-# export OZONE_GC_SETTINGS="-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps"
-#
-# .. and then use it when setting OZONE_OM_OPTS, etc. below
-
###
# Secure/privileged execution
###
@@ -233,6 +245,9 @@ export OZONE_OS_TYPE=${OZONE_OS_TYPE:-$(uname -s)}
# data transfer protocol using non-privileged ports.
# export JSVC_HOME=/usr/bin
+# Extra arguments to pass to jsvc when launching secure/privileged daemons.
+# export OZONE_DAEMON_JSVC_EXTRA_OPTS=""
+
#
# This directory contains pids for secure and privileged processes.
#export OZONE_SECURE_PID_DIR=${OZONE_PID_DIR}
@@ -240,14 +255,22 @@ export OZONE_OS_TYPE=${OZONE_OS_TYPE:-$(uname -s)}
#
# This directory contains the logs for secure and privileged processes.
# Java property: hadoop.log.dir
-# export OZONE_SECURE_LOG=${OZONE_LOG_DIR}
+# export OZONE_SECURE_LOG_DIR=${OZONE_LOG_DIR}
+###
+# Netty native (direct) memory caps (HDDS-11234)
+###
+# Both unshaded io.netty and the Ratis-shaded copy default their pooled
+# direct-memory ceiling to MaxDirectMemorySize (≈ -Xmx) per JVM, which
+# can let the resident size of a busy DataNode or S3 Gateway grow well
+# beyond the heap. To put a hard cap on each pool, export one or both
+# of the following before starting Ozone daemons. Values are raw byte
+# counts (suffixes like "m" or "g" are NOT supported by Netty's
+# property parser); for example, 536870912 = 512 MiB.
#
-# When running a secure daemon, the default value of OZONE_IDENT_STRING
-# ends up being a bit bogus. Therefore, by default, the code will
-# replace OZONE_IDENT_STRING with OZONE_xx_SECURE_USER. If one wants
-# to keep OZONE_IDENT_STRING untouched, then uncomment this line.
-# export OZONE_SECURE_IDENT_PRESERVE="true"
+# For example, to cap each pool at 4 GiB on a DataNode with -Xmx16g:
+# export OZONE_NETTY_MAX_DIRECT_MEMORY=4294967296
+# export OZONE_RATIS_NETTY_MAX_DIRECT_MEMORY=4294967296
###
# Ozone Manager specific parameters
@@ -276,6 +299,48 @@ export OZONE_OS_TYPE=${OZONE_OS_TYPE:-$(uname -s)}
#
# export OZONE_SCM_OPTS=""
+###
+# S3 Gateway specific parameters
+###
+# Specify the JVM options to be used when starting the S3 Gateway.
+# These options will be appended to the options specified as OZONE_OPTS
+# and therefore may override any similar flags set in OZONE_OPTS
+#
+# export OZONE_S3G_OPTS=""
+
+###
+# Recon specific parameters
+###
+# Specify the JVM options to be used when starting Recon.
+# These options will be appended to the options specified as OZONE_OPTS
+# and therefore may override any similar flags set in OZONE_OPTS
+#
+# export OZONE_RECON_OPTS=""
+
+###
+# HttpFS Gateway specific parameters
+###
+# Specify the JVM options to be used when starting the HttpFS Gateway.
+# These options will be appended to the options specified as OZONE_OPTS
+# and therefore may override any similar flags set in OZONE_OPTS
+#
+# export OZONE_HTTPFS_OPTS=""
+
+###
+# Client and tool command specific parameters
+###
+# Specify the JVM options to be used when running the corresponding command
+# (ozone sh, fs, admin, debug, freon, vapor). These options will be appended
+# to the options specified as OZONE_OPTS and therefore may override any
+# similar flags set in OZONE_OPTS
+#
+# export OZONE_SH_OPTS=""
+# export OZONE_FS_OPTS=""
+# export OZONE_ADMIN_OPTS=""
+# export OZONE_DEBUG_OPTS=""
+# export OZONE_FREON_OPTS=""
+# export OZONE_VAPOR_OPTS=""
+
###
# Advanced Users Only!
###
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/DatanodeVersion.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/DatanodeVersion.java
index 2717e8eb3d9d..560a1edf123b 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/DatanodeVersion.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/DatanodeVersion.java
@@ -36,6 +36,8 @@ public enum DatanodeVersion implements ComponentVersion {
STREAM_BLOCK_SUPPORT(3,
"This version has support for reading a block by streaming chunks."),
+ SHORT_CIRCUIT_READS(4, "Version with short-circuit read support."),
+
FUTURE_VERSION(-1, "Used internally in the client when the server side is "
+ " newer and an unknown server version has arrived to the client.");
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java
index 8d9cd1c6caf1..4804bfdebaca 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java
@@ -39,6 +39,10 @@ public final class HddsConfigKeys {
"hdds.heartbeat.recon.initial-interval";
public static final String HDDS_RECON_INITIAL_HEARTBEAT_INTERVAL_DEFAULT =
"2s";
+ /** Missed heartbeats against one SCM before the DN re-resolves its hostname (HDDS-15533). */
+ public static final String HDDS_HEARTBEAT_ADDRESS_REFRESH_MISSED_COUNT_THRESHOLD =
+ "hdds.heartbeat.address.refresh.missed-count-threshold";
+ public static final int HDDS_HEARTBEAT_ADDRESS_REFRESH_MISSED_COUNT_THRESHOLD_DEFAULT = 3;
public static final String HDDS_NODE_REPORT_INTERVAL =
"hdds.node.report.interval";
public static final String HDDS_NODE_REPORT_INTERVAL_DEFAULT =
@@ -116,6 +120,14 @@ public final class HddsConfigKeys {
"hdds.scm.safemode.log.interval";
public static final String HDDS_SCM_SAFEMODE_LOG_INTERVAL_DEFAULT = "1m";
+ /**
+ * Interval for background refresh of safeMode rules. 0 disables the background thread.
+ */
+ public static final String HDDS_SCM_SAFEMODE_RULE_REFRESH_INTERVAL =
+ "hdds.scm.safemode.rule.refresh.interval";
+ public static final String
+ HDDS_SCM_SAFEMODE_RULE_REFRESH_INTERVAL_DEFAULT = "5s";
+
// This configuration setting is used as a fallback location by all
// Ozone/HDDS services for their metadata. It is useful as a single
// config point for test/PoC clusters.
@@ -410,7 +422,7 @@ public final class HddsConfigKeys {
public static final String HDDS_DATANODE_DISK_BALANCER_ENABLED_KEY =
"hdds.datanode.disk.balancer.enabled";
- public static final boolean HDDS_DATANODE_DISK_BALANCER_ENABLED_DEFAULT = false;
+ public static final boolean HDDS_DATANODE_DISK_BALANCER_ENABLED_DEFAULT = true;
public static final String HDDS_DATANODE_DNS_INTERFACE_KEY =
"hdds.datanode.dns.interface";
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsUtils.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsUtils.java
index fce0f295e33d..3e6715b11c36 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsUtils.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsUtils.java
@@ -165,7 +165,7 @@ public static Collection getScmAddressForClients(
}
return Collections.singletonList(
- NetUtils.createSocketAddr(getHostName(address).get() + ":" + port));
+ NetUtils.createSocketAddr(getHostPortString(getHostName(address).get(), port)));
}
}
@@ -203,7 +203,7 @@ public static Optional getHostName(String value) {
if ((value == null) || value.isEmpty()) {
return Optional.empty();
}
- String hostname = value.replaceAll("\\:[0-9]+$", "");
+ String hostname = HostAndPort.fromString(value).getHost();
if (hostname.isEmpty()) {
return Optional.empty();
} else {
@@ -228,6 +228,21 @@ public static OptionalInt getHostPort(String value) {
}
}
+ /**
+ * Combine a host and port into a "host:port" string, wrapping the host in
+ * square brackets when it is an IPv6 literal (for example
+ * {@code [2001:db8::1]:9858}). A bare IPv6 literal joined to a port with a
+ * plain colon is ambiguous and cannot be parsed by Ratis/gRPC targets or
+ * URI-based address parsers.
+ *
+ * @param host a hostname, IPv4 literal, or (bracketed or bare) IPv6 literal
+ * @param port the port number
+ * @return the combined address, bracketed for IPv6 literals
+ */
+ public static String getHostPortString(String host, int port) {
+ return HostAndPort.fromParts(host, port).toString();
+ }
+
/**
* Retrieve a number, trying the supplied config keys in order.
* Each config value may be absent
@@ -358,6 +373,7 @@ public static boolean isReadOnly(
case PutBlock:
case PutSmallFile:
case StreamInit:
+ case StreamInitWithPutBlock:
case StreamWrite:
case FinalizeBlock:
return false;
@@ -570,23 +586,6 @@ public static File createDir(String dirPath) {
return dirFile;
}
- /**
- * Utility string formatter method to display SCM roles.
- *
- * @param nodes
- * @return String
- */
- public static String format(List nodes) {
- StringBuilder sb = new StringBuilder();
- for (String node : nodes) {
- String[] x = node.split(":");
- sb.append(String
- .format("{ HostName : %s, Ratis Port : %s, Role : %s } ", x[0], x[1],
- x[2]));
- }
- return sb.toString();
- }
-
/**
* Return Ozone service shutdown time out.
* @param conf
@@ -874,4 +873,26 @@ public static Collection getSCMNodeIds(
String scmServiceId = getScmServiceId(configuration);
return getSCMNodeIds(configuration, scmServiceId);
}
+
+ /**
+ * If {@code error} exposes an {@link AccessControlException} , returns one line error message.
+ */
+ public static String formatAccessControlExceptionLine(Throwable error) {
+ for (Throwable t = error; t != null; t = t.getCause()) {
+ if (t instanceof AccessControlException) {
+ return t.toString();
+ }
+ }
+ String msg = error != null ? error.getMessage() : null;
+ if (msg != null) {
+ String marker = AccessControlException.class.getName() + ": ";
+ int i = msg.indexOf(marker);
+ if (i >= 0) {
+ int end = msg.indexOf('\n', i);
+ String line = end < 0 ? msg.substring(i) : msg.substring(i, end);
+ return line.trim();
+ }
+ }
+ return null;
+ }
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/NodeDetails.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/NodeDetails.java
index 0984048bd513..ae871a8a86fd 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/NodeDetails.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/NodeDetails.java
@@ -103,11 +103,7 @@ public String getHostAddress() {
}
public String getRatisHostPortStr() {
- StringBuilder hostPort = new StringBuilder();
- hostPort.append(getHostName())
- .append(':')
- .append(ratisPort);
- return hostPort.toString();
+ return HddsUtils.getHostPortString(getHostName(), ratisPort);
}
public int getRatisPort() {
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/client/OzoneStoragePolicy.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/client/OzoneStoragePolicy.java
index 8ffade6a68a5..cdfbf2953db5 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/client/OzoneStoragePolicy.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/client/OzoneStoragePolicy.java
@@ -32,6 +32,8 @@ public enum OzoneStoragePolicy implements StoragePolicy {
private final StorageTier creationTier;
private final StorageTier creationFallbackTier;
+ private static OzoneStoragePolicy defaultPolicy = WARM;
+
OzoneStoragePolicy(String name, StorageTier creationTier,
StorageTier creationFallbackTier) {
this.name = name;
@@ -72,6 +74,23 @@ public StoragePolicyProto toProto() {
}
}
+ /**
+ * Converts the provided StoragePolicy to its protobuf representation.
+ *
+ * @param storagePolicy the StoragePolicy to convert.
+ * @return the corresponding StoragePolicyProto.
+ */
+ public static StoragePolicyProto toProto(StoragePolicy storagePolicy) {
+ if (storagePolicy == null) {
+ throw new IllegalArgumentException("Error: StoragePolicy cannot be null.");
+ }
+ if (!(storagePolicy instanceof OzoneStoragePolicy)) {
+ throw new IllegalArgumentException(
+ "Error: Unsupported StoragePolicy type: " + storagePolicy.getName());
+ }
+ return ((OzoneStoragePolicy) storagePolicy).toProto();
+ }
+
/**
* Converts a protobuf StoragePolicyProto to the corresponding OzoneStoragePolicy.
* @param proto the StoragePolicyProto to convert.
@@ -93,6 +112,14 @@ public static OzoneStoragePolicy fromProto(StoragePolicyProto proto) {
}
}
+ public static OzoneStoragePolicy getDefaultPolicy() {
+ return defaultPolicy;
+ }
+
+ public static void setDefaultPolicy(OzoneStoragePolicy storagePolicy) {
+ defaultPolicy = storagePolicy;
+ }
+
@Override
public String toString() {
return "OzoneStoragePolicy{"
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/conf/OzoneConfiguration.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/conf/OzoneConfiguration.java
index 0fabba6df3bf..69c4c029ce10 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/conf/OzoneConfiguration.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/conf/OzoneConfiguration.java
@@ -228,7 +228,6 @@ public static List getConfigurationResourceFiles() {
"hdds-server-framework",
"hdds-server-scm",
"ozone-common",
- "ozone-csi",
"ozone-manager",
"ozone-recon",
};
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/protocol/DatanodeDetails.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/protocol/DatanodeDetails.java
index e75e1a5c5b32..2ea4fa64213b 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/protocol/DatanodeDetails.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/protocol/DatanodeDetails.java
@@ -30,8 +30,10 @@
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.EnumMap;
import java.util.EnumSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@@ -83,7 +85,7 @@ public class DatanodeDetails extends NodeImpl implements Comparable ports;
+ private final Map ports;
private String certSerialId;
private String version;
private long setupTime;
@@ -146,17 +148,6 @@ public DatanodeID getID() {
return id;
}
- /**
- * Returns the DataNode UUID.
- *
- * @return UUID of DataNode
- */
- // TODO: Remove this in follow-up Jira (HDDS-12015)
- @Deprecated
- public UUID getUuid() {
- return id.getUuid();
- }
-
/**
* Returns the string representation of DataNode UUID.
*
@@ -254,10 +245,8 @@ public StringWithByteString getHostNameAsByteString() {
* @param port DataNode port
*/
public synchronized void setPort(Port port) {
- // If the port is already in the list remove it first and add the
- // new/updated port value.
- ports.remove(port);
- ports.add(port);
+ // Overwrites any existing port with the same name.
+ ports.put(port.getName(), port);
}
public synchronized void setPort(Name name, int port) {
@@ -282,11 +271,11 @@ public void setStandalonePort(int port) {
* @return DataNode Ports
*/
public synchronized List getPorts() {
- return new ArrayList<>(ports);
+ return new ArrayList<>(ports.values());
}
public synchronized boolean hasPort(int port) {
- for (Port p : ports) {
+ for (Port p : ports.values()) {
if (p.getValue() == port) {
return true;
}
@@ -366,38 +355,51 @@ public void setPersistedOpStateExpiryEpochSec(long expiry) {
* @return Port
*/
public synchronized Port getPort(Port.Name name) {
- Port ratisPort = null;
- for (Port port : ports) {
- if (port.getName().equals(name)) {
- return port;
- }
- if (port.getName().equals(Name.RATIS)) {
- ratisPort = port;
- }
+ final Port port = ports.get(name);
+ if (port != null) {
+ return port;
}
// if no separate admin/server/datastream port,
// return single Ratis one for compatibility
if (name == Name.RATIS_ADMIN || name == Name.RATIS_SERVER ||
name == Name.RATIS_DATASTREAM) {
- return ratisPort;
+ return ports.get(Name.RATIS);
}
return null;
}
- // CHANGE: add a helper to check whether a port is explicitly present
- // without applying compatibility fallback.
+ // Checks whether a port is explicitly present without applying the
+ // compatibility fallback in getPort.
public synchronized boolean hasPort(Port.Name name) {
- for (Port port : ports) {
- if (port.getName().equals(name)) {
- return true;
- }
- }
- return false;
+ return ports.containsKey(name);
+ }
+
+ /**
+ * Whether this datanode's exposed ports differ from {@code other}'s.
+ * Compared by name and value, since {@link Port#equals} ignores the port
+ * value.
+ *
+ * @param other another snapshot of this datanode
+ * @return true if the two port sets are not identical
+ */
+ public boolean portsChanged(DatanodeDetails other) {
+ return !portValues().equals(other.portValues());
+ }
+
+ /**
+ * A name-to-value snapshot of this datanode's ports, taken under its lock
+ * so {@link #portsChanged} can compare two nodes value-aware without holding
+ * both locks at once.
+ */
+ private synchronized Map portValues() {
+ final Map values = new EnumMap<>(Port.Name.class);
+ ports.forEach((name, port) -> values.put(name, port.getValue()));
+ return values;
}
/**
* Helper method to get the Ratis port.
- *
+ *
* @return Port
*/
public Port getRatisPort() {
@@ -587,7 +589,7 @@ public HddsProtos.DatanodeDetailsProto.Builder toProtoBuilder(
.compareTo(VERSION_HANDLES_UNKNOWN_DN_PORTS) >= 0;
final int requestedPortCount = filterPorts.size();
final boolean maySkip = requestedPortCount > 0;
- for (Port port : ports) {
+ for (Port port : ports.values()) {
if (maySkip && !filterPorts.contains(port.getName())) {
if (LOG.isDebugEnabled()) {
LOG.debug("Skip adding {} port {} to proto message",
@@ -730,7 +732,7 @@ public static final class Builder {
private StringWithByteString networkName;
private StringWithByteString networkLocation;
private int level;
- private List ports;
+ private Map ports;
private String certSerialId;
private String version;
private long setupTime;
@@ -745,7 +747,7 @@ public static final class Builder {
* DatanodeDetails#newBuilder.
*/
private Builder() {
- ports = new ArrayList<>();
+ ports = new EnumMap<>(Port.Name.class);
}
/**
@@ -761,7 +763,8 @@ public Builder setDatanodeDetails(DatanodeDetails details) {
this.networkName = details.getNetworkNameAsByteString();
this.networkLocation = details.getNetworkLocationAsByteString();
this.level = details.getLevel();
- this.ports = details.getPorts();
+ this.ports = new EnumMap<>(Port.Name.class);
+ details.getPorts().forEach(this::addPort);
this.certSerialId = details.getCertSerialId();
this.version = details.getVersion();
this.setupTime = details.getSetupTime();
@@ -877,7 +880,7 @@ public Builder setLevel(int level) {
* @return DatanodeDetails.Builder
*/
public Builder addPort(Port port) {
- this.ports.add(port);
+ this.ports.put(port.getName(), port);
return this;
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/protocol/DatanodeID.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/protocol/DatanodeID.java
index 6a0ee0b43c36..c7cda30ed4b9 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/protocol/DatanodeID.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/protocol/DatanodeID.java
@@ -44,11 +44,6 @@ private DatanodeID(final UUID uuid) {
this.uuidByteString = StringWithByteString.valueOf(uuid.toString());
}
- // Mainly used for JSON conversion
- public String getID() {
- return toString();
- }
-
@Override
public int compareTo(final DatanodeID that) {
return this.uuid.compareTo(that.uuid);
@@ -70,6 +65,11 @@ public String toString() {
return uuidByteString.getString();
}
+ // Mainly used for JSON conversion
+ public String getUuid() {
+ return toString();
+ }
+
/**
* This will be removed once the proto structure is refactored
* to remove deprecated fields.
@@ -121,11 +121,4 @@ private static HddsProtos.UUID toProto(final UUID id) {
.setLeastSigBits(id.getLeastSignificantBits())
.build();
}
-
- // TODO: Remove this in follow-up Jira. (HDDS-12015)
- // Exposing this temporarily to help with refactoring.
- @Deprecated
- public UUID getUuid() {
- return uuid;
- }
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/ratis/RatisHelper.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/ratis/RatisHelper.java
index b2813bad1e2e..4a30248f4a76 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/ratis/RatisHelper.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/ratis/RatisHelper.java
@@ -36,6 +36,7 @@
import java.util.stream.Collectors;
import javax.net.ssl.TrustManager;
import org.apache.hadoop.hdds.HddsConfigKeys;
+import org.apache.hadoop.hdds.HddsUtils;
import org.apache.hadoop.hdds.conf.ConfigurationSource;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.protocol.DatanodeDetails;
@@ -125,18 +126,13 @@ public static UUID toDatanodeId(RaftProtos.RaftPeerProto peerId) {
}
private static String toRaftPeerAddress(DatanodeDetails id, Port.Name port) {
- if (datanodeUseHostName()) {
- final String address =
- id.getHostName() + ":" + id.getPort(port).getValue();
- LOG.debug("Datanode is using hostname for raft peer address: {}",
- address);
- return address;
- } else {
- final String address =
- id.getIpAddress() + ":" + id.getPort(port).getValue();
- LOG.debug("Datanode is using IP for raft peer address: {}", address);
- return address;
- }
+ final boolean useHostName = datanodeUseHostName();
+ final String address = HddsUtils.getHostPortString(
+ useHostName ? id.getHostName() : id.getIpAddress(),
+ id.getPort(port).getValue());
+ LOG.debug("Datanode is using {} for raft peer address: {}",
+ useHostName ? "hostname" : "IP", address);
+ return address;
}
public static RaftPeerId toRaftPeerId(DatanodeDetails id) {
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/ratis/conf/RatisClientConfig.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/ratis/conf/RatisClientConfig.java
index 03d19cf6ea02..2097ea65fb3d 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/ratis/conf/RatisClientConfig.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/ratis/conf/RatisClientConfig.java
@@ -45,21 +45,21 @@ public class RatisClientConfig {
private String watchType;
@Config(key = "hdds.ratis.client.request.write.timeout",
- defaultValue = "5m",
+ defaultValue = "70s",
type = ConfigType.TIME,
tags = { OZONE, CLIENT, PERFORMANCE },
description = "Timeout for ratis client write request.")
- private Duration writeRequestTimeout = Duration.ofMinutes(5);
+ private Duration writeRequestTimeout = Duration.ofSeconds(70);
@Config(key = "hdds.ratis.client.request.watch.timeout",
- defaultValue = "3m",
+ defaultValue = "30s",
type = ConfigType.TIME,
tags = { OZONE, CLIENT, PERFORMANCE },
description = "Timeout for ratis client watch request.")
- private Duration watchRequestTimeout = Duration.ofMinutes(3);
+ private Duration watchRequestTimeout = Duration.ofSeconds(30);
@Config(key = "hdds.ratis.client.multilinear.random.retry.policy",
- defaultValue = "5s, 5, 10s, 5, 15s, 5, 20s, 5, 25s, 5, 60s, 10",
+ defaultValue = "5s, 6",
type = ConfigType.STRING,
tags = { OZONE, CLIENT, PERFORMANCE },
description = "Specifies multilinear random retry policy to be used by"
@@ -70,31 +70,31 @@ public class RatisClientConfig {
private String multilinearPolicy;
@Config(key = "hdds.ratis.client.exponential.backoff.base.sleep",
- defaultValue = "4s",
+ defaultValue = "1s",
type = ConfigType.TIME,
tags = { OZONE, CLIENT, PERFORMANCE },
description = "Specifies base sleep for exponential backoff retry policy."
- + " With the default base sleep of 4s, the sleep duration for ith"
- + " retry is min(4 * pow(2, i), max_sleep) * r, where r is "
+ + " With the default base sleep of 1s, the sleep duration for ith"
+ + " retry is min(1 * pow(2, i), max_sleep) * r, where r is "
+ "random number in the range [0.5, 1.5).")
- private Duration exponentialPolicyBaseSleep = Duration.ofSeconds(4);
+ private Duration exponentialPolicyBaseSleep = Duration.ofSeconds(1);
@Config(key = "hdds.ratis.client.exponential.backoff.max.sleep",
- defaultValue = "40s",
+ defaultValue = "5s",
type = ConfigType.TIME,
tags = { OZONE, CLIENT, PERFORMANCE },
description = "The sleep duration obtained from exponential backoff "
+ "policy is limited by the configured max sleep. Refer "
+ "dfs.ratis.client.exponential.backoff.base.sleep for further "
+ "details.")
- private Duration exponentialPolicyMaxSleep = Duration.ofSeconds(40);
+ private Duration exponentialPolicyMaxSleep = Duration.ofSeconds(5);
@Config(key = "hdds.ratis.client.exponential.backoff.max.retries",
- defaultValue = "2147483647",
+ defaultValue = "2",
type = ConfigType.INT,
tags = { OZONE, CLIENT, PERFORMANCE },
description = "Client's max retry value for the exponential backoff policy.")
- private int exponentialPolicyMaxRetries = Integer.MAX_VALUE;
+ private int exponentialPolicyMaxRetries = 2;
@Config(key = "hdds.ratis.client.retrylimited.retry.interval",
defaultValue = "1s",
@@ -215,16 +215,16 @@ public static class RaftConfig {
private Duration rpcRequestTimeout = Duration.ofSeconds(60);
@Config(key = "hdds.ratis.raft.client.rpc.watch.request.timeout",
- defaultValue = "180s",
+ defaultValue = "30s",
type = ConfigType.TIME,
tags = { OZONE, CLIENT, PERFORMANCE },
description =
"The timeout duration for ratis client watch request. "
+ "Timeout for the watch API in Ratis client to acknowledge a "
+ "particular request getting replayed to all servers. "
- + "It is highly recommended for the timeout duration to be strictly longer than "
+ + "It is recommended for the timeout duration to be at least as long as "
+ "Ratis server watch timeout (hdds.ratis.raft.server.watch.timeout)")
- private Duration rpcWatchRequestTimeout = Duration.ofSeconds(180);
+ private Duration rpcWatchRequestTimeout = Duration.ofSeconds(30);
public int getMaxOutstandingRequests() {
return maxOutstandingRequests;
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/ScmConfigKeys.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/ScmConfigKeys.java
index e0b28548e8e4..598d79a011c4 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/ScmConfigKeys.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/ScmConfigKeys.java
@@ -269,6 +269,11 @@ public final class ScmConfigKeys {
public static final String OZONE_SCM_STALENODE_INTERVAL_DEFAULT =
"5m";
+ public static final String OZONE_SCM_PENDING_CONTAINER_ROLL_INTERVAL =
+ "ozone.scm.pending.container.roll.interval";
+ public static final String OZONE_SCM_PENDING_CONTAINER_ROLL_INTERVAL_DEFAULT =
+ "5m";
+
public static final String OZONE_SCM_HEARTBEAT_RPC_TIMEOUT =
"ozone.scm.heartbeat.rpc-timeout";
public static final String OZONE_SCM_HEARTBEAT_RPC_TIMEOUT_DEFAULT =
@@ -371,6 +376,9 @@ public final class ScmConfigKeys {
"ozone.scm.pipeline.placement.impl";
public static final String OZONE_SCM_CONTAINER_PLACEMENT_EC_IMPL_KEY =
"ozone.scm.container.placement.ec.impl";
+ public static final String OZONE_SCM_CONTAINER_PLACEMENT_RACK_SCATTER_CAPACITY_AWARE_ENABLED =
+ "ozone.scm.container.placement.rack.scatter.capacity.aware.enabled";
+ public static final boolean OZONE_SCM_CONTAINER_PLACEMENT_RACK_SCATTER_CAPACITY_AWARE_ENABLED_DEFAULT = false;
public static final String OZONE_SCM_PIPELINE_OWNER_CONTAINER_COUNT =
"ozone.scm.pipeline.owner.container.count";
@@ -454,6 +462,18 @@ public final class ScmConfigKeys {
public static final boolean
OZONE_SCM_PIPELINE_AUTO_CREATE_FACTOR_ONE_DEFAULT = true;
+ /**
+ * If true, BackgroundPipelineCreator will create RATIS/THREE pipelines even
+ * when the default replication is EC. This keeps RATIS write paths warm for
+ * mixed-workload clusters. If false, RATIS/THREE pipeline creation is
+ * skipped for EC-default clusters.
+ */
+ public static final String OZONE_SCM_PIPELINE_CREATE_RATIS_THREE =
+ "ozone.scm.pipeline.creation.ratis.three";
+
+ public static final boolean
+ OZONE_SCM_PIPELINE_CREATE_RATIS_THREE_DEFAULT = true;
+
public static final String OZONE_SCM_BLOCK_DELETION_PER_DN_DISTRIBUTION_FACTOR =
"ozone.scm.block.deletion.per.dn.distribution.factor";
@@ -584,15 +604,6 @@ public final class ScmConfigKeys {
public static final long OZONE_SCM_HA_RATIS_SNAPSHOT_THRESHOLD_DEFAULT =
1000L;
- /**
- * the config will transfer value to ratis config
- * raft.server.snapshot.creation.gap, used by ratis to take snapshot
- * when manual trigger using api.
- */
- public static final String OZONE_SCM_HA_RATIS_SNAPSHOT_GAP
- = "ozone.scm.ha.ratis.server.snapshot.creation.gap";
- public static final long OZONE_SCM_HA_RATIS_SNAPSHOT_GAP_DEFAULT =
- 1024L;
public static final String OZONE_SCM_HA_RATIS_SNAPSHOT_DIR =
"ozone.scm.ha.ratis.snapshot.dir";
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientSpi.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientSpi.java
index 54be3c5686a0..60d3a1d7b70a 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientSpi.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientSpi.java
@@ -20,6 +20,7 @@
import com.google.common.annotations.VisibleForTesting;
import java.io.Closeable;
import java.io.IOException;
+import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@@ -28,6 +29,7 @@
import org.apache.hadoop.hdds.HddsUtils;
import org.apache.hadoop.hdds.client.BlockID;
import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ContainerCommandRequestProto;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ContainerCommandResponseProto;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
@@ -50,6 +52,14 @@ public interface Validator extends
// just a shortcut to avoid having to repeat long list of generic parameters
}
+ /**
+ * Validator for container read chunk through short-circuit local reads.
+ */
+ public interface ShortCircuitValidator extends
+ CheckedBiConsumer, ContainerProtos.ChunkInfo, IOException> {
+ // just a shortcut to avoid having to repeat long list of generic parameters
+ }
+
public XceiverClientSpi() {
this.referenceCount = new AtomicInteger(0);
this.isEvicted = false;
@@ -91,6 +101,10 @@ public int getRefcount() {
@Override
public abstract void close();
+ public boolean isClosed() {
+ return false;
+ }
+
/**
* Returns the pipeline of machines that host the container used by this
* client.
@@ -149,7 +163,8 @@ public void initStreamRead(BlockID blockID, StreamingReaderSpi streamObserver) t
throw new UnsupportedOperationException("Stream read is not supported");
}
- public void streamRead(ContainerCommandRequestProto request, StreamingReadResponse streamObserver) {
+ public void streamRead(ContainerCommandRequestProto request,
+ StreamingReadResponse streamObserver) throws IOException {
throw new UnsupportedOperationException("Stream read is not supported");
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerID.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerID.java
index ad7dec5fc4c7..61ae9517069d 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerID.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerID.java
@@ -27,6 +27,7 @@
import org.apache.hadoop.hdds.utils.db.DelegatedCodec;
import org.apache.hadoop.hdds.utils.db.LongCodec;
import org.apache.ratis.util.MemoizedSupplier;
+import org.apache.ratis.util.WeakValueCache;
/**
* Container ID is an integer that is a value between 1..MAX_CONTAINER ID.
@@ -42,7 +43,9 @@ public final class ContainerID implements Comparable {
LongCodec.get(), ContainerID::valueOf, c -> c.id,
ContainerID.class, DelegatedCodec.CopyType.SHALLOW);
- public static final ContainerID MIN = ContainerID.valueOf(0);
+ public static final ContainerID MIN = new ContainerID(0);
+ private static final WeakValueCache CACHE
+ = new WeakValueCache<>("containerId", ContainerID::new);
private final long id;
private final Supplier proto;
@@ -71,7 +74,11 @@ private ContainerID(long id) {
* @return ContainerID.
*/
public static ContainerID valueOf(final long containerID) {
- return new ContainerID(containerID);
+ return CACHE.getOrCreate(containerID);
+ }
+
+ static WeakValueCache getCacheForTesting() {
+ return CACHE;
}
/**
@@ -87,6 +94,10 @@ public long getId() {
return id;
}
+ public long getIdForTesting() {
+ return id;
+ }
+
public static byte[] getBytes(long id) {
return LongCodec.get().toPersistedFormat(id);
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerInfo.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerInfo.java
index 17e7d5a646e5..3b2c8ccf1a81 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerInfo.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerInfo.java
@@ -79,11 +79,8 @@ public final class ContainerInfo implements Comparable {
// field and hence maintain the original output.
@JsonIgnore
private final ContainerID containerID;
- // Delete Transaction Id is updated when new transaction for a container
- // is stored in SCM delete Table.
- // TODO: Replication Manager should consider deleteTransactionId so that
- // replica with higher deleteTransactionId is preferred over replica with
- // lower deleteTransactionId.
+ // Deprecated SCM-side delete transaction ID retained for old persisted data, SCM no longer updates this field.
+ @Deprecated
private long deleteTransactionId;
// The sequenceId of a close container cannot change, and all the
// container replica should have the same sequenceId.
@@ -226,6 +223,13 @@ public void setNumberOfKeys(long value) {
numberOfKeys = value;
}
+ /**
+ * Legacy SCM-side delete transaction ID. SCM no longer updates this field.
+ *
+ * @deprecated SCM no longer updates this field. Use DN-side container data
+ * for delete transaction tracking.
+ */
+ @Deprecated
public long getDeleteTransactionId() {
return deleteTransactionId;
}
@@ -234,10 +238,6 @@ public long getSequenceId() {
return sequenceId;
}
- public void updateDeleteTransactionId(long transactionId) {
- deleteTransactionId = max(transactionId, deleteTransactionId);
- }
-
public void updateSequenceId(long sequenceID) {
assert (isOpen() || state == HddsProtos.LifeCycleState.QUASI_CLOSED);
sequenceId = max(sequenceID, sequenceId);
@@ -483,6 +483,7 @@ public Builder setOwner(String containerOwner) {
return this;
}
+ @Deprecated
public Builder setDeleteTransactionId(long deleteTransactionID) {
this.deleteTransactionId = deleteTransactionID;
return this;
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerReplicaInfo.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerReplicaInfo.java
index b9b9d679d63b..2aaa45d695de 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerReplicaInfo.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerReplicaInfo.java
@@ -18,8 +18,8 @@
package org.apache.hadoop.hdds.scm.container;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
-import java.util.UUID;
import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.DatanodeID;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
import org.apache.hadoop.hdds.server.JsonUtils;
@@ -31,7 +31,7 @@ public final class ContainerReplicaInfo {
private long containerID;
private String state;
private DatanodeDetails datanodeDetails;
- private UUID placeOfBirth;
+ private DatanodeID placeOfBirth;
private long sequenceId;
private long keyCount;
private long bytesUsed;
@@ -46,7 +46,7 @@ public static ContainerReplicaInfo fromProto(
.setState(proto.getState())
.setDatanodeDetails(DatanodeDetails
.getFromProtoBuf(proto.getDatanodeDetails()))
- .setPlaceOfBirth(UUID.fromString(proto.getPlaceOfBirth()))
+ .setPlaceOfBirth(DatanodeID.fromUuidString(proto.getPlaceOfBirth()))
.setSequenceId(proto.getSequenceID())
.setKeyCount(proto.getKeyCount())
.setBytesUsed(proto.getBytesUsed())
@@ -71,7 +71,7 @@ public DatanodeDetails getDatanodeDetails() {
return datanodeDetails;
}
- public UUID getPlaceOfBirth() {
+ public DatanodeID getPlaceOfBirth() {
return placeOfBirth;
}
@@ -117,7 +117,7 @@ public Builder setDatanodeDetails(DatanodeDetails datanodeDetails) {
return this;
}
- public Builder setPlaceOfBirth(UUID placeOfBirth) {
+ public Builder setPlaceOfBirth(DatanodeID placeOfBirth) {
subject.placeOfBirth = placeOfBirth;
return this;
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerConfiguration.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerConfiguration.java
index 12ad1501009d..65acb9da8e15 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerConfiguration.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerConfiguration.java
@@ -206,7 +206,7 @@ public void setIterations(int count) {
* Gets the maximum percentage of healthy, in-service datanodes that will be
* involved in balancing in one iteration.
*
- * @return percentage as an integer from 0 up to and including 100
+ * @return percentage as an integer greater than 0 up to and including 100
*/
public int getMaxDatanodesPercentageToInvolvePerIteration() {
return maxDatanodesPercentageToInvolvePerIteration;
@@ -253,6 +253,17 @@ public double getMaxDatanodesRatioToInvolvePerIteration() {
return maxDatanodesPercentageToInvolvePerIteration / 100d;
}
+ /**
+ * Computes the maximum number of datanodes that may be involved in an
+ * iteration for the given eligible datanode count.
+ *
+ * @param eligibleDatanodeCount number of healthy, in-service datanodes.
+ * @return maximum datanodes that may be involved in one iteration
+ */
+ public int computeMaxDatanodesToInvolvePerIteration(int eligibleDatanodeCount) {
+ return (int) (getMaxDatanodesRatioToInvolvePerIteration() * eligibleDatanodeCount);
+ }
+
/**
* Sets the maximum percentage of healthy, in-service datanodes that will be
* involved in balancing in one iteration.
@@ -266,10 +277,10 @@ public double getMaxDatanodesRatioToInvolvePerIteration() {
*/
public void setMaxDatanodesPercentageToInvolvePerIteration(
int maxDatanodesPercentageToInvolvePerIteration) {
- if (maxDatanodesPercentageToInvolvePerIteration < 0 ||
+ if (maxDatanodesPercentageToInvolvePerIteration <= 0 ||
maxDatanodesPercentageToInvolvePerIteration > 100) {
throw new IllegalArgumentException(String.format("Argument %d is " +
- "illegal. Percentage must be from 0 up to and including 100.",
+ "illegal. Percentage must be greater than 0 up to and including 100.",
maxDatanodesPercentageToInvolvePerIteration));
}
this.maxDatanodesPercentageToInvolvePerIteration =
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/common/helpers/AllocatedBlock.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/common/helpers/AllocatedBlock.java
index 888985a2e1e2..c176fb0ff8bf 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/common/helpers/AllocatedBlock.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/common/helpers/AllocatedBlock.java
@@ -17,7 +17,9 @@
package org.apache.hadoop.hdds.scm.container.common.helpers;
+import jakarta.annotation.Nullable;
import org.apache.hadoop.hdds.client.ContainerBlockID;
+import org.apache.hadoop.hdds.client.StorageTier;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
/**
@@ -28,12 +30,17 @@ public final class AllocatedBlock {
private final Pipeline pipeline;
private final ContainerBlockID containerBlockID;
+ private final @Nullable StorageTier storageTier;
+ private final boolean isFallBack;
+
/**
* Builder for AllocatedBlock.
*/
public static class Builder {
private Pipeline pipeline;
private ContainerBlockID containerBlockID;
+ private @Nullable StorageTier storageTier;
+ private boolean isFallBack;
public Builder setPipeline(Pipeline p) {
this.pipeline = p;
@@ -45,14 +52,27 @@ public Builder setContainerBlockID(ContainerBlockID blockId) {
return this;
}
+ public Builder setStorageTier(StorageTier storageTier) {
+ this.storageTier = storageTier;
+ return this;
+ }
+
+ public Builder setIsFallBack(boolean fallBack) {
+ isFallBack = fallBack;
+ return this;
+ }
+
public AllocatedBlock build() {
- return new AllocatedBlock(pipeline, containerBlockID);
+ return new AllocatedBlock(pipeline, containerBlockID, storageTier, isFallBack);
}
}
- private AllocatedBlock(Pipeline pipeline, ContainerBlockID containerBlockID) {
+ private AllocatedBlock(Pipeline pipeline, ContainerBlockID containerBlockID,
+ StorageTier storageTier, boolean isFallBack) {
this.pipeline = pipeline;
this.containerBlockID = containerBlockID;
+ this.storageTier = storageTier;
+ this.isFallBack = isFallBack;
}
public Pipeline getPipeline() {
@@ -70,6 +90,17 @@ public static Builder newBuilder() {
public Builder toBuilder() {
return new Builder()
.setContainerBlockID(containerBlockID)
- .setPipeline(pipeline);
+ .setPipeline(pipeline)
+ .setStorageTier(storageTier)
+ .setIsFallBack(isFallBack);
+ }
+
+ @Nullable
+ public StorageTier getStorageTier() {
+ return storageTier;
+ }
+
+ public boolean isFallBack() {
+ return isFallBack;
}
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/ha/SCMNodeInfo.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/ha/SCMNodeInfo.java
index dc2393fe4a99..98b138de1ce7 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/ha/SCMNodeInfo.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/ha/SCMNodeInfo.java
@@ -39,11 +39,13 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import java.util.OptionalInt;
import net.jcip.annotations.Immutable;
import org.apache.hadoop.hdds.HddsUtils;
import org.apache.hadoop.hdds.conf.ConfigurationException;
import org.apache.hadoop.hdds.conf.ConfigurationSource;
+import org.apache.hadoop.hdds.scm.net.HostAndPort;
import org.apache.hadoop.ozone.ha.ConfUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -60,10 +62,10 @@ public class SCMNodeInfo {
private static final Logger LOG = LoggerFactory.getLogger(SCMNodeInfo.class);
private final String serviceId;
private final String nodeId;
- private final String blockClientAddress;
- private final String scmClientAddress;
- private final String scmSecurityAddress;
- private final String scmDatanodeAddress;
+ private final HostAndPort blockClientAddress;
+ private final HostAndPort scmClientAddress;
+ private final HostAndPort scmSecurityAddress;
+ private final HostAndPort scmDatanodeAddress;
/**
* Build SCM Node information from configuration.
@@ -130,6 +132,9 @@ public static List buildNodeInfo(ConfigurationSource conf) {
String scmClientAddress = getHostNameFromConfigKeys(conf,
OZONE_SCM_CLIENT_ADDRESS_KEY,
OZONE_SCM_NAMES).orElse(null);
+ if (scmClientAddress == null) {
+ throw new ConfigurationException(OZONE_SCM_CLIENT_ADDRESS_KEY + " is not set");
+ }
String scmBlockClientAddress = getHostNameFromConfigKeys(conf,
OZONE_SCM_BLOCK_CLIENT_ADDRESS_KEY).orElse(scmClientAddress);
@@ -162,14 +167,10 @@ public static List buildNodeInfo(ConfigurationSource conf) {
scmNodeInfoList.add(new SCMNodeInfo(scmServiceId,
SCM_DUMMY_NODEID,
- scmBlockClientAddress == null ? null :
- buildAddress(scmBlockClientAddress, scmBlockClientPort),
- scmClientAddress == null ? null :
- buildAddress(scmClientAddress, scmClientPort),
- scmSecurityClientAddress == null ? null :
- buildAddress(scmSecurityClientAddress, scmSecurityPort),
- scmDatanodeAddress == null ? null :
- buildAddress(scmDatanodeAddress, scmDatanodePort)));
+ buildAddress(scmBlockClientAddress, scmBlockClientPort),
+ buildAddress(scmClientAddress, scmClientPort),
+ buildAddress(scmSecurityClientAddress, scmSecurityPort),
+ buildAddress(scmDatanodeAddress, scmDatanodePort)));
return scmNodeInfoList;
@@ -177,8 +178,8 @@ public static List buildNodeInfo(ConfigurationSource conf) {
}
- public static String buildAddress(String address, int port) {
- return address + ':' + port;
+ private static HostAndPort buildAddress(String address, int port) {
+ return new HostAndPort(address, port);
}
public static int getPort(ConfigurationSource conf,
@@ -209,14 +210,14 @@ public static int getPort(ConfigurationSource conf,
* @param scmDatanodeAddress
*/
public SCMNodeInfo(String serviceId, String nodeId,
- String blockClientAddress, String scmClientAddress,
- String scmSecurityAddress, String scmDatanodeAddress) {
+ HostAndPort blockClientAddress, HostAndPort scmClientAddress,
+ HostAndPort scmSecurityAddress, HostAndPort scmDatanodeAddress) {
this.serviceId = serviceId;
this.nodeId = nodeId;
- this.blockClientAddress = blockClientAddress;
- this.scmClientAddress = scmClientAddress;
- this.scmSecurityAddress = scmSecurityAddress;
- this.scmDatanodeAddress = scmDatanodeAddress;
+ this.blockClientAddress = Objects.requireNonNull(blockClientAddress, "blockClientAddress == null");
+ this.scmClientAddress = Objects.requireNonNull(scmClientAddress, "scmClientAddress == null");
+ this.scmSecurityAddress = Objects.requireNonNull(scmSecurityAddress, "scmSecurityAddress == null");
+ this.scmDatanodeAddress = Objects.requireNonNull(scmDatanodeAddress, "scmDatanodeAddress == null");
}
public String getServiceId() {
@@ -228,18 +229,22 @@ public String getNodeId() {
}
public String getBlockClientAddress() {
- return blockClientAddress;
+ return blockClientAddress.getHostAndPortString();
}
public String getScmClientAddress() {
- return scmClientAddress;
+ return scmClientAddress.getHostAndPortString();
}
public String getScmSecurityAddress() {
- return scmSecurityAddress;
+ return scmSecurityAddress.getHostAndPortString();
}
- public String getScmDatanodeAddress() {
+ public HostAndPort getScmDatanodeHostPortAddress() {
return scmDatanodeAddress;
}
+
+ public String getScmDatanodeAddress() {
+ return scmDatanodeAddress.getHostAndPortString();
+ }
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java
new file mode 100644
index 000000000000..2a142ea8a112
--- /dev/null
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hadoop.hdds.scm.net;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.Objects;
+import org.apache.hadoop.net.NetUtils;
+
+/**
+ * A class for host and port.
+ * It also has an address which can be updated from time to time.
+ */
+public class HostAndPort {
+ private final String host;
+ private final int port;
+ private final String hostAndPortString;
+ private final int hash;
+ /** The address can be updated from time to time. */
+ private volatile InetSocketAddress address;
+
+ public HostAndPort(String host, int port) {
+ this.host = host;
+ this.port = port;
+ this.hostAndPortString = host + ":" + port;
+ this.hash = host.hashCode() ^ Integer.hashCode(port);
+ this.address = NetUtils.createSocketAddr(hostAndPortString);
+ }
+
+ public String getHostName() {
+ return host;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public String getHostAndPortString() {
+ return hostAndPortString;
+ }
+
+ public InetSocketAddress getAddress() {
+ return address;
+ }
+
+ /** Re-resolves host:port and returns the new address if its IP changed, else null. No mutation. */
+ public InetSocketAddress resolveLatest() {
+ final InetSocketAddress latest = NetUtils.createSocketAddr(hostAndPortString);
+ final InetAddress latestIp = latest.getAddress();
+ if (latestIp == null || latestIp.equals(address.getAddress())) {
+ return null;
+ }
+ return latest;
+ }
+
+ /** Commits an address re-resolved via {@link #resolveLatest()}. */
+ public void setAddress(InetSocketAddress newAddress) {
+ this.address = Objects.requireNonNull(newAddress, "newAddress == null");
+ }
+
+ @Override
+ public int hashCode() {
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (!(obj instanceof HostAndPort)) {
+ return false;
+ }
+ final HostAndPort that = (HostAndPort) obj;
+ // address must not be compared
+ return this.hash == that.hash
+ && this.port == that.port
+ && this.host.equals(that.host);
+ }
+
+ @Override
+ public String toString() {
+ final InetSocketAddress a = getAddress();
+ final Object resolved = a != null && a.getAddress() != null ? a.getAddress() : "";
+ return hostAndPortString + "/" + resolved;
+ }
+}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/pipeline/Pipeline.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/pipeline/Pipeline.java
index 1b40cf2f5b38..a0fbed13ad2c 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/pipeline/Pipeline.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/pipeline/Pipeline.java
@@ -581,6 +581,7 @@ public String toString() {
b.append(" {").append(datanodeDetails)
.append(", ReplicaIndex: ").append(this.getReplicaIndex(datanodeDetails)).append("},");
}
+
b.append(']')
.append(", ReplicationConfig: ").append(replicationConfig)
.append(", State:").append(getPipelineState())
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/pipeline/PipelineID.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/pipeline/PipelineID.java
index d7ea21d024eb..bb2aa7cc6262 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/pipeline/PipelineID.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/pipeline/PipelineID.java
@@ -18,12 +18,14 @@
package org.apache.hadoop.hdds.scm.pipeline;
import com.fasterxml.jackson.annotation.JsonIgnore;
+import java.nio.ByteBuffer;
import java.util.UUID;
import java.util.function.Supplier;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
import org.apache.hadoop.hdds.utils.db.Codec;
import org.apache.hadoop.hdds.utils.db.DelegatedCodec;
import org.apache.hadoop.hdds.utils.db.UuidCodec;
+import org.apache.hadoop.ozone.util.UUIDUtil;
import org.apache.ratis.util.MemoizedSupplier;
/**
@@ -52,6 +54,19 @@ public static PipelineID randomId() {
return new PipelineID(UUID.randomUUID());
}
+ /**
+ * Generates a random PipelineID using {@link java.util.Random} instead of
+ * {@link java.security.SecureRandom}. This avoids contention on the shared
+ * {@code SecureRandom} instance and is suitable for non-sensitive,
+ * throwaway IDs such as read pipelines, where predictability of the next
+ * ID has no security impact.
+ */
+ public static PipelineID insecureRandomId() {
+ byte[] bytes = UUIDUtil.insecureRandomUUIDBytes();
+ ByteBuffer buf = ByteBuffer.wrap(bytes);
+ return new PipelineID(new UUID(buf.getLong(), buf.getLong()));
+ }
+
public static PipelineID valueOf(UUID id) {
return new PipelineID(id);
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/ContainerCommandResponseBuilders.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/ContainerCommandResponseBuilders.java
index c60f3d7449a0..831c899bde57 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/ContainerCommandResponseBuilders.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/ContainerCommandResponseBuilders.java
@@ -65,11 +65,15 @@ public final class ContainerCommandResponseBuilders {
public static Builder getContainerCommandResponse(
ContainerCommandRequestProto request, Result result, String message) {
- return ContainerCommandResponseProto.newBuilder()
+ ContainerCommandResponseProto.Builder builder = ContainerCommandResponseProto.newBuilder()
.setCmdType(request.getCmdType())
.setTraceID(request.getTraceID())
.setResult(result)
.setMessage(message);
+ if (request.hasClientId() && request.hasCallId()) {
+ builder.setClientId(request.getClientId()).setCallId(request.getCallId());
+ }
+ return builder;
}
/**
@@ -82,10 +86,14 @@ public static Builder getContainerCommandResponse(
public static Builder getSuccessResponseBuilder(
ContainerCommandRequestProto request) {
- return ContainerCommandResponseProto.newBuilder()
+ ContainerCommandResponseProto.Builder builder = ContainerCommandResponseProto.newBuilder()
.setCmdType(request.getCmdType())
.setTraceID(request.getTraceID())
.setResult(Result.SUCCESS);
+ if (request.hasClientId() && request.hasCallId()) {
+ builder.setClientId(request.getClientId()).setCallId(request.getCallId());
+ }
+ return builder;
}
/**
@@ -149,10 +157,10 @@ public static ContainerCommandResponseProto putBlockResponseSuccess(
}
public static ContainerCommandResponseProto getBlockDataResponse(
- ContainerCommandRequestProto msg, BlockData data) {
+ ContainerCommandRequestProto msg, BlockData data, boolean shortCircuitGranted) {
GetBlockResponseProto.Builder getBlock = GetBlockResponseProto.newBuilder()
- .setBlockData(data);
+ .setBlockData(data).setShortCircuitAccessGranted(shortCircuitGranted);
return getSuccessResponseBuilder(msg)
.setGetBlock(getBlock)
@@ -381,9 +389,7 @@ public static ContainerCommandResponseProto getEchoResponse(
.newBuilder()
.setPayload(UnsafeByteOperations.unsafeWrap(RandomUtils.secure().randomBytes(responsePayload)));
- return getSuccessResponseBuilder(msg)
- .setEcho(echo)
- .build();
+ return getSuccessResponseBuilder(msg).setEcho(echo).build();
}
public static ContainerCommandResponseProto getGetContainerMerkleTreeResponse(
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockLocationInfo.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockLocationInfo.java
index e84a05b35521..2322fba3a86e 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockLocationInfo.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockLocationInfo.java
@@ -17,8 +17,10 @@
package org.apache.hadoop.hdds.scm.storage;
+import jakarta.annotation.Nullable;
import java.util.Objects;
import org.apache.hadoop.hdds.client.BlockID;
+import org.apache.hadoop.hdds.client.StorageTier;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
import org.apache.hadoop.hdds.security.token.OzoneBlockTokenIdentifier;
import org.apache.hadoop.security.token.Token;
@@ -42,6 +44,8 @@ public class BlockLocationInfo {
private int partNumber;
// The block is under construction. Apply to hsynced file last block.
private boolean underConstruction;
+ @Nullable private StorageTier storageTier;
+ private boolean isFallBack;
protected BlockLocationInfo(Builder builder) {
this.blockID = builder.blockID;
@@ -51,6 +55,9 @@ protected BlockLocationInfo(Builder builder) {
this.token = builder.token;
this.partNumber = builder.partNumber;
this.createVersion = builder.createVersion;
+ this.storageTier = builder.storageTier;
+ this.isFallBack = builder.isFallBack;
+
}
public void setCreateVersion(long version) {
@@ -121,6 +128,23 @@ public boolean isUnderConstruction() {
return this.underConstruction;
}
+ @Nullable
+ public StorageTier getStorageTier() {
+ return storageTier;
+ }
+
+ public boolean getIsFallBack() {
+ return isFallBack;
+ }
+
+ public void setIsFallBack(boolean fallBack) {
+ isFallBack = fallBack;
+ }
+
+ public void setStorageTier(@Nullable StorageTier storageTier) {
+ this.storageTier = storageTier;
+ }
+
/**
* Builder of BlockLocationInfo.
*/
@@ -132,6 +156,8 @@ public static class Builder {
private Pipeline pipeline;
private int partNumber;
private long createVersion;
+ @Nullable private StorageTier storageTier;
+ private boolean isFallBack;
public Builder setBlockID(BlockID blockId) {
this.blockID = blockId;
@@ -168,6 +194,16 @@ public Builder setCreateVersion(long version) {
return this;
}
+ public Builder setStorageTier(StorageTier storageTier) {
+ this.storageTier = storageTier;
+ return this;
+ }
+
+ public Builder setIsFallBack(boolean fallBack) {
+ isFallBack = fallBack;
+ return this;
+ }
+
public BlockLocationInfo build() {
return new BlockLocationInfo(this);
}
@@ -181,7 +217,9 @@ public String toString() {
", token=" + token +
", pipeline=" + pipeline +
", createVersion=" + createVersion +
- ", partNumber=" + partNumber
+ ", partNumber=" + partNumber +
+ ", storageTier=" + storageTier +
+ ", isFallBack=" + isFallBack
+ '}';
}
@@ -213,12 +251,13 @@ public boolean equals(Object o) {
createVersion == that.createVersion &&
Objects.equals(blockID, that.blockID) &&
Objects.equals(token, that.token) &&
- Objects.equals(pipeline, that.pipeline);
+ Objects.equals(pipeline, that.pipeline) &&
+ Objects.equals(storageTier, that.storageTier);
}
@Override
public int hashCode() {
return Objects.hash(blockID, length, offset, token, createVersion,
- pipeline);
+ pipeline, storageTier);
}
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/storage/ContainerProtocolCalls.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/storage/ContainerProtocolCalls.java
index 443638062559..2f86f878f369 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/storage/ContainerProtocolCalls.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/storage/ContainerProtocolCalls.java
@@ -215,6 +215,35 @@ public static GetBlockResponseProto getBlock(XceiverClientSpi xceiverClient,
return getBlock(xceiverClient, getValidatorList(), datanodeBlockID, token, replicaIndexes);
}
+ /**
+ * Gets block metadata from a datanode.
+ *
+ *
+ * @param xceiverClient client to perform call
+ * @param blockID blockID to identify container
+ * @param token a token for this block (may be null)
+ * @param datanode datanode to query
+ * @param replicaIndexes replica indexes for EC pipelines
+ * @return container protocol get block response
+ * @throws IOException if there is an I/O error while performing the call
+ */
+ public static GetBlockResponseProto getBlockFromDatanode(
+ XceiverClientSpi xceiverClient,
+ BlockID blockID,
+ Token extends TokenIdentifier> token,
+ DatanodeDetails datanode,
+ Map replicaIndexes) throws IOException {
+ ContainerCommandRequestProto.Builder builder = ContainerCommandRequestProto
+ .newBuilder()
+ .setCmdType(Type.GetBlock)
+ .setContainerID(blockID.getContainerID());
+ if (token != null) {
+ builder.setEncodedToken(token.encodeToUrlString());
+ }
+ return getBlock(xceiverClient, getValidatorList(), builder, blockID, datanode,
+ replicaIndexes);
+ }
+
private static GetBlockResponseProto getBlock(XceiverClientSpi xceiverClient,
List validators,
ContainerCommandRequestProto.Builder builder, BlockID blockID,
@@ -238,6 +267,18 @@ private static GetBlockResponseProto getBlock(XceiverClientSpi xceiverClient,
return response.getGetBlock();
}
+ public static GetBlockResponseProto getBlock(XceiverClientSpi xceiverClient,
+ List validators, ContainerCommandRequestProto.Builder builder,
+ DatanodeDetails datanode) throws IOException {
+ String traceId = TracingUtil.exportCurrentSpan();
+ if (traceId != null) {
+ builder.setTraceID(traceId);
+ }
+ final ContainerCommandRequestProto request = builder.setDatanodeUuid(datanode.getUuidString()).build();
+ ContainerCommandResponseProto response = xceiverClient.sendCommand(request, validators);
+ return response.getGetBlock();
+ }
+
/**
* Calls the container protocol to get the length of a committed block.
*
@@ -291,8 +332,18 @@ public static XceiverClientReply putBlockAsync(XceiverClientSpi xceiverClient,
boolean eof,
String tokenString)
throws IOException, InterruptedException, ExecutionException {
+ return putBlockAsync(xceiverClient, containerBlockData, eof, tokenString, true);
+ }
+
+ public static XceiverClientReply putBlockAsync(XceiverClientSpi xceiverClient,
+ BlockData containerBlockData,
+ boolean eof,
+ String tokenString,
+ boolean containerAutoCreate)
+ throws IOException, InterruptedException, ExecutionException {
final ContainerCommandRequestProto request = getPutBlockRequest(
- xceiverClient.getPipeline(), containerBlockData, eof, tokenString);
+ xceiverClient.getPipeline(), containerBlockData, eof, tokenString,
+ containerAutoCreate);
return xceiverClient.sendCommandAsync(request);
}
@@ -329,10 +380,19 @@ public static ContainerProtos.FinalizeBlockResponseProto finalizeBlock(
public static ContainerCommandRequestProto getPutBlockRequest(
Pipeline pipeline, BlockData containerBlockData, boolean eof,
String tokenString) throws IOException {
+ return getPutBlockRequest(pipeline, containerBlockData, eof, tokenString, true);
+ }
+
+ public static ContainerCommandRequestProto getPutBlockRequest(
+ Pipeline pipeline, BlockData containerBlockData, boolean eof,
+ String tokenString, boolean containerAutoCreate) throws IOException {
PutBlockRequestProto.Builder createBlockRequest =
PutBlockRequestProto.newBuilder()
.setBlockData(containerBlockData)
.setEof(eof);
+ if (!containerAutoCreate) {
+ createBlockRequest.setContainerAutoCreate(false);
+ }
final String id = pipeline.getFirstNode().getUuidString();
ContainerCommandRequestProto.Builder builder =
ContainerCommandRequestProto.newBuilder().setCmdType(Type.PutBlock)
@@ -443,6 +503,17 @@ public static XceiverClientReply writeChunkAsync(
int replicationIndex, BlockData blockData, boolean close,
HddsProtos.StorageTypeProto storageType)
throws IOException, ExecutionException, InterruptedException {
+ return writeChunkAsync(xceiverClient, chunk, blockID, data, tokenString,
+ replicationIndex, blockData, close, storageType, true);
+ }
+
+ @SuppressWarnings("parameternumber")
+ public static XceiverClientReply writeChunkAsync(
+ XceiverClientSpi xceiverClient, ChunkInfo chunk, BlockID blockID,
+ ByteString data, String tokenString,
+ int replicationIndex, BlockData blockData, boolean close,
+ HddsProtos.StorageTypeProto storageType, boolean containerAutoCreate)
+ throws IOException, ExecutionException, InterruptedException {
DatanodeBlockID datanodeBlockID = getDatanodeBlockID(
blockID, replicationIndex, storageType);
@@ -458,6 +529,9 @@ public static XceiverClientReply writeChunkAsync(
.setEof(close);
writeChunkRequest.setBlock(createBlockRequest);
}
+ if (!containerAutoCreate) {
+ writeChunkRequest.setContainerAutoCreate(false);
+ }
String id = xceiverClient.getPipeline().getFirstNode().getUuidString();
ContainerCommandRequestProto.Builder builder =
ContainerCommandRequestProto.newBuilder()
@@ -740,6 +814,19 @@ public static GetSmallFileResponseProto readSmallFile(XceiverClientSpi client,
public static EchoResponseProto echo(XceiverClientSpi client, String encodedContainerID,
long containerID, ByteString payloadReqBytes, int payloadRespSizeKB, int sleepTimeMs, boolean readOnly)
throws IOException {
+ return echo(client, encodedContainerID, containerID, payloadReqBytes, payloadRespSizeKB,
+ sleepTimeMs, readOnly, null, 0, false);
+ }
+
+ /**
+ * Send an echo to DataNode with clientId and callId in request.
+ *
+ * @return EchoResponseProto
+ */
+ @SuppressWarnings("checkstyle:parameternumber")
+ public static EchoResponseProto echo(XceiverClientSpi client, String encodedContainerID,
+ long containerID, ByteString payloadReqBytes, int payloadRespSizeKB, int sleepTimeMs, boolean readOnly,
+ ByteString clientId, long callID, boolean noValidation) throws IOException {
ContainerProtos.EchoRequestProto getEcho =
EchoRequestProto
.newBuilder()
@@ -756,6 +843,9 @@ public static EchoResponseProto echo(XceiverClientSpi client, String encodedCont
.setContainerID(containerID)
.setDatanodeUuid(id)
.setEcho(getEcho);
+ if (clientId != null) {
+ builder.setClientId(clientId).setCallId(callID);
+ }
if (!encodedContainerID.isEmpty()) {
builder.setEncodedToken(encodedContainerID);
}
@@ -765,7 +855,7 @@ public static EchoResponseProto echo(XceiverClientSpi client, String encodedCont
}
ContainerCommandRequestProto request = builder.build();
ContainerCommandResponseProto response =
- client.sendCommand(request, getValidatorList());
+ client.sendCommand(request, noValidation ? new ArrayList<>() : getValidatorList());
return response.getEcho();
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/tracing/TracingConfig.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/tracing/TracingConfig.java
index ddbc67543796..5fe6dfcdb86e 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/tracing/TracingConfig.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/tracing/TracingConfig.java
@@ -49,6 +49,18 @@ public class TracingConfig extends ReconfigurableConfig {
)
private boolean tracingEnabled;
+ @Config(
+ key = "ozone.tracing.client.application-aware",
+ defaultValue = "true",
+ type = ConfigType.BOOLEAN,
+ reconfigurable = true,
+ tags = { ConfigTag.OZONE, ConfigTag.HDDS },
+ description = "Only effective when ozone.tracing.enabled=false. When true, Ozone will "
+ + "continue an application-supplied trace (via GlobalOpenTelemetry or a wire-propagated "
+ + "context) as child spans, but will NOT start a new root trace on its own."
+ )
+ private boolean applicationAware = true;
+
@Config(
key = "ozone.tracing.endpoint",
defaultValue = "",
@@ -83,6 +95,10 @@ public boolean isTracingEnabled() {
return tracingEnabled;
}
+ public boolean isApplicationAware() {
+ return applicationAware;
+ }
+
@PostConstruct
public void validate() {
if (tracingEndpoint.isEmpty()) {
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/tracing/TracingUtil.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/tracing/TracingUtil.java
index 9b7f6347fef2..c5624c3c2144 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/tracing/TracingUtil.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/tracing/TracingUtil.java
@@ -17,6 +17,7 @@
package org.apache.hadoop.hdds.tracing;
+import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
@@ -26,16 +27,21 @@
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
+import io.opentelemetry.context.propagation.ContextPropagators;
+import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
-import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
+import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
+import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
import org.apache.hadoop.hdds.conf.ConfigurationSource;
import org.apache.ratis.util.function.CheckedRunnable;
import org.apache.ratis.util.function.CheckedSupplier;
@@ -50,8 +56,12 @@ public final class TracingUtil {
private static final String NULL_SPAN_AS_STRING = "";
private static volatile boolean isInit = false;
+ private static volatile boolean tracingEnabled;
+ private static volatile boolean applicationAware;
private static Tracer tracer = OpenTelemetry.noop().getTracer("noop");
- private static SdkTracerProvider sdkTracerProvider;
+ private static volatile SdkTracerProvider sdkTracerProvider;
+ private static BatchSpanProcessor batchSpanProcessor;
+ public static final String GLOBAL_TRACER_NAME = "ozone";
private TracingUtil() {
}
@@ -61,14 +71,20 @@ private TracingUtil() {
*/
public static synchronized void initTracing(
String serviceName, TracingConfig tracingConfig) {
- if (!tracingConfig.isTracingEnabled() || isInit) {
+ initTracing(serviceName, tracingConfig, false);
+ }
+
+ private static synchronized void initTracing(
+ String serviceName, TracingConfig tracingConfig, boolean isReconfig) {
+ if (isInit) {
return;
}
try {
- initialize(serviceName, tracingConfig);
+ initialize(serviceName, tracingConfig, isReconfig);
isInit = true;
- LOG.info("Initialized tracing service: {}", serviceName);
+ LOG.info("Initialized tracing service: {} (enabled={}, applicationAware={})",
+ serviceName, tracingEnabled, applicationAware);
} catch (Exception e) {
LOG.error("Failed to initialize tracing", e);
}
@@ -90,19 +106,142 @@ public static synchronized void initTracing(
public static synchronized void reconfigureTracing(
String serviceName, TracingConfig tracingConfig) {
shutdownTracing();
- initTracing(serviceName, tracingConfig);
+ initTracing(serviceName, tracingConfig, true);
}
- private static void shutdownTracing() {
- if (sdkTracerProvider != null) {
- sdkTracerProvider.shutdown();
+ /**
+ * Drain the BatchSpanProcessor queue without shutting down.
+ * Call from short-lived CLIs before the JVM exits.
+ */
+ public static synchronized void flushTracing() {
+ if (batchSpanProcessor == null) {
+ return;
+ }
+ try {
+ // Best-effort: wait up to 10s for span export; remaining spans may be dropped on exit.
+ batchSpanProcessor.forceFlush().join(10, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ LOG.warn("Tracing flush: forceFlush failed", e);
+ }
+ }
+
+ /**
+ * This function initializes tracing, runs the command in a span, and exports spans before returning for CLI spans.
+ */
+ public static R execute(
+ String serviceName,
+ String spanName,
+ ConfigurationSource conf,
+ CheckedSupplier supplier) throws E {
+ initTracing(serviceName, conf);
+ try {
+ return executeInNewSpan(spanName, supplier);
+ } finally {
+ flushTracing();
+ }
+ }
+
+ static void shutdownTracing() {
+ try {
+ if (sdkTracerProvider != null) {
+ sdkTracerProvider.shutdown().join(10L, TimeUnit.SECONDS);
+ }
+ } catch (Exception e) {
+ LOG.warn("Tracing shutdown failed", e);
+ } finally {
sdkTracerProvider = null;
+ batchSpanProcessor = null;
+ tracer = OpenTelemetry.noop().getTracer("noop");
+ tracingEnabled = false;
+ applicationAware = false;
+ isInit = false;
+ }
+ }
+
+ private static void initialize(String serviceName, TracingConfig cfg, boolean isReconfig) {
+ tracingEnabled = cfg.isTracingEnabled();
+ applicationAware = cfg.isApplicationAware();
+
+ if (!tracingEnabled && !applicationAware) {
+ tracer = OpenTelemetry.noop().getTracer(GLOBAL_TRACER_NAME);
+ return;
+ }
+
+ // Server reconfiguration reprioritizes Ozone's SDK over any adopted global,
+ // and re-registers the global name and tracer.
+ if (isReconfig && tracingEnabled) {
+ initOzoneSdk(serviceName, cfg, true);
+ return;
+ }
+
+ // Global first: adopt an application-registered GlobalOpenTelemetry when present.
+ if (GlobalOpenTelemetry.isSet() && isRealGlobal(GlobalOpenTelemetry.get())) {
+ tracer = GlobalOpenTelemetry.get().getTracer(GLOBAL_TRACER_NAME);
+ LOG.info("Tracing: adopted application GlobalOpenTelemetry");
+ return;
+ }
+
+ // No app-supplied global — build Ozone's SDK and always register it as the JVM global,
+ // so any co-resident library observes the same tracer whenever tracing is valid.
+ initOzoneSdk(serviceName, cfg, true);
+ }
+
+ private static void initOzoneSdk(String serviceName, TracingConfig cfg, boolean registerGlobal) {
+ SdkTracerProvider tracerProvider = buildSdkTracerProvider(serviceName, cfg);
+ try {
+ OpenTelemetrySdk sdk;
+ if (registerGlobal) {
+ sdk = OpenTelemetrySdk.builder()
+ .setTracerProvider(tracerProvider)
+ .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
+ .build();
+ if (!GlobalOpenTelemetry.isSet() || !isRealGlobal(GlobalOpenTelemetry.get())) {
+ GlobalOpenTelemetry.set(sdk);
+ }
+ tracer = GlobalOpenTelemetry.get().getTracer(GLOBAL_TRACER_NAME);
+ } else {
+ sdk = OpenTelemetrySdk.builder()
+ .setTracerProvider(tracerProvider)
+ .build();
+ tracer = sdk.getTracer(GLOBAL_TRACER_NAME);
+ }
+ sdkTracerProvider = tracerProvider;
+ } catch (RuntimeException e) {
+ tracerProvider.shutdown();
+ batchSpanProcessor = null;
+ throw e;
+ }
+ }
+
+ /**
+ * Distinguish an application-registered GlobalOpenTelemetry from the OTel built-in noop.
+ * OpenTelemetry.noop() returns a singleton, so identity comparison is sufficient.
+ */
+ private static boolean isRealGlobal(OpenTelemetry global) {
+ return global != null && global != OpenTelemetry.noop();
+ }
+
+ /**
+ * Whether to wrap the delegate in a JDK tracing proxy.
+ * Fully enabled: always wrap. App-aware: wrap only when parent span is valid
+ */
+ private static boolean shouldCreateTracingProxy(ConfigurationSource conf) {
+ TracingConfig tc = conf.getObject(TracingConfig.class);
+ if (tc.isTracingEnabled()) {
+ return true;
+ }
+ if (!tc.isApplicationAware() || !hasUsableTracer()) {
+ return false;
}
- tracer = OpenTelemetry.noop().getTracer("noop");
- isInit = false;
+ return Span.current().getSpanContext().isValid();
}
- private static void initialize(String serviceName, TracingConfig tracingConfig) {
+ /**
+ * Build the SdkTracerProvider using the configured OTLP endpoint and sampler.
+ * Extracted so both enabled and application-aware modes share exporter/sampler setup.
+ */
+ private static SdkTracerProvider buildSdkTracerProvider(
+ String serviceName, TracingConfig tracingConfig) {
//Fetch and log the right tracing parameters based on config, environment variable and default value priority.
String otelEndPoint = tracingConfig.getTracingEndpoint();
double samplerRatio = tracingConfig.getTraceSamplerRatio();
@@ -113,11 +252,11 @@ private static void initialize(String serviceName, TracingConfig tracingConfig)
Map spanMap = parseSpanSamplingConfig(spanSamplingConfig);
Resource resource = Resource.create(Attributes.of(AttributeKey.stringKey("service.name"), serviceName));
- OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
+ SpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
.setEndpoint(otelEndPoint)
.build();
- SimpleSpanProcessor spanProcessor = SimpleSpanProcessor.builder(spanExporter).build();
+ batchSpanProcessor = BatchSpanProcessor.builder(spanExporter).build();
// Choose sampler based on span sampling config. If it is empty use trace based sampling only.
// else use custom SpanSampler.
@@ -129,35 +268,30 @@ private static void initialize(String serviceName, TracingConfig tracingConfig)
sampler = new SpanSampler(rootSampler, spanMap);
}
- SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
- .addSpanProcessor(spanProcessor)
+ return SdkTracerProvider.builder()
+ .addSpanProcessor(batchSpanProcessor)
.setResource(resource)
.setSampler(sampler)
.build();
+ }
- try {
- OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
- .setTracerProvider(tracerProvider)
- .build();
- tracer = openTelemetry.getTracer(serviceName);
- sdkTracerProvider = tracerProvider;
- } catch (RuntimeException e) {
- tracerProvider.shutdown();
- throw e;
- }
+ private static boolean canStartSpanWithoutParent() {
+ return tracingEnabled;
}
/**
* Export the active tracing span as a string.
+ * When tracing is disabled, not initialized, or no valid span is in scope,
+ * {@link Span#current()} returns an invalid span; there is nothing to encode and this
+ * method returns an empty string. Callers must accept that as "no context to propagate".
*
- * @return encoded tracing context.
+ * @return encoded W3C trace context, or empty string if there is no valid active span.
*/
public static String exportCurrentSpan() {
Span currentSpan = Span.current();
if (!currentSpan.getSpanContext().isValid()) {
return NULL_SPAN_AS_STRING;
}
-
StringBuilder builder = new StringBuilder();
W3CTraceContextPropagator propagator = W3CTraceContextPropagator.getInstance();
propagator.inject(Context.current(), builder,
@@ -167,13 +301,23 @@ public static String exportCurrentSpan() {
/**
* Create a new scope and use the imported span as the parent.
+ * Short-circuits to an invalid span when there is no usable tracer:
+ * - tracing was never initialized (tracer is still the noop), or
+ * - application-aware mode is on but no app-supplied SDK was adopted (sdkTracerProvider == null
+ * and the current tracer is the noop).
*
* @param name name of the newly created scope
* @param encodedParent Encoded parent span (could be null or empty)
* @return Tracing scope.
*/
public static Span importAndCreateSpan(String name, String encodedParent) {
+ if (!hasUsableTracer()) {
+ return Span.getInvalid();
+ }
if (encodedParent == null || encodedParent.isEmpty()) {
+ if (!canStartSpanWithoutParent()) {
+ return Span.getInvalid();
+ }
return tracer.spanBuilder(name).setNoParent().startSpan();
}
@@ -184,6 +328,17 @@ public static Span importAndCreateSpan(String name, String encodedParent) {
.startSpan();
}
+ /**
+ * True when the current tracer can actually build spans — an Ozone-owned SDK is configured,
+ * or an adopted GlobalOpenTelemetry provides a non-noop tracer.
+ */
+ private static boolean hasUsableTracer() {
+ if (sdkTracerProvider != null) {
+ return true;
+ }
+ return GlobalOpenTelemetry.isSet() && isRealGlobal(GlobalOpenTelemetry.get());
+ }
+
/**
* Creates a proxy of the implementation and trace all the method calls.
*
@@ -197,7 +352,7 @@ public static Span importAndCreateSpan(String name, String encodedParent) {
*/
public static T createProxy(
T delegate, Class itf, ConfigurationSource conf) {
- if (!isTracingEnabled(conf)) {
+ if (!shouldCreateTracingProxy(conf)) {
return delegate;
}
Class> aClass = delegate.getClass();
@@ -210,6 +365,20 @@ public static boolean isTracingEnabled(ConfigurationSource conf) {
return conf.getObject(TracingConfig.class).isTracingEnabled();
}
+ /**
+ * Returns true when tracing may actually produce spans:
+ * - fully enabled (ozone.tracing.enabled=true), or
+ * - application-aware AND an SDK is configured (either Ozone-owned or an adopted global);
+ * without an SDK, application-aware is a passthrough that would emit noop spans anyway.
+ */
+ public static boolean isTracingActive(ConfigurationSource conf) {
+ TracingConfig tc = conf.getObject(TracingConfig.class);
+ if (tc.isTracingEnabled()) {
+ return true;
+ }
+ return tc.isApplicationAware() && hasUsableTracer();
+ }
+
/**
* Function to parse span sampling config. The input is in the form :.
* The sample rate must be a number between 0 and 1. Any value other than that will LOG an error.
@@ -375,7 +544,8 @@ private void parse(String carrier) {
/**
* Creates a new span, using the current context as a parent if valid;
- * otherwise, creates a root span.
+ * Otherwise starts a root span only when {@code ozone.tracing.enabled=true};
+ * if not, returns an invalid span so application-aware mode never starts a new trace.
*/
private static Span buildSpan(String spanName) {
Context currentContext = Context.current();
@@ -383,8 +553,55 @@ private static Span buildSpan(String spanName) {
if (parentSpan.getSpanContext().isValid()) {
return tracer.spanBuilder(spanName).setParent(currentContext).startSpan();
- } else {
- return tracer.spanBuilder(spanName).setNoParent().startSpan();
}
+ if (!canStartSpanWithoutParent()) {
+ return Span.getInvalid();
+ }
+ return tracer.spanBuilder(spanName).setNoParent().startSpan();
+ }
+
+ /**
+ * A TextMapGetter implementation to extract tracing info from getHeader.
+ */
+ public static class HttpHeaderGetter implements TextMapGetter> {
+
+ @Override
+ public Iterable keys(Function carrier) {
+ // Not used during the extract call, so returning an empty list.
+ return Collections.emptyList();
+ }
+
+ @Override
+ public String get(Function carrier, String key) {
+ return carrier == null ? null : carrier.apply(key);
+ }
+ }
+
+ public static TraceCloseable createActivatedSpanFromW3cHttpHeaders(
+ String spanName, Function getHeader, ConfigurationSource conf) {
+ if (conf == null || !isTracingActive(conf)) {
+ return () -> { };
+ }
+
+ Context remote = W3CTraceContextPropagator.getInstance()
+ .extract(Context.current(), getHeader, new HttpHeaderGetter());
+
+ if (!Span.fromContext(remote).getSpanContext().isValid()) {
+ if (!canStartSpanWithoutParent()) {
+ return () -> { };
+ }
+ return createActivatedSpan(spanName);
+ }
+
+ Span span = tracer.spanBuilder(spanName)
+ .setParent(remote)
+ .startSpan();
+
+ Scope scope = span.makeCurrent();
+
+ return () -> {
+ scope.close();
+ span.end();
+ };
}
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/CompositeKey.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/CompositeKey.java
index 2f54ae7c7019..ab8b244f1bfe 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/CompositeKey.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/CompositeKey.java
@@ -18,53 +18,94 @@
package org.apache.hadoop.hdds.utils;
import java.util.Arrays;
+import java.util.Objects;
+import org.apache.ratis.util.Preconditions;
/**
* This is a utility to combine multiple objects as a key that can be used in
* hash map access. The advantage of this is that it is cheap in comparison
* to other methods like string concatenation.
- *
- * For example, if a composition of volume, bucket and key is needed to
- * access a hash map, the natural method is:
- *
- * This is costly because it creates (and stores) a new buffer.
- *
- * In comparison, the following achieve the same logic without creating any new
- * buffer.
- *
- *
*/
-public final class CompositeKey {
- private final int hashCode;
- private final Object[] components;
-
- CompositeKey(Object[] components) {
- this.components = components;
- this.hashCode = Arrays.hashCode(components);
+public abstract class CompositeKey {
+ /** The same as {@link Arrays#hashCode(Object[])} for one loop step. */
+ static int hash(int result, Object next) {
+ return 31 * result + next.hashCode();
}
- @Override
- public int hashCode() {
- return hashCode;
+ private static final class TwoComponents extends CompositeKey {
+ private final int hashCode;
+ private final Object first;
+ private final Object second;
+
+ private TwoComponents(Object first, Object second) {
+ this.first = Objects.requireNonNull(first, "first == null");
+ this.second = Objects.requireNonNull(second, "second == null");
+ this.hashCode = hash(hash(1, first), second);
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (!(obj instanceof TwoComponents)) {
+ return false;
+ }
+ final TwoComponents that = (TwoComponents) obj;
+ return this.hashCode == that.hashCode
+ && this.first.equals(that.first)
+ && this.second.equals(that.second);
+ }
}
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof CompositeKey)) {
- return false;
+ private static final class MultiComponents extends CompositeKey {
+ private final int hashCode;
+ private final Object[] components;
+
+ MultiComponents(Object[] components) {
+ Preconditions.assertTrue(components.length > 2, () -> "components.length " + components.length + " <= 2");
+ for (int i = 0; i < components.length; i++) {
+ final int j = i;
+ Objects.requireNonNull(components[j], () -> "components[" + j + "] == null");
+ }
+
+ this.hashCode = Arrays.hashCode(components);
+ this.components = components;
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (!(obj instanceof MultiComponents)) {
+ return false;
+ }
+ final MultiComponents that = (MultiComponents) obj;
+ return this.hashCode == that.hashCode
+ && Arrays.equals(this.components, that.components);
}
- CompositeKey other = (CompositeKey) obj;
- return Arrays.equals(components, other.components);
+ }
+
+ public static CompositeKey combineTwoKeys(Object first, Object second) {
+ return new TwoComponents(first, second);
+ }
+
+ public static CompositeKey combineMultiKeys(Object[] components) {
+ return new MultiComponents(components);
}
public static Object combineKeys(Object[] components) {
- return components.length == 1 ?
- components[0] : new CompositeKey(components);
+ return components.length == 1 ? components[0]
+ : components.length == 2 ? CompositeKey.combineTwoKeys(components[0], components[1])
+ : CompositeKey.combineMultiKeys(components);
}
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/ConnectionFailureUtils.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/ConnectionFailureUtils.java
new file mode 100644
index 000000000000..d7f45b1cd7e3
--- /dev/null
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/ConnectionFailureUtils.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hadoop.hdds.utils;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.NoRouteToHostException;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.util.concurrent.ExecutionException;
+import org.apache.hadoop.hdds.scm.container.common.helpers.StorageContainerException;
+import org.apache.ratis.protocol.exceptions.TimeoutIOException;
+
+/**
+ * Shared classifier for exceptions where the cached peer IP is no longer
+ * reachable and DNS re-resolution is the only plausible recovery path.
+ *
+ * Used by both {@code SCMFailoverProxyProviderBase} and
+ * {@code OMFailoverProxyProviderBase} to gate the DNS-refresh-on-failure
+ * code path so that application-level errors (NotLeader, AccessControl,
+ * OMException, RetryAction) do not trigger spurious DNS lookups.
+ *
+ * The classifier must match the failure shapes seen in production
+ * Kubernetes deployments where the peer pod has been rescheduled to a
+ * new IP under a stable hostname:
+ *
+ *
{@link ConnectException} -- the TCP SYN was refused. Seen on
+ * OpenStack / fast-RST environments.
+ *
{@link SocketTimeoutException} (and its IPC subclass
+ * {@code ConnectTimeoutException}) -- the SYN was dropped silently.
+ * This is the dominant failure shape on AWS EC2 / EKS where the
+ * network silently drops packets to a defunct pod IP. The PR that
+ * introduced this helper (HDDS-15514) is sold on this case; it
+ * must be in the filter.
+ *
{@link NoRouteToHostException} -- routing table no longer
+ * reaches the cached IP.
+ *
{@link UnknownHostException} -- the hostname itself failed to
+ * resolve at the time the IPC layer reconstructed the address.
+ *
{@link EOFException} -- a load balancer or iptables RST closed
+ * the half-open connection cleanly. Common in Kubernetes when an
+ * IP is reassigned to an unrelated pod that rejects the RPC
+ * handshake.
+ *
{@link SocketException} (e.g. "Connection reset") -- the peer
+ * sent RST mid-stream.
+ *
+ * The walk is bounded to {@value #MAX_CAUSE_DEPTH} levels to defend
+ * against cause chains that have been constructed (in violation of
+ * {@code Throwable.initCause}'s contract) into a cycle of length > 1.
+ */
+public final class ConnectionFailureUtils {
+
+ /**
+ * Maximum depth of the {@code Throwable.getCause()} chain we walk
+ * before giving up. Matches Hadoop's own walkers in
+ * {@code RemoteException} handling.
+ */
+ static final int MAX_CAUSE_DEPTH = 16;
+
+ private ConnectionFailureUtils() {
+ }
+
+ /**
+ * Returns true when any link in {@code t}'s cause chain (up to
+ * {@link #MAX_CAUSE_DEPTH} levels) is one of the connection-class
+ * exceptions documented on this class.
+ *
+ * @param t the throwable to classify. {@code null} returns false.
+ */
+ public static boolean isConnectionFailure(Throwable t) {
+ Throwable cause = t;
+ for (int depth = 0; cause != null && depth < MAX_CAUSE_DEPTH; depth++) {
+ // ConnectException and NoRouteToHostException both extend
+ // SocketException, so the SocketException check below already matches
+ // them. They remain listed in this class's Javadoc as connection-
+ // failure shapes for documentation.
+ if (cause instanceof SocketTimeoutException
+ || cause instanceof UnknownHostException
+ || cause instanceof EOFException
+ || cause instanceof SocketException) {
+ return true;
+ }
+ Throwable next = cause.getCause();
+ if (next == cause) {
+ break;
+ }
+ cause = next;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the first {@link StorageContainerException} or
+ * {@link TimeoutIOException} in {@code ex}'s cause chain (through
+ * {@link ExecutionException} and nested {@link IOException} wrappers).
+ */
+ public static IOException unwrapCause(IOException ex) {
+ Throwable t = ex;
+ while (t != null) {
+ if (t instanceof TimeoutIOException || t instanceof StorageContainerException) {
+ return (IOException) t;
+ }
+ if (t instanceof ExecutionException && t.getCause() != null) {
+ t = t.getCause();
+ continue;
+ }
+ if (t.getCause() instanceof IOException) {
+ t = t.getCause();
+ continue;
+ }
+ break;
+ }
+ return ex;
+ }
+}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/SimpleStriped.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/SimpleStriped.java
index ec83553473e6..390b11e7a186 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/SimpleStriped.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/SimpleStriped.java
@@ -18,7 +18,6 @@
package org.apache.hadoop.hdds.utils;
import com.google.common.util.concurrent.Striped;
-import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
@@ -45,8 +44,7 @@ private SimpleStriped() {
* @param fair whether to use a fair ordering policy
* @return a new {@code Striped}
*/
- public static Striped readWriteLock(int stripes,
- boolean fair) {
+ public static Striped readWriteLock(int stripes, boolean fair) {
return Striped.custom(stripes, () -> new ReentrantReadWriteLock(fair));
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/SlidingWindow.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/SlidingWindow.java
index 316c88aba57b..0054b24f6c52 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/SlidingWindow.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/SlidingWindow.java
@@ -156,7 +156,7 @@ public long getExpiryDurationMillis() {
/**
* A custom monotonic clock implementation to allow overriding the current time for testing purposes.
* Implementation of Clock that uses System.nanoTime() for real usage.
- * The class {@code org.apache.ozone.test.TestClock} provides a mock clock which can be used
+ * The class {@code org.apache.ozone.test.MockClock} provides a mock clock which can be used
* to manipulate the current time in tests.
*/
public static final class MonotonicClock extends Clock {
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/db/StringCodec.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/db/StringCodec.java
index b1a6120e72de..247070f49758 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/db/StringCodec.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/db/StringCodec.java
@@ -24,11 +24,16 @@
* using {@link StandardCharsets#UTF_8},
* a variable-length character encoding.
*/
-public final class StringCodec extends StringCodecBase {
- private static final StringCodec CODEC = new StringCodec();
+public final class StringCodec extends StringCodecBase.WithFallback {
+ private static final StringCodec CODEC_WITH_FALLBACK = new StringCodec();
+ private static final Codec CODEC_NO_FALLBACK = new StringCodecBase(StandardCharsets.UTF_8) { };
public static StringCodec get() {
- return CODEC;
+ return CODEC_WITH_FALLBACK;
+ }
+
+ public static Codec getCodecNoFallback() {
+ return CODEC_NO_FALLBACK;
}
private StringCodec() {
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/db/StringCodecBase.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/db/StringCodecBase.java
index 62196a1bfffe..f64f19318311 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/db/StringCodecBase.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/db/StringCodecBase.java
@@ -78,7 +78,7 @@ CharsetDecoder newDecoder() {
*
* For a fixed-length {@link Codec},
* each character is encoded to the same number of bytes and
- * {@link #getSerializedSizeUpperBound(String)} equals to the serialized size.
+ * {@code getSerializedSizeUpperBound(String)} equals to the serialized size.
*/
public boolean isFixedLength() {
return fixedLength;
@@ -112,20 +112,29 @@ private PutToByteBuffer encode(
};
}
- String decode(ByteBuffer buffer) {
+ String decodeNoFallback(ByteBuffer buffer) throws CodecException {
+ try {
+ return newDecoder().decode(buffer.asReadOnlyBuffer()).toString();
+ } catch (Exception e) {
+ throw new CodecException("Failed to decode " + buffer, e);
+ }
+ }
+
+ String decodeWithFallback(ByteBuffer buffer) {
Runnable error = null;
try {
return newDecoder().decode(buffer.asReadOnlyBuffer()).toString();
} catch (Exception e) {
- error = () -> LOG.warn("Failed to decode buffer with " + charset
- + ", buffer = (hex) " + StringUtils.bytes2Hex(buffer), e);
+ error = () -> LOG.warn("Failed to decode buffer with {}, buffer = (hex) {}",
+ charset, StringUtils.bytes2Hex(buffer, 20), e);
// For compatibility, try decoding using StringUtils.
final String decoded = StringUtils.bytes2String(buffer, charset);
// Decoded successfully, update error message.
- error = () -> LOG.warn("Decode (hex) " + StringUtils.bytes2Hex(buffer, 20)
- + "\n Attempt failed : " + charset + " (see exception below)"
- + "\n Retry succeeded: decoded to " + decoded, e);
+ error = () -> LOG.warn("Decode (hex) {}" +
+ "\n Attempt failed : {} (see exception below)" +
+ "\n Retry succeeded: decoded to {}",
+ StringUtils.bytes2Hex(buffer, 20), charset, decoded, e);
return decoded;
} finally {
if (error != null) {
@@ -177,8 +186,8 @@ public CodecBuffer toCodecBuffer(@Nonnull String object, CodecBuffer.Allocator a
}
@Override
- public String fromCodecBuffer(@Nonnull CodecBuffer buffer) {
- return decode(buffer.asReadOnlyByteBuffer());
+ public String fromCodecBuffer(@Nonnull CodecBuffer buffer) throws CodecException {
+ return decodeNoFallback(buffer.asReadOnlyByteBuffer());
}
@Override
@@ -187,12 +196,28 @@ public byte[] toPersistedFormat(String object) throws CodecException {
}
@Override
- public String fromPersistedFormat(byte[] bytes) {
- return decode(ByteBuffer.wrap(bytes));
+ public String fromPersistedFormat(byte[] bytes) throws CodecException {
+ return decodeNoFallback(ByteBuffer.wrap(bytes));
}
@Override
public String copyObject(String object) {
return object;
}
+
+ static class WithFallback extends StringCodecBase {
+ WithFallback(Charset charset) {
+ super(charset);
+ }
+
+ @Override
+ public String fromCodecBuffer(@Nonnull CodecBuffer buffer) {
+ return decodeWithFallback(buffer.asReadOnlyByteBuffer());
+ }
+
+ @Override
+ public String fromPersistedFormat(byte[] bytes) {
+ return decodeWithFallback(ByteBuffer.wrap(bytes));
+ }
+ }
}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/io_/retry/CallReturn.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/io_/retry/CallReturn.java
new file mode 100644
index 000000000000..c765d26b3758
--- /dev/null
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/io_/retry/CallReturn.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.hadoop.io_.retry;
+
+import com.google.common.base.Preconditions;
+import org.apache.hadoop.io.retry.RetryPolicy;
+
+/** The call return from a method invocation. */
+class CallReturn {
+ /** The return state. */
+ enum State {
+ /** Call is returned successfully. */
+ RETURNED,
+ /** Call throws an exception. */
+ EXCEPTION,
+ /** Call should be retried according to the {@link RetryPolicy}. */
+ RETRY,
+ }
+
+ static final CallReturn RETRY = new CallReturn(State.RETRY);
+
+ private final Object returnValue;
+ private final Throwable thrown;
+ private final State state;
+
+ CallReturn(Object r) {
+ this(r, null, State.RETURNED);
+ }
+ CallReturn(Throwable t) {
+ this(null, t, State.EXCEPTION);
+ Preconditions.checkNotNull(t);
+ }
+ private CallReturn(State s) {
+ this(null, null, s);
+ }
+ private CallReturn(Object r, Throwable t, State s) {
+ Preconditions.checkArgument(r == null || t == null);
+ returnValue = r;
+ thrown = t;
+ state = s;
+ }
+
+ State getState() {
+ return state;
+ }
+
+ Object getReturnValue() throws Throwable {
+ if (state == State.EXCEPTION) {
+ throw thrown;
+ }
+ Preconditions.checkState(state == State.RETURNED, "state == %s", state);
+ return returnValue;
+ }
+}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/io_/retry/RetryInvocationHandler.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/io_/retry/RetryInvocationHandler.java
new file mode 100644
index 000000000000..604b82ea2a61
--- /dev/null
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/io_/retry/RetryInvocationHandler.java
@@ -0,0 +1,442 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.hadoop.io_.retry;
+
+import org.apache.hadoop.io.retry.AtMostOnce;
+import org.apache.hadoop.io.retry.FailoverProxyProvider;
+import org.apache.hadoop.io.retry.FailoverProxyProvider.ProxyInfo;
+import org.apache.hadoop.io.retry.Idempotent;
+import org.apache.hadoop.io.retry.MultiException;
+import org.apache.hadoop.io.retry.RetryPolicy;
+import org.apache.hadoop.io.retry.RetryPolicy.RetryAction;
+import org.apache.hadoop.ipc_.*;
+import org.apache.hadoop.ipc_.Client.ConnectionId;
+import org.apache.hadoop.util.Time;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+
+/**
+ * A {@link RpcInvocationHandler} which supports client side retry .
+ */
+public class RetryInvocationHandler implements RpcInvocationHandler {
+ public static final Logger LOG = LoggerFactory.getLogger(
+ RetryInvocationHandler.class);
+
+ public static final ThreadLocal SET_CALL_ID_FOR_TEST =
+ ThreadLocal.withInitial(() -> true);
+
+ static class Call {
+ private final Method method;
+ private final Object[] args;
+ private final boolean isRpc;
+ private final int callId;
+ private final Counters counters = new Counters();
+
+ private final RetryPolicy retryPolicy;
+ private final RetryInvocationHandler> retryInvocationHandler;
+
+ private RetryInfo retryInfo;
+
+ Call(Method method, Object[] args, boolean isRpc, int callId,
+ RetryInvocationHandler> retryInvocationHandler) {
+ this.method = method;
+ this.args = args;
+ this.isRpc = isRpc;
+ this.callId = callId;
+
+ this.retryPolicy = retryInvocationHandler.getRetryPolicy(method);
+ this.retryInvocationHandler = retryInvocationHandler;
+ }
+
+ synchronized Long getWaitTime(final long now) {
+ return retryInfo == null? null: retryInfo.retryTime - now;
+ }
+
+ /** Invoke the call once without retrying. */
+ synchronized CallReturn invokeOnce() {
+ try {
+ if (retryInfo != null) {
+ return processWaitTimeAndRetryInfo();
+ }
+
+ // The number of times this invocation handler has ever been failed over
+ // before this method invocation attempt. Used to prevent concurrent
+ // failed method invocations from triggering multiple failover attempts.
+ final long failoverCount = retryInvocationHandler.getFailoverCount();
+ try {
+ return invoke();
+ } catch (Exception e) {
+ if (LOG.isTraceEnabled()) {
+ LOG.trace(toString(), e);
+ }
+ if (Thread.currentThread().isInterrupted()) {
+ // If interrupted, do not retry.
+ throw e;
+ }
+
+ retryInfo = retryInvocationHandler.handleException(
+ method, callId, retryPolicy, counters, failoverCount, e);
+ return processWaitTimeAndRetryInfo();
+ }
+ } catch(Throwable t) {
+ return new CallReturn(t);
+ }
+ }
+
+ /**
+ * It first processes the wait time, if there is any,
+ * and then invokes {@link #processRetryInfo()}.
+ * If the wait time is positive, it sleeps.
+ *
+ * @return {@link CallReturn#RETRY}
+ */
+ CallReturn processWaitTimeAndRetryInfo() throws InterruptedIOException {
+ final Long waitTime = getWaitTime(Time.monotonicNow());
+ LOG.trace("#{} processRetryInfo: retryInfo={}, waitTime={}",
+ callId, retryInfo, waitTime);
+ if (waitTime != null && waitTime > 0) {
+ try {
+ Thread.sleep(waitTime);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Interrupted while waiting to retry", e);
+ }
+ InterruptedIOException intIOE = new InterruptedIOException(
+ "Retry interrupted");
+ intIOE.initCause(e);
+ throw intIOE;
+ }
+ }
+ processRetryInfo();
+ return CallReturn.RETRY;
+ }
+
+ synchronized void processRetryInfo() {
+ counters.retries++;
+ if (retryInfo.isFailover()) {
+ retryInvocationHandler.proxyDescriptor.failover(
+ retryInfo.expectedFailoverCount, method, callId);
+ counters.failovers++;
+ }
+ retryInfo = null;
+ }
+
+ CallReturn invoke() throws Throwable {
+ return new CallReturn(invokeMethod());
+ }
+
+ Object invokeMethod() throws Throwable {
+ if (isRpc && SET_CALL_ID_FOR_TEST.get()) {
+ Client.setCallIdAndRetryCount(callId, counters.retries);
+ }
+ return retryInvocationHandler.invokeMethod(method, args);
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "#" + callId + ": "
+ + method.getDeclaringClass().getSimpleName() + "." + method.getName()
+ + "(" + (args == null || args.length == 0? "": Arrays.toString(args))
+ + ")";
+ }
+ }
+
+ static class Counters {
+ /** Counter for retries. */
+ private int retries;
+ /** Counter for method invocation has been failed over. */
+ private int failovers;
+ }
+
+ private static class ProxyDescriptor {
+ private final FailoverProxyProvider fpp;
+ /** Count the associated proxy provider has ever been failed over. */
+ private long failoverCount = 0;
+
+ private ProxyInfo proxyInfo;
+
+ ProxyDescriptor(FailoverProxyProvider fpp) {
+ this.fpp = fpp;
+ this.proxyInfo = fpp.getProxy();
+ }
+
+ synchronized ProxyInfo getProxyInfo() {
+ return proxyInfo;
+ }
+
+ synchronized T getProxy() {
+ return proxyInfo.proxy;
+ }
+
+ synchronized long getFailoverCount() {
+ return failoverCount;
+ }
+
+ synchronized void failover(long expectedFailoverCount, Method method,
+ int callId) {
+ // Make sure that concurrent failed invocations only cause a single
+ // actual failover.
+ if (failoverCount == expectedFailoverCount) {
+ fpp.performFailover(proxyInfo.proxy);
+ failoverCount++;
+ } else {
+ LOG.warn("A failover has occurred since the start of call #" + callId
+ + " " + proxyInfo.getString(method.getName()));
+ }
+ proxyInfo = fpp.getProxy();
+ }
+
+ boolean idempotentOrAtMostOnce(Method method) throws NoSuchMethodException {
+ final Method m = fpp.getInterface()
+ .getMethod(method.getName(), method.getParameterTypes());
+ return m.isAnnotationPresent(Idempotent.class)
+ || m.isAnnotationPresent(AtMostOnce.class);
+ }
+
+ void close() throws IOException {
+ fpp.close();
+ }
+ }
+
+ private static class RetryInfo {
+ private final long retryTime;
+ private final long delay;
+ private final RetryAction action;
+ private final long expectedFailoverCount;
+ private final Exception failException;
+
+ RetryInfo(long delay, RetryAction action, long expectedFailoverCount,
+ Exception failException) {
+ this.delay = delay;
+ this.retryTime = Time.monotonicNow() + delay;
+ this.action = action;
+ this.expectedFailoverCount = expectedFailoverCount;
+ this.failException = failException;
+ }
+
+ boolean isFailover() {
+ return action != null
+ && action.action == RetryAction.RetryDecision.FAILOVER_AND_RETRY;
+ }
+
+ boolean isFail() {
+ return action != null
+ && action.action == RetryAction.RetryDecision.FAIL;
+ }
+
+ Exception getFailException() {
+ return failException;
+ }
+
+ static RetryInfo newRetryInfo(RetryPolicy policy, Exception e,
+ Counters counters, boolean idempotentOrAtMostOnce,
+ long expectedFailoverCount) throws Exception {
+ RetryAction max = null;
+ long maxRetryDelay = 0;
+ Exception ex = null;
+
+ final Iterable exceptions = e instanceof MultiException ?
+ ((MultiException) e).getExceptions().values()
+ : Collections.singletonList(e);
+ for (Exception exception : exceptions) {
+ final RetryAction a = policy.shouldRetry(exception,
+ counters.retries, counters.failovers, idempotentOrAtMostOnce);
+ if (a.action != RetryAction.RetryDecision.FAIL) {
+ // must be a retry or failover
+ if (a.delayMillis > maxRetryDelay) {
+ maxRetryDelay = a.delayMillis;
+ }
+ }
+
+ if (max == null || max.action.compareTo(a.action) < 0) {
+ max = a;
+ if (a.action == RetryAction.RetryDecision.FAIL) {
+ ex = exception;
+ }
+ }
+ }
+
+ return new RetryInfo(maxRetryDelay, max, expectedFailoverCount, ex);
+ }
+
+ @Override
+ public String toString() {
+ return "RetryInfo{" +
+ "retryTime=" + retryTime +
+ ", delay=" + delay +
+ ", action=" + action +
+ ", expectedFailoverCount=" + expectedFailoverCount +
+ ", failException=" + failException +
+ '}';
+ }
+ }
+
+ private final ProxyDescriptor proxyDescriptor;
+
+ private volatile boolean hasSuccessfulCall = false;
+
+ private HashSet failedAtLeastOnce = new HashSet<>();
+
+ private final RetryPolicy defaultPolicy;
+ private final Map methodNameToPolicyMap;
+
+ protected RetryInvocationHandler(FailoverProxyProvider proxyProvider,
+ RetryPolicy retryPolicy) {
+ this(proxyProvider, retryPolicy, Collections.emptyMap());
+ }
+
+ protected RetryInvocationHandler(FailoverProxyProvider proxyProvider,
+ RetryPolicy defaultPolicy,
+ Map methodNameToPolicyMap) {
+ this.proxyDescriptor = new ProxyDescriptor<>(proxyProvider);
+ this.defaultPolicy = defaultPolicy;
+ this.methodNameToPolicyMap = methodNameToPolicyMap;
+ }
+
+ private RetryPolicy getRetryPolicy(Method method) {
+ final RetryPolicy policy = methodNameToPolicyMap.get(method.getName());
+ return policy != null? policy: defaultPolicy;
+ }
+
+ private long getFailoverCount() {
+ return proxyDescriptor.getFailoverCount();
+ }
+
+ private Call newCall(Method method, Object[] args, boolean isRpc,
+ int callId) {
+ return new Call(method, args, isRpc, callId, this);
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+ final boolean isRpc = isRpcInvocation(proxyDescriptor.getProxy());
+ final int callId = isRpc? Client.nextCallId(): RpcConstants.INVALID_CALL_ID;
+
+ final Call call = newCall(method, args, isRpc, callId);
+ while (true) {
+ final CallReturn c = call.invokeOnce();
+ final CallReturn.State state = c.getState();
+ if (state != CallReturn.State.RETRY) {
+ return c.getReturnValue();
+ }
+ }
+ }
+
+ private RetryInfo handleException(final Method method, final int callId,
+ final RetryPolicy policy, final Counters counters,
+ final long expectFailoverCount, final Exception e) throws Exception {
+ final RetryInfo retryInfo = RetryInfo.newRetryInfo(policy, e,
+ counters, proxyDescriptor.idempotentOrAtMostOnce(method),
+ expectFailoverCount);
+ if (retryInfo.isFail()) {
+ // fail.
+ if (retryInfo.action.reason != null) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Exception while invoking call #" + callId + " "
+ + proxyDescriptor.getProxyInfo().getString(method.getName())
+ + ". Not retrying because " + retryInfo.action.reason, e);
+ }
+ }
+ throw retryInfo.getFailException();
+ }
+
+ log(method, retryInfo.isFailover(), counters.failovers, counters.retries, retryInfo.delay, e);
+ return retryInfo;
+ }
+
+ private void log(final Method method, final boolean isFailover, final int failovers,
+ final int retries, final long delay, final Exception ex) {
+ boolean info = true;
+ // If this is the first failover to this proxy, skip logging at INFO level
+ if (!failedAtLeastOnce.contains(proxyDescriptor.getProxyInfo().toString()))
+ {
+ failedAtLeastOnce.add(proxyDescriptor.getProxyInfo().toString());
+
+ // If successful calls were made to this proxy, log info even for first
+ // failover
+ info = hasSuccessfulCall;
+ if (!info && !LOG.isDebugEnabled()) {
+ return;
+ }
+ }
+
+ final StringBuilder b = new StringBuilder()
+ .append(ex)
+ .append(", while invoking ")
+ .append(proxyDescriptor.getProxyInfo().getString(method.getName()));
+ if (failovers > 0) {
+ b.append(" after ").append(failovers).append(" failover attempts");
+ }
+ b.append(isFailover? ". Trying to failover ": ". Retrying ");
+ b.append(delay > 0? "after sleeping for " + delay + "ms.": "immediately.");
+ b.append(" Current retry count: ").append(retries).append(".");
+
+ if (info) {
+ LOG.info(b.toString());
+ } else {
+ LOG.debug(b.toString(), ex);
+ }
+ }
+
+ protected Object invokeMethod(Method method, Object[] args) throws Throwable {
+ try {
+ if (!method.isAccessible()) {
+ method.setAccessible(true);
+ }
+ final Object r = method.invoke(proxyDescriptor.getProxy(), args);
+ hasSuccessfulCall = true;
+ return r;
+ } catch (InvocationTargetException e) {
+ throw e.getCause();
+ }
+ }
+
+ static boolean isRpcInvocation(Object proxy) {
+ if (proxy instanceof ProtocolTranslator) {
+ proxy = ((ProtocolTranslator) proxy).getUnderlyingProxyObject();
+ }
+ if (!Proxy.isProxyClass(proxy.getClass())) {
+ return false;
+ }
+ final InvocationHandler ih = Proxy.getInvocationHandler(proxy);
+ return ih instanceof RpcInvocationHandler;
+ }
+
+ @Override
+ public void close() throws IOException {
+ proxyDescriptor.close();
+ }
+
+ @Override //RpcInvocationHandler
+ public ConnectionId getConnectionId() {
+ return RPC.getConnectionIdForProxy(proxyDescriptor.getProxy());
+ }
+}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/io_/retry/RetryPolicies.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/io_/retry/RetryPolicies.java
new file mode 100644
index 000000000000..a31091b6d375
--- /dev/null
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/io_/retry/RetryPolicies.java
@@ -0,0 +1,421 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.hadoop.io_.retry;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.NoRouteToHostException;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.hadoop.io.retry.RetryPolicy;
+import org.apache.hadoop.ipc_.RemoteException;
+import org.apache.hadoop.ipc_.RetriableException;
+import org.apache.hadoop.net.ConnectTimeoutException;
+import org.apache.hadoop.security.AccessControlException;
+import org.apache.hadoop.security.token.SecretManager.InvalidToken;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ * A collection of useful implementations of {@link RetryPolicy}.
+ *
+ */
+public class RetryPolicies {
+
+ public static final Logger LOG = LoggerFactory.getLogger(RetryPolicies.class);
+
+ /**
+ *
+ * Try once, and fail by re-throwing the exception.
+ * This corresponds to having no retry mechanism in place.
+ *
+ */
+ public static final RetryPolicy TRY_ONCE_THEN_FAIL = new TryOnceThenFail();
+
+ /**
+ *
+ * Keep trying forever.
+ *
+ */
+ public static final RetryPolicy RETRY_FOREVER = new RetryForever();
+
+ /**
+ *
+ * Keep trying forever with a fixed time between attempts.
+ *
+ * Keep trying a limited number of times, waiting a growing amount of time between attempts,
+ * and then fail by re-throwing the exception.
+ * The time between attempts is sleepTime mutliplied by a random
+ * number in the range of [0, 2 to the number of retries)
+ *
+ *
+ *
+ * @param timeUnit timeUnit.
+ * @param maxRetries maxRetries.
+ * @param sleepTime sleepTime.
+ * @return RetryPolicy.
+ */
+ public static final RetryPolicy exponentialBackoffRetry(
+ int maxRetries, long sleepTime, TimeUnit timeUnit) {
+ return new ExponentialBackoffRetry(maxRetries, sleepTime, timeUnit);
+ }
+
+ public static final RetryPolicy failoverOnNetworkException(int maxFailovers) {
+ return failoverOnNetworkException(TRY_ONCE_THEN_FAIL, maxFailovers);
+ }
+
+ public static final RetryPolicy failoverOnNetworkException(
+ RetryPolicy fallbackPolicy, int maxFailovers) {
+ return failoverOnNetworkException(fallbackPolicy, maxFailovers, 0, 0);
+ }
+
+ public static final RetryPolicy failoverOnNetworkException(
+ RetryPolicy fallbackPolicy, int maxFailovers, long delayMillis,
+ long maxDelayBase) {
+ return new FailoverOnNetworkExceptionRetry(fallbackPolicy, maxFailovers,
+ delayMillis, maxDelayBase);
+ }
+
+ static class TryOnceThenFail implements RetryPolicy {
+ @Override
+ public RetryAction shouldRetry(Exception e, int retries, int failovers,
+ boolean isIdempotentOrAtMostOnce) throws Exception {
+ return new RetryAction(RetryAction.RetryDecision.FAIL, 0, "try once " +
+ "and fail.");
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ } else {
+ return obj != null && obj.getClass() == this.getClass();
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return this.getClass().hashCode();
+ }
+ }
+
+ static class RetryForever implements RetryPolicy {
+ @Override
+ public RetryAction shouldRetry(Exception e, int retries, int failovers,
+ boolean isIdempotentOrAtMostOnce) throws Exception {
+ return RetryAction.RETRY;
+ }
+ }
+
+ /**
+ * Retry up to maxRetries.
+ * The actual sleep time of the n-th retry is f(n, sleepTime),
+ * where f is a function provided by the subclass implementation.
+ *
+ * The object of the subclasses should be immutable;
+ * otherwise, the subclass must override hashCode(), equals(..) and toString().
+ */
+ static abstract class RetryLimited implements RetryPolicy {
+ final int maxRetries;
+ final long sleepTime;
+ final TimeUnit timeUnit;
+
+ private String myString;
+
+ RetryLimited(int maxRetries, long sleepTime, TimeUnit timeUnit) {
+ if (maxRetries < 0) {
+ throw new IllegalArgumentException("maxRetries = " + maxRetries+" < 0");
+ }
+ if (sleepTime < 0) {
+ throw new IllegalArgumentException("sleepTime = " + sleepTime + " < 0");
+ }
+
+ this.maxRetries = maxRetries;
+ this.sleepTime = sleepTime;
+ this.timeUnit = timeUnit;
+ }
+
+ @Override
+ public RetryAction shouldRetry(Exception e, int retries, int failovers,
+ boolean isIdempotentOrAtMostOnce) throws Exception {
+ if (retries >= maxRetries) {
+ return new RetryAction(RetryAction.RetryDecision.FAIL, 0 , getReason());
+ }
+ return new RetryAction(RetryAction.RetryDecision.RETRY,
+ timeUnit.toMillis(calculateSleepTime(retries)), getReason());
+ }
+
+ protected String getReason() {
+ return constructReasonString(maxRetries);
+ }
+
+ public static String constructReasonString(int retries) {
+ return "retries get failed due to exceeded maximum allowed retries " +
+ "number: " + retries;
+ }
+
+ protected abstract long calculateSleepTime(int retries);
+
+ @Override
+ public int hashCode() {
+ return toString().hashCode();
+ }
+
+ @Override
+ public boolean equals(final Object that) {
+ if (this == that) {
+ return true;
+ } else if (that == null || this.getClass() != that.getClass()) {
+ return false;
+ }
+ return this.toString().equals(that.toString());
+ }
+
+ @Override
+ public String toString() {
+ if (myString == null) {
+ myString = getClass().getSimpleName() + "(maxRetries=" + maxRetries
+ + ", sleepTime=" + sleepTime + " " + timeUnit + ")";
+ }
+ return myString;
+ }
+ }
+
+ static class RetryUpToMaximumCountWithFixedSleep extends RetryLimited {
+ public RetryUpToMaximumCountWithFixedSleep(int maxRetries, long sleepTime, TimeUnit timeUnit) {
+ super(maxRetries, sleepTime, timeUnit);
+ }
+
+ @Override
+ protected long calculateSleepTime(int retries) {
+ return sleepTime;
+ }
+ }
+
+ static class ExponentialBackoffRetry extends RetryLimited {
+
+ public ExponentialBackoffRetry(
+ int maxRetries, long sleepTime, TimeUnit timeUnit) {
+ super(maxRetries, sleepTime, timeUnit);
+
+ if (maxRetries < 0) {
+ throw new IllegalArgumentException("maxRetries = " + maxRetries + " < 0");
+ } else if (maxRetries >= Long.SIZE - 1) {
+ //calculateSleepTime may overflow.
+ throw new IllegalArgumentException("maxRetries = " + maxRetries
+ + " >= " + (Long.SIZE - 1));
+ }
+ }
+
+ @Override
+ protected long calculateSleepTime(int retries) {
+ return calculateExponentialTime(sleepTime, retries + 1);
+ }
+
+ }
+
+ /**
+ * Fail over and retry in the case of:
+ * Immediate socket exceptions (e.g. no route to host, econnrefused)
+ * Socket exceptions after initial connection when operation is idempotent
+ *
+ * The first failover is immediate, while all subsequent failovers wait an
+ * exponentially-increasing random amount of time.
+ *
+ * Fail immediately in the case of:
+ * Socket exceptions after initial connection when operation is not idempotent
+ *
+ * Fall back on underlying retry policy otherwise.
+ */
+ static class FailoverOnNetworkExceptionRetry implements RetryPolicy {
+
+ private RetryPolicy fallbackPolicy;
+ private int maxFailovers;
+ private int maxRetries;
+ private long delayMillis;
+ private long maxDelayBase;
+
+ public FailoverOnNetworkExceptionRetry(RetryPolicy fallbackPolicy,
+ int maxFailovers) {
+ this(fallbackPolicy, maxFailovers, 0, 0, 0);
+ }
+
+ public FailoverOnNetworkExceptionRetry(RetryPolicy fallbackPolicy,
+ int maxFailovers, long delayMillis, long maxDelayBase) {
+ this(fallbackPolicy, maxFailovers, 0, delayMillis, maxDelayBase);
+ }
+
+ public FailoverOnNetworkExceptionRetry(RetryPolicy fallbackPolicy,
+ int maxFailovers, int maxRetries, long delayMillis, long maxDelayBase) {
+ this.fallbackPolicy = fallbackPolicy;
+ this.maxFailovers = maxFailovers;
+ this.maxRetries = maxRetries;
+ this.delayMillis = delayMillis;
+ this.maxDelayBase = maxDelayBase;
+ }
+
+ /**
+ * @return 0 if this is our first failover/retry (i.e., retry immediately),
+ * sleep exponentially otherwise
+ */
+ private long getFailoverOrRetrySleepTime(int times) {
+ return times == 0 ? 0 :
+ calculateExponentialTime(delayMillis, times, maxDelayBase);
+ }
+
+ @Override
+ public RetryAction shouldRetry(Exception e, int retries,
+ int failovers, boolean isIdempotentOrAtMostOnce) throws Exception {
+ if (failovers >= maxFailovers) {
+ return new RetryAction(RetryAction.RetryDecision.FAIL, 0,
+ "failovers (" + failovers + ") exceeded maximum allowed ("
+ + maxFailovers + ")");
+ }
+ if (retries - failovers > maxRetries) {
+ return new RetryAction(RetryAction.RetryDecision.FAIL, 0, "retries ("
+ + retries + ") exceeded maximum allowed (" + maxRetries + ")");
+ }
+
+ if (isSaslFailure(e)) {
+ return new RetryAction(RetryAction.RetryDecision.FAIL, 0,
+ "SASL failure");
+ }
+
+ if (e instanceof ConnectException ||
+ e instanceof EOFException ||
+ e instanceof NoRouteToHostException ||
+ e instanceof UnknownHostException ||
+ e instanceof ConnectTimeoutException) {
+ return new RetryAction(RetryAction.RetryDecision.FAILOVER_AND_RETRY,
+ getFailoverOrRetrySleepTime(failovers));
+ } else if (e instanceof RetriableException
+ || getWrappedRetriableException(e) != null) {
+ // RetriableException or RetriableException wrapped
+ return new RetryAction(RetryAction.RetryDecision.RETRY,
+ getFailoverOrRetrySleepTime(retries));
+ } else if (e instanceof InvalidToken) {
+ return new RetryAction(RetryAction.RetryDecision.FAIL, 0,
+ "Invalid or Cancelled Token");
+ } else if (e instanceof AccessControlException ||
+ hasWrappedAccessControlException(e)) {
+ return new RetryAction(RetryAction.RetryDecision.FAIL, 0,
+ "Access denied");
+ } else if (e instanceof SocketException
+ || (e instanceof IOException && !(e instanceof RemoteException))) {
+ if (isIdempotentOrAtMostOnce) {
+ return new RetryAction(RetryAction.RetryDecision.FAILOVER_AND_RETRY,
+ getFailoverOrRetrySleepTime(retries));
+ } else {
+ return new RetryAction(RetryAction.RetryDecision.FAIL, 0,
+ "the invoked method is not idempotent, and unable to determine "
+ + "whether it was invoked");
+ }
+ } else {
+ return fallbackPolicy.shouldRetry(e, retries, failovers,
+ isIdempotentOrAtMostOnce);
+ }
+ }
+ }
+
+ /**
+ * Return a value which is time increasing exponentially as a
+ * function of retries, +/- 0%-50% of that value, chosen
+ * randomly.
+ *
+ * @param time the base amount of time to work with
+ * @param retries the number of retries that have so occurred so far
+ * @param cap value at which to cap the base sleep time
+ * @return an amount of time to sleep
+ */
+ private static long calculateExponentialTime(long time, int retries,
+ long cap) {
+ long baseTime = Math.min(time * (1L << retries), cap);
+ return (long) (baseTime * (ThreadLocalRandom.current().nextDouble() + 0.5));
+ }
+
+ private static long calculateExponentialTime(long time, int retries) {
+ return calculateExponentialTime(time, retries, Long.MAX_VALUE);
+ }
+
+ private static boolean isSaslFailure(Exception e) {
+ Throwable current = e;
+ do {
+ if (current instanceof SaslException) {
+ return true;
+ }
+ current = current.getCause();
+ } while (current != null);
+
+ return false;
+ }
+
+ static RetriableException getWrappedRetriableException(Exception e) {
+ if (!(e instanceof RemoteException)) {
+ return null;
+ }
+ Exception unwrapped = ((RemoteException)e).unwrapRemoteException(
+ RetriableException.class);
+ return unwrapped instanceof RetriableException ?
+ (RetriableException) unwrapped : null;
+ }
+
+ private static boolean hasWrappedAccessControlException(Exception e) {
+ Throwable throwable = e;
+ while (!(throwable instanceof AccessControlException) &&
+ throwable.getCause() != null) {
+ throwable = throwable.getCause();
+ }
+ return throwable instanceof AccessControlException;
+ }
+}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/io_/retry/RetryProxy.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/io_/retry/RetryProxy.java
new file mode 100644
index 000000000000..210e5e3b6552
--- /dev/null
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/io_/retry/RetryProxy.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.hadoop.io_.retry;
+
+import java.lang.reflect.Proxy;
+import org.apache.hadoop.io.retry.DefaultFailoverProxyProvider;
+import org.apache.hadoop.io.retry.FailoverProxyProvider;
+import org.apache.hadoop.io.retry.RetryPolicy;
+
+/**
+ *
+ * A factory for creating retry proxies.
+ *
+ */
+public class RetryProxy {
+ /**
+ *
+ * Create a proxy for an interface of an implementation class
+ * using the same retry policy for each method in the interface.
+ *
+ * @param iface the interface that the retry will implement
+ * @param implementation the instance whose methods should be retried
+ * @param retryPolicy the policy for retrying method call failures
+ * @param T.
+ * @return the retry proxy
+ */
+ public static Object create(Class iface, T implementation,
+ RetryPolicy retryPolicy) {
+ return RetryProxy.create(iface,
+ new DefaultFailoverProxyProvider(iface, implementation),
+ retryPolicy);
+ }
+
+ /**
+ * Create a proxy for an interface of implementations of that interface using
+ * the given {@link FailoverProxyProvider} and the same retry policy for each
+ * method in the interface.
+ *
+ * @param iface the interface that the retry will implement
+ * @param proxyProvider provides implementation instances whose methods should be retried
+ * @param retryPolicy the policy for retrying or failing over method call failures
+ * @param T.
+ * @return the retry proxy
+ */
+ public static Object create(Class iface,
+ FailoverProxyProvider proxyProvider, RetryPolicy retryPolicy) {
+ return Proxy.newProxyInstance(
+ proxyProvider.getInterface().getClassLoader(),
+ new Class>[] { iface },
+ new RetryInvocationHandler(proxyProvider, retryPolicy)
+ );
+ }
+
+}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/AsyncCallLimitExceededException.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/AsyncCallLimitExceededException.java
deleted file mode 100644
index 4050b68ff4f7..000000000000
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/AsyncCallLimitExceededException.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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.
- */
-
-package org.apache.hadoop.ipc_;
-
-import java.io.IOException;
-
-/**
- * Signals that an AsyncCallLimitExceededException has occurred. This class is
- * used to make application code using async RPC aware that limit of max async
- * calls is reached, application code need to retrieve results from response of
- * established async calls to avoid buffer overflow in order for follow-on async
- * calls going correctly.
- */
-public class AsyncCallLimitExceededException extends IOException {
- private static final long serialVersionUID = 1L;
-
- public AsyncCallLimitExceededException(String message) {
- super(message);
- }
-}
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/CallQueueManager.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/CallQueueManager.java
index f52a606bdcaa..a2d53def35f9 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/CallQueueManager.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/CallQueueManager.java
@@ -43,10 +43,6 @@ public class CallQueueManager
extends AbstractQueue implements BlockingQueue {
public static final Logger LOG =
LoggerFactory.getLogger(CallQueueManager.class);
- // Number of checkpoints for empty queue.
- private static final int CHECKPOINT_NUM = 20;
- // Interval to check empty queue.
- private static final long CHECKPOINT_INTERVAL_MS = 10;
@SuppressWarnings("unchecked")
static Class extends BlockingQueue> convertQueueClass(
@@ -86,14 +82,6 @@ public CallQueueManager(Class extends BlockingQueue> backingClass,
backingClass, maxQueueSize, schedulerClass, clientBackOffEnabled);
}
- CallQueueManager(BlockingQueue queue, RpcScheduler scheduler,
- boolean clientBackOffEnabled) {
- this.putRef = new AtomicReference>(queue);
- this.takeRef = new AtomicReference>(queue);
- this.scheduler = scheduler;
- this.clientBackOffEnabled = clientBackOffEnabled;
- }
-
private static T createScheduler(
Class theClass, int priorityLevels, String ns, Configuration conf) {
// Used for custom, configurable scheduler
@@ -346,69 +334,6 @@ private static int parseNumLevels(String ns, Configuration conf) {
return retval;
}
- /**
- * Replaces active queue with the newly requested one and transfers
- * all calls to the newQ before returning.
- *
- * @param schedulerClass input schedulerClass.
- * @param queueClassToUse input queueClassToUse.
- * @param maxSize input maxSize.
- * @param ns input ns.
- * @param conf input configuration.
- */
- public synchronized void swapQueue(
- Class extends RpcScheduler> schedulerClass,
- Class extends BlockingQueue> queueClassToUse, int maxSize,
- String ns, Configuration conf) {
- int priorityLevels = parseNumLevels(ns, conf);
- this.scheduler.stop();
- RpcScheduler newScheduler = createScheduler(schedulerClass, priorityLevels,
- ns, conf);
- BlockingQueue newQ = createCallQueueInstance(queueClassToUse,
- priorityLevels, maxSize, ns, conf);
-
- // Our current queue becomes the old queue
- BlockingQueue oldQ = putRef.get();
-
- // Swap putRef first: allow blocked puts() to be unblocked
- putRef.set(newQ);
-
- // Wait for handlers to drain the oldQ
- while (!queueIsReallyEmpty(oldQ)) {}
-
- // Swap takeRef to handle new calls
- takeRef.set(newQ);
-
- this.scheduler = newScheduler;
-
- LOG.info("Old Queue: " + stringRepr(oldQ) + ", " +
- "Replacement: " + stringRepr(newQ));
- }
-
- /**
- * Checks if queue is empty by checking at CHECKPOINT_NUM points with
- * CHECKPOINT_INTERVAL_MS interval.
- * This doesn't mean the queue might not fill up at some point later, but
- * it should decrease the probability that we lose a call this way.
- */
- private boolean queueIsReallyEmpty(BlockingQueue> q) {
- for (int i = 0; i < CHECKPOINT_NUM; i++) {
- try {
- Thread.sleep(CHECKPOINT_INTERVAL_MS);
- } catch (InterruptedException ie) {
- return false;
- }
- if (!q.isEmpty()) {
- return false;
- }
- }
- return true;
- }
-
- private String stringRepr(Object o) {
- return o.getClass().getName() + '@' + Integer.toHexString(o.hashCode());
- }
-
@Override
public int drainTo(Collection super E> c) {
return takeRef.get().drainTo(c);
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/Client.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/Client.java
index f1a67df33053..e54844672b22 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/Client.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/Client.java
@@ -27,7 +27,7 @@
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableUtils;
-import org.apache.hadoop.io.retry.RetryPolicies;
+import org.apache.hadoop.io_.retry.RetryPolicies;
import org.apache.hadoop.io.retry.RetryPolicy;
import org.apache.hadoop.io.retry.RetryPolicy.RetryAction;
import org.apache.hadoop.ipc_.RPC.RpcKind;
@@ -83,39 +83,19 @@ public class Client implements AutoCloseable {
private static final ThreadLocal callId = new ThreadLocal();
private static final ThreadLocal retryCount = new ThreadLocal();
- private static final ThreadLocal