mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-25 13:36:50 +00:00
perf(server): lighten internode data-plane stack (#3735)
* refactor(server): split internode dispatch scaffold
* test(server): cover internode dispatch prefix split
* refactor(server): name internode stack boundaries
* perf(server): skip internode request logging layer
* perf(server): skip internode trace layer
* perf(server): use lite internode request context
* feat(metrics): track internode rpc duration
* feat(ecstore): add put object stage summary logs
* test(metrics): update internode descriptor expectations
* fix(server): tighten internode path matching
* fix(pr): address review follow-up comments
* style(ecstore): simplify commit tail duration field
* refactor(ecstore): group put stage summary fields
* refactor(ecstore): inline put stage summary log
* fix(s3): return storage class for object attributes
* merge: sync latest main and resolve object attributes conflict
* fmt
* fix(server): remove duplicate rpc imports
* build(deps): bump memmap2 for RUSTSEC-2026-0186
* fix(s3select): align object_store with datafusion
* chore(deps): prune workspace dependencies
* perf(fuzz): optimize CI runtime with build/run split and matrix parallelization
Separate fuzz harness compilation from execution to eliminate redundant
builds across targets. Introduce matrix-based parallel execution for
PR smoke and nightly fuzz jobs.
Changes:
- Split CI workflow into `fuzz-build` (compile once) and matrix run jobs
(`pr-fuzz-smoke`, `nightly-fuzz-corpus`) that execute targets in parallel
- Add `BUILD_ONLY` mode to run_ci_targets.sh / run_nightly_targets.sh
- Add run_single_target.sh for matrix jobs (no build phase)
- Optimize `local_metadata` fuzz target: reduce prefix iterations from
8-10 (4 functions each) to 5 critical prefixes (parser-only), cutting
per-iteration cost by ~3-5x
- Move archive path validation (`validate_extract_relative_path`,
`normalize_extract_entry_key`) from `rustfs` to `rustfs-utils::path`,
eliminating `rustfs` binary crate dependency from fuzz harness
- Remove `rustfs` from fuzz/Cargo.toml (drops significant transitive deps)
- Add unit tests for archive path validation in rustfs-utils
- Update fuzz/README.md with new workflow and script documentation
Expected CI improvement: PR smoke wall-clock from ~120min (frequent
timeout) to ~40min; nightly from ~180min to ~60min.
* refactor(fuzz): consolidate scripts and fix prefix test alignment
Replace three duplicated shell scripts (run_ci_targets.sh,
run_nightly_targets.sh, run_single_target.sh) with a single
parameterized run.sh that supports BUILD_ONLY, SKIP_BUILD, and
MAX_TOTAL_TIME environment variables.
Fix local_metadata fuzz target prefix testing: replace always-true
'len > 0' guard with lengths aligned to xl.meta binary layout
(4/5/8/12 bytes for magic+version+header fields). Remove redundant
empty-slice test.
Hoist RUSTFLAGS to workflow top-level env to eliminate per-job
duplication. Update README with unified script documentation.
Net: -118 lines, zero functionality loss.
* perf(fuzz): optimize CI runtime with build/run split and matrix parallelization
Restructure fuzz CI workflow to eliminate redundant compilation and
run targets in parallel via matrix strategy.
Workflow changes:
- Split into fuzz-build (compile once) and matrix run jobs
- PR smoke: 3 targets parallel, 60s each, timeout 30min (was 120min)
- Nightly: 3 targets parallel, 300s each, timeout 60min (was 180min)
- Pass compiled harness via actions/artifact between jobs
- Hoist RUSTFLAGS to workflow top-level env
Script consolidation:
- Replace 3 duplicated scripts with single parameterized run.sh
- Supports BUILD_ONLY, SKIP_BUILD, MAX_TOTAL_TIME env vars
Target optimizations:
- Remove rustfs binary crate from fuzz dependencies (was pulling
979 transitive deps); move archive path validation to rustfs-utils
- Optimize local_metadata: reduce prefix iterations from 8-10x4
calls to 5 prefixes with parser-only (no decompress), aligned
with xl.meta binary layout (4/5/8/12 bytes)
- Add unit tests for archive path validation in rustfs-utils
- Update fuzz/README.md with unified script documentation
Expected: PR smoke wall-clock from ~120min (frequent timeout)
to ~40min; nightly from ~180min to ~60min.
* fix(rpc): resolve internode metrics via app context
* fmt
* ci: speed up fuzz smoke artifact restore
---------
Signed-off-by: houseme <housemecn@gmail.com>
This commit is contained in:
+105
-53
@@ -47,108 +47,160 @@ concurrency:
|
|||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
CARGO_BUILD_TARGET: x86_64-unknown-linux-gnu
|
CARGO_BUILD_TARGET: x86_64-unknown-linux-gnu
|
||||||
|
RUSTFLAGS: "--cfg tokio_unstable -C target-feature=-crt-static"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# Phase 1: Build all fuzz harness binaries once.
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
fuzz-build:
|
||||||
|
name: Build Fuzz Harness
|
||||||
|
if: >
|
||||||
|
github.event_name == 'pull_request' ||
|
||||||
|
github.event_name == 'schedule' ||
|
||||||
|
github.event_name == 'workflow_dispatch'
|
||||||
|
runs-on: ubicloud-standard-4
|
||||||
|
timeout-minutes: 45
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Setup Rust environment
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
rust-version: nightly
|
||||||
|
cache-shared-key: fuzz-${{ hashFiles('fuzz/Cargo.lock') }}
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
cache-save-if: ${{ github.ref == 'refs/heads/main' || github.event_name == 'schedule' }}
|
||||||
|
|
||||||
|
- name: Install cargo-fuzz
|
||||||
|
uses: taiki-e/install-action@v2
|
||||||
|
with:
|
||||||
|
tool: cargo-fuzz
|
||||||
|
|
||||||
|
- name: Build all fuzz targets
|
||||||
|
run: BUILD_ONLY=1 ./scripts/fuzz/run.sh
|
||||||
|
|
||||||
|
- name: Stage prebuilt fuzz binaries
|
||||||
|
run: |
|
||||||
|
binary_root="fuzz/prebuilt/${CARGO_BUILD_TARGET}/release"
|
||||||
|
mkdir -p "${binary_root}"
|
||||||
|
for target in path_containment bucket_validation local_metadata; do
|
||||||
|
install -Dm755 "fuzz/target/${CARGO_BUILD_TARGET}/release/${target}" "${binary_root}/${target}"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Upload prebuilt fuzz binaries
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: fuzz-prebuilt-binaries-${{ github.run_number }}
|
||||||
|
path: |
|
||||||
|
fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release/path_containment
|
||||||
|
fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release/bucket_validation
|
||||||
|
fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release/local_metadata
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
compression-level: 0
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# Phase 2 (PR): Run smoke targets in parallel via matrix.
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
pr-fuzz-smoke:
|
pr-fuzz-smoke:
|
||||||
name: PR Fuzz Smoke
|
name: "Smoke / ${{ matrix.target }}"
|
||||||
|
needs: fuzz-build
|
||||||
if: >
|
if: >
|
||||||
github.event_name == 'pull_request' ||
|
github.event_name == 'pull_request' ||
|
||||||
(github.event_name == 'workflow_dispatch' &&
|
(github.event_name == 'workflow_dispatch' &&
|
||||||
(github.event.inputs.profile == 'smoke' || github.event.inputs.profile == 'both'))
|
(github.event.inputs.profile == 'smoke' || github.event.inputs.profile == 'both'))
|
||||||
runs-on: ubicloud-standard-4
|
runs-on: ubicloud-standard-4
|
||||||
timeout-minutes: 120
|
timeout-minutes: 30
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
target: [path_containment, bucket_validation, local_metadata]
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||||
MAX_TOTAL_TIME: 60
|
|
||||||
ARTIFACT_ROOT: artifacts
|
|
||||||
RUSTFLAGS: "--cfg tokio_unstable -C target-feature=-crt-static"
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup Rust environment
|
- name: Download prebuilt fuzz binaries
|
||||||
uses: ./.github/actions/setup
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
rust-version: nightly
|
name: fuzz-prebuilt-binaries-${{ github.run_number }}
|
||||||
cache-shared-key: fuzz-smoke-${{ hashFiles('fuzz/Cargo.lock') }}
|
path: fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
|
||||||
|
|
||||||
- name: Install cargo-fuzz
|
- name: Restore executable bit
|
||||||
uses: taiki-e/install-action@v2
|
run: chmod +x "fuzz/prebuilt/${CARGO_BUILD_TARGET}/release/${{ matrix.target }}"
|
||||||
with:
|
|
||||||
tool: cargo-fuzz
|
|
||||||
|
|
||||||
- name: Run path_containment smoke target
|
- name: Run ${{ matrix.target }}
|
||||||
env:
|
env:
|
||||||
FUZZ_TARGET: path_containment
|
FUZZ_TARGET: ${{ matrix.target }}
|
||||||
run: ./scripts/fuzz/run_ci_targets.sh
|
MAX_TOTAL_TIME: 60
|
||||||
|
SKIP_BUILD: 1
|
||||||
- name: Run bucket_validation smoke target
|
USE_PREBUILT_BINARY: 1
|
||||||
env:
|
PREBUILT_BINARY_DIR: ${{ github.workspace }}/fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release
|
||||||
FUZZ_TARGET: bucket_validation
|
run: ./scripts/fuzz/run.sh
|
||||||
run: ./scripts/fuzz/run_ci_targets.sh
|
|
||||||
|
|
||||||
- name: Run local_metadata smoke target
|
|
||||||
env:
|
|
||||||
FUZZ_TARGET: local_metadata
|
|
||||||
run: ./scripts/fuzz/run_ci_targets.sh
|
|
||||||
|
|
||||||
- name: Run bounded fuzz smoke targets
|
|
||||||
if: ${{ false }}
|
|
||||||
run: ./scripts/fuzz/run_ci_targets.sh
|
|
||||||
|
|
||||||
- name: Upload fuzz smoke artifacts
|
- name: Upload fuzz smoke artifacts
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: fuzz-smoke-${{ github.run_number }}
|
name: fuzz-smoke-${{ matrix.target }}-${{ github.run_number }}
|
||||||
path: |
|
path: |
|
||||||
fuzz/artifacts/**
|
fuzz/artifacts/**
|
||||||
fuzz/corpus/**
|
fuzz/corpus/${{ matrix.target }}/**
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# Phase 2 (Nightly): Run all targets in parallel via matrix.
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
nightly-fuzz-corpus:
|
nightly-fuzz-corpus:
|
||||||
name: Nightly Fuzz Corpus
|
name: "Nightly / ${{ matrix.target }}"
|
||||||
|
needs: fuzz-build
|
||||||
if: >
|
if: >
|
||||||
github.event_name == 'schedule' ||
|
github.event_name == 'schedule' ||
|
||||||
(github.event_name == 'workflow_dispatch' &&
|
(github.event_name == 'workflow_dispatch' &&
|
||||||
(github.event.inputs.profile == 'nightly' || github.event.inputs.profile == 'both'))
|
(github.event.inputs.profile == 'nightly' || github.event.inputs.profile == 'both'))
|
||||||
runs-on: ubicloud-standard-4
|
runs-on: ubicloud-standard-4
|
||||||
timeout-minutes: 180
|
timeout-minutes: 60
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
target: [path_containment, bucket_validation, local_metadata]
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||||
MAX_TOTAL_TIME: 300
|
|
||||||
ARTIFACT_ROOT: artifacts
|
|
||||||
RUSTFLAGS: "--cfg tokio_unstable -C target-feature=-crt-static"
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup Rust environment
|
- name: Download prebuilt fuzz binaries
|
||||||
uses: ./.github/actions/setup
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
rust-version: nightly
|
name: fuzz-prebuilt-binaries-${{ github.run_number }}
|
||||||
cache-shared-key: fuzz-nightly-${{ hashFiles('fuzz/Cargo.lock') }}
|
path: fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
|
||||||
|
|
||||||
- name: Install cargo-fuzz
|
- name: Restore executable bit
|
||||||
uses: taiki-e/install-action@v2
|
run: chmod +x "fuzz/prebuilt/${CARGO_BUILD_TARGET}/release/${{ matrix.target }}"
|
||||||
with:
|
|
||||||
tool: cargo-fuzz
|
|
||||||
|
|
||||||
- name: Run nightly fuzz targets
|
- name: Run ${{ matrix.target }}
|
||||||
run: ./scripts/fuzz/run_nightly_targets.sh
|
env:
|
||||||
|
FUZZ_TARGET: ${{ matrix.target }}
|
||||||
|
MAX_TOTAL_TIME: 300
|
||||||
|
SKIP_BUILD: 1
|
||||||
|
USE_PREBUILT_BINARY: 1
|
||||||
|
PREBUILT_BINARY_DIR: ${{ github.workspace }}/fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release
|
||||||
|
run: ./scripts/fuzz/run.sh
|
||||||
|
|
||||||
- name: Upload nightly fuzz artifacts
|
- name: Upload nightly fuzz artifacts
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: fuzz-nightly-${{ github.run_number }}
|
name: fuzz-nightly-${{ matrix.target }}-${{ github.run_number }}
|
||||||
path: |
|
path: |
|
||||||
fuzz/artifacts/**
|
fuzz/artifacts/**
|
||||||
fuzz/corpus/**
|
fuzz/corpus/${{ matrix.target }}/**
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|||||||
Generated
+16
-21
@@ -7830,9 +7830,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "primefield"
|
name = "primefield"
|
||||||
version = "0.14.0-rc.12"
|
version = "0.14.0-rc.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8675564771a62f69a0af716b03e89b917b963c7b173b5855575e84fd4f605ca0"
|
checksum = "2db02b39ea98560a1fec81df6266f3c1ef7fdde06ac5ef17f69aee6101602630"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-bigint 0.7.5",
|
"crypto-bigint 0.7.5",
|
||||||
"crypto-common 0.2.2",
|
"crypto-common 0.2.2",
|
||||||
@@ -8250,9 +8250,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn"
|
name = "quinn"
|
||||||
version = "0.11.9"
|
version = "0.11.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
@@ -8270,9 +8270,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn-proto"
|
name = "quinn-proto"
|
||||||
version = "0.11.14"
|
version = "0.11.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -8306,9 +8306,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.46"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@@ -9347,7 +9347,6 @@ dependencies = [
|
|||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"rustfs-checksums",
|
"rustfs-checksums",
|
||||||
"rustfs-common",
|
"rustfs-common",
|
||||||
"rustfs-concurrency",
|
|
||||||
"rustfs-config",
|
"rustfs-config",
|
||||||
"rustfs-credentials",
|
"rustfs-credentials",
|
||||||
"rustfs-data-usage",
|
"rustfs-data-usage",
|
||||||
@@ -9628,7 +9627,6 @@ dependencies = [
|
|||||||
"rustfs-utils",
|
"rustfs-utils",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.11.0",
|
|
||||||
"temp-env",
|
"temp-env",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
@@ -9694,14 +9692,12 @@ dependencies = [
|
|||||||
"rustfs-ecstore",
|
"rustfs-ecstore",
|
||||||
"rustfs-s3-ops",
|
"rustfs-s3-ops",
|
||||||
"rustfs-s3-types",
|
"rustfs-s3-types",
|
||||||
"rustfs-storage-api",
|
|
||||||
"rustfs-targets",
|
"rustfs-targets",
|
||||||
"rustfs-utils",
|
"rustfs-utils",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"starshard",
|
"starshard",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"time",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -9966,7 +9962,6 @@ dependencies = [
|
|||||||
"futures-core",
|
"futures-core",
|
||||||
"http 1.4.2",
|
"http 1.4.2",
|
||||||
"metrics",
|
"metrics",
|
||||||
"object_store",
|
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustfs-common",
|
"rustfs-common",
|
||||||
@@ -10282,9 +10277,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.40"
|
version = "0.23.41"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
|
checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"log",
|
"log",
|
||||||
@@ -11454,9 +11449,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sysinfo"
|
name = "sysinfo"
|
||||||
version = "0.39.4"
|
version = "0.39.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a71c821502e2786cc74c965f86561090a2b08fea91507cb599b5da291486a2aa"
|
checksum = "2c8bd2130a9b60bee2581bf82cfe89ee836424d1f37dcfa4ce21509611684673"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -11666,9 +11661,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.49"
|
version = "0.3.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469"
|
checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -11689,9 +11684,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.29"
|
version = "0.2.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d"
|
checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"time-core",
|
"time-core",
|
||||||
|
|||||||
+3
-4
@@ -185,7 +185,7 @@ jsonwebtoken = { version = "10.4.0", features = ["aws_lc_rs"] }
|
|||||||
openidconnect = { version = "4.0", default-features = false }
|
openidconnect = { version = "4.0", default-features = false }
|
||||||
pbkdf2 = "0.13.0"
|
pbkdf2 = "0.13.0"
|
||||||
rsa = { version = "0.10.0-rc.18" }
|
rsa = { version = "0.10.0-rc.18" }
|
||||||
rustls = { version = "0.23.40", default-features = false, features = ["aws-lc-rs", "logging", "tls12", "prefer-post-quantum", "std"] }
|
rustls = { version = "0.23.41", default-features = false, features = ["aws-lc-rs", "logging", "tls12", "prefer-post-quantum", "std"] }
|
||||||
rustls-native-certs = "0.8"
|
rustls-native-certs = "0.8"
|
||||||
rustls-pki-types = "1.14.1"
|
rustls-pki-types = "1.14.1"
|
||||||
sha1 = "0.11.0"
|
sha1 = "0.11.0"
|
||||||
@@ -197,7 +197,7 @@ zeroize = { version = "1.9.0", features = ["derive"] }
|
|||||||
chrono = { version = "0.4.45", features = ["serde"] }
|
chrono = { version = "0.4.45", features = ["serde"] }
|
||||||
humantime = "2.3.0"
|
humantime = "2.3.0"
|
||||||
jiff = { version = "0.2.29", features = ["serde"] }
|
jiff = { version = "0.2.29", features = ["serde"] }
|
||||||
time = { version = "0.3.49", features = ["std", "parsing", "formatting", "macros", "serde"] }
|
time = { version = "0.3.51", features = ["std", "parsing", "formatting", "macros", "serde"] }
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
deadpool-postgres = { version = "0.14", features = ["rt_tokio_1"] }
|
deadpool-postgres = { version = "0.14", features = ["rt_tokio_1"] }
|
||||||
@@ -253,7 +253,6 @@ moka = { version = "0.12.15", features = ["future"] }
|
|||||||
netif = "0.1.6"
|
netif = "0.1.6"
|
||||||
num_cpus = { version = "1.17.0" }
|
num_cpus = { version = "1.17.0" }
|
||||||
nvml-wrapper = "0.12.1"
|
nvml-wrapper = "0.12.1"
|
||||||
object_store = "0.13.2"
|
|
||||||
parking_lot = "0.12.5"
|
parking_lot = "0.12.5"
|
||||||
path-absolutize = "3.1.1"
|
path-absolutize = "3.1.1"
|
||||||
path-clean = "1.0.1"
|
path-clean = "1.0.1"
|
||||||
@@ -282,7 +281,7 @@ snafu = "0.9.1"
|
|||||||
snap = "1.1.1"
|
snap = "1.1.1"
|
||||||
starshard = { version = "2.2.1", features = ["rayon", "async", "serde"] }
|
starshard = { version = "2.2.1", features = ["rayon", "async", "serde"] }
|
||||||
strum = { version = "0.28.0", features = ["derive"] }
|
strum = { version = "0.28.0", features = ["derive"] }
|
||||||
sysinfo = "0.39.4"
|
sysinfo = "0.39.5"
|
||||||
temp-env = "0.3.6"
|
temp-env = "0.3.6"
|
||||||
tempfile = "3.27.0"
|
tempfile = "3.27.0"
|
||||||
test-case = "3.3.1"
|
test-case = "3.3.1"
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ memmap2 = { workspace = true }
|
|||||||
libc.workspace = true
|
libc.workspace = true
|
||||||
rustix = { workspace = true }
|
rustix = { workspace = true }
|
||||||
rustfs-madmin.workspace = true
|
rustfs-madmin.workspace = true
|
||||||
rustfs-concurrency.workspace = true
|
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
aes-gcm.workspace = true
|
aes-gcm.workspace = true
|
||||||
chacha20poly1305.workspace = true
|
chacha20poly1305.workspace = true
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ const EVENT_SET_DISK_MULTIPART: &str = "set_disk_multipart";
|
|||||||
const EVENT_SET_DISK_WRITE: &str = "set_disk_write";
|
const EVENT_SET_DISK_WRITE: &str = "set_disk_write";
|
||||||
const EVENT_SET_DISK_HEAL: &str = "set_disk_heal";
|
const EVENT_SET_DISK_HEAL: &str = "set_disk_heal";
|
||||||
const EVENT_SET_DISK_COMMIT_TAIL_SLOW: &str = "set_disk_commit_tail_slow";
|
const EVENT_SET_DISK_COMMIT_TAIL_SLOW: &str = "set_disk_commit_tail_slow";
|
||||||
|
const EVENT_SET_DISK_PUT_OBJECT_STAGE_SUMMARY: &str = "set_disk_put_object_stage_summary";
|
||||||
const SET_DISK_COMMIT_TAIL_WARN_THRESHOLD_MS: u128 = 5_000;
|
const SET_DISK_COMMIT_TAIL_WARN_THRESHOLD_MS: u128 = 5_000;
|
||||||
|
|
||||||
use crate::rio::{EtagResolvable, HashReader, HashReaderMut, TryGetIndex as _};
|
use crate::rio::{EtagResolvable, HashReader, HashReaderMut, TryGetIndex as _};
|
||||||
@@ -1138,10 +1139,8 @@ impl rustfs_storage_api::ObjectIO for SetDisks {
|
|||||||
writers.push(w);
|
writers.push(w);
|
||||||
errors.push(e);
|
errors.push(e);
|
||||||
}
|
}
|
||||||
rustfs_io_metrics::record_put_object_stage_duration(
|
let writer_setup_ms = writer_setup_stage_start.elapsed().as_millis() as u64;
|
||||||
"set_disk_writer_setup",
|
rustfs_io_metrics::record_put_object_stage_duration("set_disk_writer_setup", writer_setup_ms as f64);
|
||||||
writer_setup_stage_start.elapsed().as_secs_f64() * 1000.0,
|
|
||||||
);
|
|
||||||
|
|
||||||
let nil_count = errors.iter().filter(|&e| e.is_none()).count();
|
let nil_count = errors.iter().filter(|&e| e.is_none()).count();
|
||||||
if nil_count < write_quorum {
|
if nil_count < write_quorum {
|
||||||
@@ -1202,10 +1201,8 @@ impl rustfs_storage_api::ObjectIO for SetDisks {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
rustfs_io_metrics::record_put_object_stage_duration(
|
let encode_ms = encode_stage_start.elapsed().as_millis() as u64;
|
||||||
"set_disk_encode",
|
rustfs_io_metrics::record_put_object_stage_duration("set_disk_encode", encode_ms as f64);
|
||||||
encode_stage_start.elapsed().as_secs_f64() * 1000.0,
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = mem::replace(&mut data.stream, reader);
|
let _ = mem::replace(&mut data.stream, reader);
|
||||||
// if let Err(err) = close_bitrot_writers(&mut writers).await {
|
// if let Err(err) = close_bitrot_writers(&mut writers).await {
|
||||||
@@ -1314,12 +1311,9 @@ impl rustfs_storage_api::ObjectIO for SetDisks {
|
|||||||
write_quorum,
|
write_quorum,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
rustfs_io_metrics::record_put_object_stage_duration(
|
let rename_stage_ms = rename_stage_start.elapsed().as_millis() as u64;
|
||||||
"set_disk_rename",
|
rustfs_io_metrics::record_put_object_stage_duration("set_disk_rename", rename_stage_ms as f64);
|
||||||
rename_stage_start.elapsed().as_secs_f64() * 1000.0,
|
if (rename_stage_ms as u128) >= SET_DISK_COMMIT_TAIL_WARN_THRESHOLD_MS {
|
||||||
);
|
|
||||||
let rename_stage_ms = rename_stage_start.elapsed().as_millis();
|
|
||||||
if rename_stage_ms >= SET_DISK_COMMIT_TAIL_WARN_THRESHOLD_MS {
|
|
||||||
warn!(
|
warn!(
|
||||||
event = EVENT_SET_DISK_COMMIT_TAIL_SLOW,
|
event = EVENT_SET_DISK_COMMIT_TAIL_SLOW,
|
||||||
component = LOG_COMPONENT_ECSTORE,
|
component = LOG_COMPONENT_ECSTORE,
|
||||||
@@ -1328,23 +1322,22 @@ impl rustfs_storage_api::ObjectIO for SetDisks {
|
|||||||
bucket = %bucket,
|
bucket = %bucket,
|
||||||
object = %object,
|
object = %object,
|
||||||
tmp_dir = %tmp_dir,
|
tmp_dir = %tmp_dir,
|
||||||
duration_ms = rename_stage_ms as u64,
|
duration_ms = { rename_stage_ms },
|
||||||
write_quorum,
|
write_quorum,
|
||||||
state = "slow",
|
state = "slow",
|
||||||
"SetDisk commit tail stage is slow"
|
"SetDisk commit tail stage is slow"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut cleanup_stage_ms: Option<u64> = None;
|
||||||
if let Some(old_dir) = op_old_dir {
|
if let Some(old_dir) = op_old_dir {
|
||||||
let cleanup_stage_start = Instant::now();
|
let cleanup_stage_start = Instant::now();
|
||||||
self.commit_rename_data_dir(&cleanup_disks, bucket, object, &old_dir.to_string(), write_quorum)
|
self.commit_rename_data_dir(&cleanup_disks, bucket, object, &old_dir.to_string(), write_quorum)
|
||||||
.await?;
|
.await?;
|
||||||
rustfs_io_metrics::record_put_object_stage_duration(
|
let cleanup_ms = cleanup_stage_start.elapsed().as_millis() as u64;
|
||||||
"set_disk_old_data_cleanup",
|
cleanup_stage_ms = Some(cleanup_ms);
|
||||||
cleanup_stage_start.elapsed().as_secs_f64() * 1000.0,
|
rustfs_io_metrics::record_put_object_stage_duration("set_disk_old_data_cleanup", cleanup_ms as f64);
|
||||||
);
|
if (cleanup_ms as u128) >= SET_DISK_COMMIT_TAIL_WARN_THRESHOLD_MS {
|
||||||
let cleanup_stage_ms = cleanup_stage_start.elapsed().as_millis();
|
|
||||||
if cleanup_stage_ms >= SET_DISK_COMMIT_TAIL_WARN_THRESHOLD_MS {
|
|
||||||
warn!(
|
warn!(
|
||||||
event = EVENT_SET_DISK_COMMIT_TAIL_SLOW,
|
event = EVENT_SET_DISK_COMMIT_TAIL_SLOW,
|
||||||
component = LOG_COMPONENT_ECSTORE,
|
component = LOG_COMPONENT_ECSTORE,
|
||||||
@@ -1354,7 +1347,7 @@ impl rustfs_storage_api::ObjectIO for SetDisks {
|
|||||||
object = %object,
|
object = %object,
|
||||||
tmp_dir = %tmp_dir,
|
tmp_dir = %tmp_dir,
|
||||||
old_dir = %old_dir,
|
old_dir = %old_dir,
|
||||||
duration_ms = cleanup_stage_ms as u64,
|
duration_ms = cleanup_ms,
|
||||||
write_quorum,
|
write_quorum,
|
||||||
state = "slow",
|
state = "slow",
|
||||||
"SetDisk commit tail stage is slow"
|
"SetDisk commit tail stage is slow"
|
||||||
@@ -1411,10 +1404,51 @@ impl rustfs_storage_api::ObjectIO for SetDisks {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if issue3031_diag_enabled() {
|
||||||
|
warn!(
|
||||||
|
event = EVENT_SET_DISK_PUT_OBJECT_STAGE_SUMMARY,
|
||||||
|
component = LOG_COMPONENT_ECSTORE,
|
||||||
|
subsystem = LOG_SUBSYSTEM_SET_DISK,
|
||||||
|
bucket = %bucket,
|
||||||
|
object = %object,
|
||||||
|
write_quorum,
|
||||||
|
write_path = write_path.metric_label(),
|
||||||
|
writer_setup_ms,
|
||||||
|
encode_ms,
|
||||||
|
rename_ms = rename_stage_ms,
|
||||||
|
cleanup_ms = cleanup_stage_ms.unwrap_or_default(),
|
||||||
|
cleanup_present = cleanup_stage_ms.is_some(),
|
||||||
|
commit_tail_ms = total_commit_tail_ms as u64,
|
||||||
|
result = "success",
|
||||||
|
"SetDisk put_object stage summary"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(ObjectInfo::from_file_info(&fi, bucket, object, opts.versioned || opts.version_suspended))
|
Ok(ObjectInfo::from_file_info(&fi, bucket, object, opts.versioned || opts.version_suspended))
|
||||||
}
|
}
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
if issue3031_diag_enabled()
|
||||||
|
&& let Err(err) = &result
|
||||||
|
{
|
||||||
|
let stage_hint = if err.to_string().contains("not enough disks to write") {
|
||||||
|
"writer_setup_or_quorum"
|
||||||
|
} else {
|
||||||
|
"unknown"
|
||||||
|
};
|
||||||
|
warn!(
|
||||||
|
event = EVENT_SET_DISK_PUT_OBJECT_STAGE_SUMMARY,
|
||||||
|
component = LOG_COMPONENT_ECSTORE,
|
||||||
|
subsystem = LOG_SUBSYSTEM_SET_DISK,
|
||||||
|
bucket = %bucket,
|
||||||
|
object = %object,
|
||||||
|
result = "error",
|
||||||
|
stage_hint,
|
||||||
|
error = %err,
|
||||||
|
"SetDisk put_object stage summary"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if issue3031_diag_enabled() {
|
if issue3031_diag_enabled() {
|
||||||
warn!(
|
warn!(
|
||||||
target: "rustfs_ecstore::set_disk",
|
target: "rustfs_ecstore::set_disk",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const INTERNODE_OPERATION_RECV_BYTES_TOTAL: &str = "rustfs_system_network_intern
|
|||||||
const INTERNODE_OPERATION_REQUESTS_OUTGOING_TOTAL: &str = "rustfs_system_network_internode_operation_requests_outgoing_total";
|
const INTERNODE_OPERATION_REQUESTS_OUTGOING_TOTAL: &str = "rustfs_system_network_internode_operation_requests_outgoing_total";
|
||||||
const INTERNODE_OPERATION_REQUESTS_INCOMING_TOTAL: &str = "rustfs_system_network_internode_operation_requests_incoming_total";
|
const INTERNODE_OPERATION_REQUESTS_INCOMING_TOTAL: &str = "rustfs_system_network_internode_operation_requests_incoming_total";
|
||||||
const INTERNODE_OPERATION_ERRORS_TOTAL: &str = "rustfs_system_network_internode_operation_errors_total";
|
const INTERNODE_OPERATION_ERRORS_TOTAL: &str = "rustfs_system_network_internode_operation_errors_total";
|
||||||
|
const INTERNODE_OPERATION_DURATION_MS: &str = "rustfs_system_network_internode_operation_duration_ms";
|
||||||
const INTERNODE_OPERATION_CLASSIFIED_ERRORS_TOTAL: &str = "rustfs_system_network_internode_operation_classified_errors_total";
|
const INTERNODE_OPERATION_CLASSIFIED_ERRORS_TOTAL: &str = "rustfs_system_network_internode_operation_classified_errors_total";
|
||||||
const INTERNODE_OPERATION_RETRIES_TOTAL: &str = "rustfs_system_network_internode_operation_retries_total";
|
const INTERNODE_OPERATION_RETRIES_TOTAL: &str = "rustfs_system_network_internode_operation_retries_total";
|
||||||
const INTERNODE_OPERATION_RETRY_SUCCESSES_TOTAL: &str = "rustfs_system_network_internode_operation_retry_successes_total";
|
const INTERNODE_OPERATION_RETRY_SUCCESSES_TOTAL: &str = "rustfs_system_network_internode_operation_retry_successes_total";
|
||||||
@@ -74,6 +75,10 @@ pub const INTERNODE_OPERATION_METRICS: &[InternodeOperationMetricDescriptor] = &
|
|||||||
name: INTERNODE_OPERATION_ERRORS_TOTAL,
|
name: INTERNODE_OPERATION_ERRORS_TOTAL,
|
||||||
labels: OPERATION_BACKEND_LABELS,
|
labels: OPERATION_BACKEND_LABELS,
|
||||||
},
|
},
|
||||||
|
InternodeOperationMetricDescriptor {
|
||||||
|
name: INTERNODE_OPERATION_DURATION_MS,
|
||||||
|
labels: OPERATION_BACKEND_LABELS,
|
||||||
|
},
|
||||||
InternodeOperationMetricDescriptor {
|
InternodeOperationMetricDescriptor {
|
||||||
name: INTERNODE_OPERATION_CLASSIFIED_ERRORS_TOTAL,
|
name: INTERNODE_OPERATION_CLASSIFIED_ERRORS_TOTAL,
|
||||||
labels: OPERATION_BACKEND_CLASSIFICATION_LABELS,
|
labels: OPERATION_BACKEND_CLASSIFICATION_LABELS,
|
||||||
@@ -208,6 +213,12 @@ impl InternodeMetrics {
|
|||||||
counter!(INTERNODE_OPERATION_ERRORS_TOTAL, OPERATION_LABEL => operation, BACKEND_LABEL => backend).increment(1);
|
counter!(INTERNODE_OPERATION_ERRORS_TOTAL, OPERATION_LABEL => operation, BACKEND_LABEL => backend).increment(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn record_duration_for_operation_and_backend(&self, operation: &'static str, backend: &'static str, duration: Duration) {
|
||||||
|
let duration_ms = duration.as_secs_f64() * 1000.0;
|
||||||
|
metrics::histogram!(INTERNODE_OPERATION_DURATION_MS, OPERATION_LABEL => operation, BACKEND_LABEL => backend)
|
||||||
|
.record(duration_ms);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn record_classified_error_for_operation_and_backend(
|
pub fn record_classified_error_for_operation_and_backend(
|
||||||
&self,
|
&self,
|
||||||
operation: &'static str,
|
operation: &'static str,
|
||||||
@@ -382,14 +393,14 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn operation_metric_descriptors_include_backend_and_operation_labels() {
|
fn operation_metric_descriptors_include_backend_and_operation_labels() {
|
||||||
assert_eq!(INTERNODE_OPERATION_METRICS.len(), 9);
|
assert_eq!(INTERNODE_OPERATION_METRICS.len(), 10);
|
||||||
for metric in &INTERNODE_OPERATION_METRICS[..5] {
|
for metric in &INTERNODE_OPERATION_METRICS[..6] {
|
||||||
assert_eq!(metric.labels, &[OPERATION_LABEL, BACKEND_LABEL]);
|
assert_eq!(metric.labels, &[OPERATION_LABEL, BACKEND_LABEL]);
|
||||||
}
|
}
|
||||||
for metric in &INTERNODE_OPERATION_METRICS[5..8] {
|
for metric in &INTERNODE_OPERATION_METRICS[6..9] {
|
||||||
assert_eq!(metric.labels, &[OPERATION_LABEL, BACKEND_LABEL, CLASSIFICATION_LABEL]);
|
assert_eq!(metric.labels, &[OPERATION_LABEL, BACKEND_LABEL, CLASSIFICATION_LABEL]);
|
||||||
}
|
}
|
||||||
assert_eq!(INTERNODE_OPERATION_METRICS[8].labels, &[STAGE_LABEL, DOMINANT_ERROR_LABEL]);
|
assert_eq!(INTERNODE_OPERATION_METRICS[9].labels, &[STAGE_LABEL, DOMINANT_ERROR_LABEL]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -406,18 +417,22 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
INTERNODE_OPERATION_METRICS[5].name,
|
INTERNODE_OPERATION_METRICS[5].name,
|
||||||
"rustfs_system_network_internode_operation_classified_errors_total"
|
"rustfs_system_network_internode_operation_duration_ms"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
INTERNODE_OPERATION_METRICS[6].name,
|
INTERNODE_OPERATION_METRICS[6].name,
|
||||||
"rustfs_system_network_internode_operation_retries_total"
|
"rustfs_system_network_internode_operation_classified_errors_total"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
INTERNODE_OPERATION_METRICS[7].name,
|
INTERNODE_OPERATION_METRICS[7].name,
|
||||||
"rustfs_system_network_internode_operation_retry_successes_total"
|
"rustfs_system_network_internode_operation_retries_total"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
INTERNODE_OPERATION_METRICS[8].name,
|
INTERNODE_OPERATION_METRICS[8].name,
|
||||||
|
"rustfs_system_network_internode_operation_retry_successes_total"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
INTERNODE_OPERATION_METRICS[9].name,
|
||||||
"rustfs_system_storage_erasure_write_quorum_failures_total"
|
"rustfs_system_storage_erasure_write_quorum_failures_total"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ aes-gcm = { workspace = true }
|
|||||||
argon2 = { workspace = true }
|
argon2 = { workspace = true }
|
||||||
chacha20poly1305 = { workspace = true }
|
chacha20poly1305 = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
sha2 = { workspace = true }
|
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
zeroize = { workspace = true, features = ["derive"] }
|
zeroize = { workspace = true, features = ["derive"] }
|
||||||
|
|
||||||
|
|||||||
@@ -60,10 +60,8 @@ quick-xml = { workspace = true, features = ["serialize", "serde-types", "encodin
|
|||||||
tokio = { workspace = true, features = ["test-util"] }
|
tokio = { workspace = true, features = ["test-util"] }
|
||||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||||
axum = { workspace = true }
|
axum = { workspace = true }
|
||||||
rustfs-storage-api = { workspace = true }
|
|
||||||
rustfs-utils = { workspace = true, features = ["path"] }
|
rustfs-utils = { workspace = true, features = ["path"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
time = { workspace = true }
|
|
||||||
criterion = { workspace = true }
|
criterion = { workspace = true }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ rustfs-storage-api.workspace = true
|
|||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
futures-core = { workspace = true }
|
futures-core = { workspace = true }
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
object_store = { workspace = true }
|
|
||||||
pin-project-lite.workspace = true
|
pin-project-lite.workspace = true
|
||||||
s3s.workspace = true
|
s3s.workspace = true
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ use crate::{
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use datafusion::object_store::{
|
||||||
|
Attributes, CopyOptions, Error as o_Error, GetOptions, GetRange, GetResult, GetResultPayload, ListResult, MultipartUpload,
|
||||||
|
ObjectMeta, ObjectStore, PutMultipartOptions, PutOptions, PutPayload, PutResult, Result, path::Path,
|
||||||
|
};
|
||||||
use futures::pin_mut;
|
use futures::pin_mut;
|
||||||
use futures::{Stream, StreamExt, future::ready, stream};
|
use futures::{Stream, StreamExt, future::ready, stream};
|
||||||
use futures_core::stream::BoxStream;
|
use futures_core::stream::BoxStream;
|
||||||
use http::{HeaderMap, HeaderValue, header::HeaderName};
|
use http::{HeaderMap, HeaderValue, header::HeaderName};
|
||||||
use object_store::{
|
|
||||||
Attributes, CopyOptions, Error as o_Error, GetOptions, GetRange, GetResult, GetResultPayload, ListResult, MultipartUpload,
|
|
||||||
ObjectMeta, ObjectStore, PutMultipartOptions, PutOptions, PutPayload, PutResult, Result, path::Path,
|
|
||||||
};
|
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
use rustfs_common::DEFAULT_DELIMITER;
|
use rustfs_common::DEFAULT_DELIMITER;
|
||||||
use rustfs_storage_api::{HTTPRangeSpec, ObjectIO as _, ObjectOperations as _};
|
use rustfs_storage_api::{HTTPRangeSpec, ObjectIO as _, ObjectOperations as _};
|
||||||
@@ -1080,8 +1080,8 @@ mod test {
|
|||||||
scan_range_read_start, scan_range_stream, select_read_headers,
|
scan_range_read_start, scan_range_stream, select_read_headers,
|
||||||
};
|
};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
use datafusion::object_store::{self, GetRange};
|
||||||
use futures::{StreamExt, TryStreamExt, stream};
|
use futures::{StreamExt, TryStreamExt, stream};
|
||||||
use object_store::GetRange;
|
|
||||||
use s3s::dto::{
|
use s3s::dto::{
|
||||||
CSVInput, CSVOutput, ExpressionType, InputSerialization, OutputSerialization, SelectObjectContentInput,
|
CSVInput, CSVOutput, ExpressionType, InputSerialization, OutputSerialization, SelectObjectContentInput,
|
||||||
SelectObjectContentRequest,
|
SelectObjectContentRequest,
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ use datafusion::{
|
|||||||
record_batch::RecordBatch,
|
record_batch::RecordBatch,
|
||||||
},
|
},
|
||||||
execution::{SessionStateBuilder, context::SessionState, runtime_env::RuntimeEnvBuilder},
|
execution::{SessionStateBuilder, context::SessionState, runtime_env::RuntimeEnvBuilder},
|
||||||
|
object_store::{ObjectStore, ObjectStoreExt, memory::InMemory, path::Path},
|
||||||
parquet::arrow::ArrowWriter,
|
parquet::arrow::ArrowWriter,
|
||||||
prelude::SessionContext,
|
prelude::SessionContext,
|
||||||
};
|
};
|
||||||
use object_store::{ObjectStore, ObjectStoreExt, memory::InMemory, path::Path};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ impl SessionCtxFactory {
|
|||||||
QueryError::StoreError { e: e.to_string() }
|
QueryError::StoreError { e: e.to_string() }
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
df_session_state.with_object_store(&store_url, Arc::new(store)).build()
|
df_session_state.with_object_store(&store_url, store).build()
|
||||||
} else {
|
} else {
|
||||||
let store: EcObjectStore =
|
let store: EcObjectStore =
|
||||||
EcObjectStore::new(context.input.clone()).map_err(|_| QueryError::NotImplemented { err: String::new() })?;
|
EcObjectStore::new(context.input.clone()).map_err(|_| QueryError::NotImplemented { err: String::new() })?;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use std::path::Component;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -550,6 +551,52 @@ pub fn trim_etag(etag: &str) -> String {
|
|||||||
etag.trim_matches('"').to_string()
|
etag.trim_matches('"').to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if `path` contains a `..` component (parent directory traversal).
|
||||||
|
fn contains_parent_dir_component(path: &str) -> bool {
|
||||||
|
path.split(['/', '\\']).any(|component| component == "..")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates that an archive entry path does not escape the target bucket.
|
||||||
|
///
|
||||||
|
/// Rejects paths containing `..` components, root prefixes, or device/prefix
|
||||||
|
/// components that would allow path traversal outside the extraction root.
|
||||||
|
///
|
||||||
|
/// Returns `Ok(())` if the path is safe, or `Err(message)` describing the violation.
|
||||||
|
pub fn validate_extract_relative_path(path: &str) -> Result<(), String> {
|
||||||
|
let p = Path::new(path);
|
||||||
|
if p.components()
|
||||||
|
.any(|c| matches!(c, Component::Prefix(_) | Component::RootDir | Component::ParentDir))
|
||||||
|
|| contains_parent_dir_component(p.to_string_lossy().as_ref())
|
||||||
|
{
|
||||||
|
return Err("archive entry path must stay within the target bucket".to_string());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normalizes an archive entry key by applying a prefix, trimming slashes,
|
||||||
|
/// and ensuring directory entries end with `/`.
|
||||||
|
///
|
||||||
|
/// Validates both the raw path and the final key against path traversal.
|
||||||
|
///
|
||||||
|
/// Returns `Ok(normalized_key)` or `Err(message)` if the path is unsafe.
|
||||||
|
pub fn normalize_extract_entry_key(path: &str, prefix: Option<&str>, is_dir: bool) -> Result<String, String> {
|
||||||
|
validate_extract_relative_path(path)?;
|
||||||
|
let path = path.trim_matches('/');
|
||||||
|
let mut key = match prefix {
|
||||||
|
Some(prefix) if !path.is_empty() => format!("{prefix}/{path}"),
|
||||||
|
Some(prefix) => prefix.to_string(),
|
||||||
|
None => path.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_dir && !key.ends_with('/') {
|
||||||
|
key.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_extract_relative_path(&key)?;
|
||||||
|
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -918,4 +965,36 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_extract_relative_path_accepts_safe_paths() {
|
||||||
|
assert!(validate_extract_relative_path("file.txt").is_ok());
|
||||||
|
assert!(validate_extract_relative_path("dir/file.txt").is_ok());
|
||||||
|
assert!(validate_extract_relative_path("a/b/c").is_ok());
|
||||||
|
assert!(validate_extract_relative_path("").is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_extract_relative_path_rejects_traversal() {
|
||||||
|
assert!(validate_extract_relative_path("../escape.txt").is_err());
|
||||||
|
assert!(validate_extract_relative_path("dir/../../../escape.txt").is_err());
|
||||||
|
assert!(validate_extract_relative_path("/absolute/path").is_err());
|
||||||
|
assert!(validate_extract_relative_path("dir/..").is_err());
|
||||||
|
assert!(validate_extract_relative_path("..").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_normalize_extract_entry_key_basic() {
|
||||||
|
assert_eq!(normalize_extract_entry_key("file.txt", None, false).unwrap(), "file.txt");
|
||||||
|
assert_eq!(normalize_extract_entry_key("file.txt", Some("prefix"), false).unwrap(), "prefix/file.txt");
|
||||||
|
assert_eq!(normalize_extract_entry_key("dir", None, true).unwrap(), "dir/");
|
||||||
|
assert_eq!(normalize_extract_entry_key("", Some("prefix"), false).unwrap(), "prefix");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_normalize_extract_entry_key_rejects_traversal() {
|
||||||
|
assert!(normalize_extract_entry_key("../escape.txt", None, false).is_err());
|
||||||
|
assert!(normalize_extract_entry_key("file.txt", Some("../bad"), false).is_err());
|
||||||
|
assert!(normalize_extract_entry_key("/absolute", None, false).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+145
-5349
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,6 @@ cargo-fuzz = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
arbitrary = "1"
|
arbitrary = "1"
|
||||||
libfuzzer-sys = "0.4"
|
libfuzzer-sys = "0.4"
|
||||||
rustfs = { path = "../rustfs", default-features = false }
|
|
||||||
rustfs-ecstore = { path = "../crates/ecstore" }
|
rustfs-ecstore = { path = "../crates/ecstore" }
|
||||||
rustfs-filemeta = { path = "../crates/filemeta" }
|
rustfs-filemeta = { path = "../crates/filemeta" }
|
||||||
rustfs-policy = { path = "../crates/policy" }
|
rustfs-policy = { path = "../crates/policy" }
|
||||||
|
|||||||
+24
-7
@@ -34,7 +34,7 @@ Crash reproducers are written under `fuzz/artifacts/<target>/`.
|
|||||||
## Targets
|
## Targets
|
||||||
|
|
||||||
- `bucket_validation`
|
- `bucket_validation`
|
||||||
- Exercises RustFS bucket-name and bucket/object argument validation without pulling in the full `rustfs` binary crate graph.
|
- Exercises RustFS bucket-name and bucket/object argument validation.
|
||||||
- `archive_extract`
|
- `archive_extract`
|
||||||
- Exercises archive entry path normalization, prefix application, and bucket-namespace containment checks.
|
- Exercises archive entry path normalization, prefix application, and bucket-namespace containment checks.
|
||||||
- `path_containment`
|
- `path_containment`
|
||||||
@@ -53,17 +53,34 @@ cd fuzz
|
|||||||
cargo +nightly fuzz run path_containment
|
cargo +nightly fuzz run path_containment
|
||||||
```
|
```
|
||||||
|
|
||||||
Run bounded smoke targets from the repository root:
|
Use the unified runner script from the repository root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/fuzz/run_ci_targets.sh
|
# Build + run all smoke targets (60s each)
|
||||||
|
./scripts/fuzz/run.sh
|
||||||
|
|
||||||
|
# Build only (no fuzz run)
|
||||||
|
BUILD_ONLY=1 ./scripts/fuzz/run.sh
|
||||||
|
|
||||||
|
# Build + run a single target
|
||||||
|
FUZZ_TARGET=path_containment ./scripts/fuzz/run.sh
|
||||||
|
|
||||||
|
# Nightly-style: 300s per target
|
||||||
|
MAX_TOTAL_TIME=300 ./scripts/fuzz/run.sh
|
||||||
|
|
||||||
|
# Skip build (use pre-built harness)
|
||||||
|
SKIP_BUILD=1 FUZZ_TARGET=local_metadata ./scripts/fuzz/run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Run a longer local/nightly-style pass:
|
## CI Workflow
|
||||||
|
|
||||||
```bash
|
The GitHub Actions workflow (`.github/workflows/fuzz.yml`) uses a **build/run separation** pattern:
|
||||||
./scripts/fuzz/run_nightly_targets.sh
|
|
||||||
```
|
1. **`fuzz-build`** — compiles all fuzz targets once, then uploads only the prebuilt smoke harness binaries needed by later jobs.
|
||||||
|
2. **`pr-fuzz-smoke`** — matrix job that runs each target in parallel (60s each). Downloads the prebuilt binaries and executes them directly, so the job does not need to restore the full `fuzz/target/` tree or reinstall `cargo-fuzz`.
|
||||||
|
3. **`nightly-fuzz-corpus`** — matrix job that reuses the same prebuilt binaries and runs each target in parallel (300s each) on a daily schedule.
|
||||||
|
|
||||||
|
This design avoids redundant compilation across targets and keeps wall-clock time low.
|
||||||
|
|
||||||
## Seed Corpus
|
## Seed Corpus
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use rustfs::app::object_usecase::{normalize_extract_entry_key, validate_extract_relative_path};
|
use rustfs_utils::path::{normalize_extract_entry_key, validate_extract_relative_path};
|
||||||
|
|
||||||
fn parse_case(data: &[u8]) -> (String, Option<String>, bool, Vec<String>) {
|
fn parse_case(data: &[u8]) -> (String, Option<String>, bool, Vec<String>) {
|
||||||
let text = String::from_utf8_lossy(data);
|
let text = String::from_utf8_lossy(data);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use rustfs_filemeta::FileMeta;
|
use rustfs_filemeta::FileMeta;
|
||||||
use rustfs_utils::compress::{CompressionAlgorithm, decompress_block};
|
use rustfs_utils::compress::{CompressionAlgorithm, decompress_block};
|
||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
fn pick_algorithm(tag: u8) -> CompressionAlgorithm {
|
fn pick_algorithm(tag: u8) -> CompressionAlgorithm {
|
||||||
match tag % 6 {
|
match tag % 6 {
|
||||||
@@ -16,26 +15,42 @@ fn pick_algorithm(tag: u8) -> CompressionAlgorithm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exercise_payload(payload: &[u8], algorithm: CompressionAlgorithm) {
|
/// Full exercise: all parsers + decompression on the complete payload.
|
||||||
|
fn exercise_full(payload: &[u8], algorithm: CompressionAlgorithm) {
|
||||||
let _ = FileMeta::load(payload);
|
let _ = FileMeta::load(payload);
|
||||||
let _ = FileMeta::load_or_convert(payload);
|
let _ = FileMeta::load_or_convert(payload);
|
||||||
let _ = FileMeta::read_format_versions(payload);
|
let _ = FileMeta::read_format_versions(payload);
|
||||||
let _ = decompress_block(payload, algorithm);
|
let _ = decompress_block(payload, algorithm);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn interesting_prefix_lengths(len: usize) -> BTreeSet<usize> {
|
/// Lightweight exercise: only metadata parsers on a prefix (no decompression).
|
||||||
let mut lengths = BTreeSet::from([0usize, 1, 2, 4, 5, 8, 16, 32]);
|
/// Prefix tests catch partial-input / truncated-header panics cheaply.
|
||||||
lengths.insert(len);
|
fn exercise_prefix(payload: &[u8]) {
|
||||||
lengths.insert(len.saturating_sub(1));
|
let _ = FileMeta::load(payload);
|
||||||
lengths.into_iter().filter(|candidate| *candidate <= len).collect()
|
let _ = FileMeta::read_format_versions(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
fuzz_target!(|data: &[u8]| {
|
fuzz_target!(|data: &[u8]| {
|
||||||
let algorithm = pick_algorithm(data.first().copied().unwrap_or_default());
|
let algorithm = pick_algorithm(data.first().copied().unwrap_or_default());
|
||||||
|
|
||||||
exercise_payload(data, algorithm);
|
// Full exercise on the complete input.
|
||||||
|
exercise_full(data, algorithm);
|
||||||
|
|
||||||
for prefix_len in interesting_prefix_lengths(data.len()) {
|
// Prefix probing aligned with xl.meta binary layout:
|
||||||
exercise_payload(&data[..prefix_len], algorithm);
|
// 4 = magic header ("XL2 ")
|
||||||
|
// 5 = magic + version byte
|
||||||
|
// 8 = magic + version + start of bin32 length field
|
||||||
|
// 12 = magic + version + bin32 length (4 bytes) + CRC start
|
||||||
|
// len-1 = truncated-at-end
|
||||||
|
//
|
||||||
|
// Only test prefixes that are strictly smaller than the full input
|
||||||
|
// (the full input is already tested above).
|
||||||
|
for prefix_len in [4usize, 5, 8, 12] {
|
||||||
|
if prefix_len < data.len() {
|
||||||
|
exercise_prefix(&data[..prefix_len]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if data.len() > 1 {
|
||||||
|
exercise_prefix(&data[..data.len() - 1]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,12 +14,43 @@
|
|||||||
|
|
||||||
//! Object application use-case contracts.
|
//! Object application use-case contracts.
|
||||||
|
|
||||||
|
use super::object_api_utils::to_s3s_etag;
|
||||||
|
use super::quota::checker::QuotaChecker;
|
||||||
|
use super::storageclass;
|
||||||
|
// Performance metrics recording (with zero-copy-metrics integration)
|
||||||
|
use super::ECStore;
|
||||||
|
use super::{AppReplicationConfigExt as _, AppVersioningConfigExt as _, predict_lifecycle_expiration, validate_restore_request};
|
||||||
|
use super::{DiskError, is_all_buckets_not_found};
|
||||||
|
use super::{DynReader, HashReader, WritePlan, wrap_reader};
|
||||||
|
use super::{Error as EcstoreError, StorageError, is_err_bucket_not_found, is_err_object_not_found, is_err_version_not_found};
|
||||||
|
use super::{MIN_DISK_COMPRESSIBLE_SIZE, is_disk_compressible};
|
||||||
|
use super::{get_lock_acquire_timeout, is_valid_storage_class};
|
||||||
|
use super::{
|
||||||
|
lifecycle::{
|
||||||
|
bucket_lifecycle_audit::LcEventSrc,
|
||||||
|
bucket_lifecycle_ops::{enqueue_transition_immediate, post_restore_opts},
|
||||||
|
lifecycle::{self, TransitionOptions},
|
||||||
|
},
|
||||||
|
metadata_sys,
|
||||||
|
object_lock::{
|
||||||
|
objectlock::{get_object_legalhold_meta, get_object_retention_meta},
|
||||||
|
objectlock_sys::{BucketObjectLockSys, check_object_lock_for_deletion, is_retention_active},
|
||||||
|
},
|
||||||
|
quota::QuotaOperation,
|
||||||
|
replication::{
|
||||||
|
DeletedObjectReplicationInfo, ObjectOpts as ReplicationObjectOpts, check_replicate_delete, get_must_replicate_options,
|
||||||
|
must_replicate, schedule_replication, schedule_replication_delete,
|
||||||
|
},
|
||||||
|
tagging::decode_tags,
|
||||||
|
versioning_sys::BucketVersioningSys,
|
||||||
|
};
|
||||||
use crate::app::context::{
|
use crate::app::context::{
|
||||||
AppContext, get_global_app_context, resolve_notify_interface_for_context, resolve_object_store_handle_for_context,
|
AppContext, get_global_app_context, resolve_notify_interface_for_context, resolve_object_store_handle_for_context,
|
||||||
};
|
};
|
||||||
use crate::config::RustFSBufferConfig;
|
use crate::config::RustFSBufferConfig;
|
||||||
use crate::delete_tail_activity::{DeleteTailActivityGuard, DeleteTailStage};
|
use crate::delete_tail_activity::{DeleteTailActivityGuard, DeleteTailStage};
|
||||||
use crate::error::ApiError;
|
use crate::error::ApiError;
|
||||||
|
use crate::server::convert_ecstore_object_info;
|
||||||
use crate::storage::access::{PostObjectRequestMarker, authorize_request, has_bypass_governance_header, req_info_mut};
|
use crate::storage::access::{PostObjectRequestMarker, authorize_request, has_bypass_governance_header, req_info_mut};
|
||||||
use crate::storage::concurrency::{
|
use crate::storage::concurrency::{
|
||||||
ConcurrencyManager, GetObjectGuard, get_concurrency_aware_buffer_size, get_concurrency_manager,
|
ConcurrencyManager, GetObjectGuard, get_concurrency_aware_buffer_size, get_concurrency_manager,
|
||||||
@@ -46,38 +77,6 @@ use http::{HeaderMap, HeaderValue, StatusCode};
|
|||||||
use md5::Context as Md5Context;
|
use md5::Context as Md5Context;
|
||||||
use metrics::{counter, histogram};
|
use metrics::{counter, histogram};
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
use rustfs_object_capacity::capacity_manager::get_capacity_manager;
|
|
||||||
// Performance metrics recording (with zero-copy-metrics integration)
|
|
||||||
use super::ECStore;
|
|
||||||
use super::object_api_utils::to_s3s_etag;
|
|
||||||
use super::quota::checker::QuotaChecker;
|
|
||||||
use super::storageclass;
|
|
||||||
use super::{AppReplicationConfigExt as _, AppVersioningConfigExt as _, predict_lifecycle_expiration, validate_restore_request};
|
|
||||||
use super::{DiskError, is_all_buckets_not_found};
|
|
||||||
use super::{DynReader, HashReader, WritePlan, wrap_reader};
|
|
||||||
use super::{Error as EcstoreError, StorageError, is_err_bucket_not_found, is_err_object_not_found, is_err_version_not_found};
|
|
||||||
use super::{MIN_DISK_COMPRESSIBLE_SIZE, is_disk_compressible};
|
|
||||||
use super::{get_lock_acquire_timeout, is_valid_storage_class};
|
|
||||||
use super::{
|
|
||||||
lifecycle::{
|
|
||||||
bucket_lifecycle_audit::LcEventSrc,
|
|
||||||
bucket_lifecycle_ops::{enqueue_transition_immediate, post_restore_opts},
|
|
||||||
lifecycle::{self, TransitionOptions},
|
|
||||||
},
|
|
||||||
metadata_sys,
|
|
||||||
object_lock::{
|
|
||||||
objectlock::{get_object_legalhold_meta, get_object_retention_meta},
|
|
||||||
objectlock_sys::{BucketObjectLockSys, check_object_lock_for_deletion, is_retention_active},
|
|
||||||
},
|
|
||||||
quota::QuotaOperation,
|
|
||||||
replication::{
|
|
||||||
DeletedObjectReplicationInfo, ObjectOpts as ReplicationObjectOpts, check_replicate_delete, get_must_replicate_options,
|
|
||||||
must_replicate, schedule_replication, schedule_replication_delete,
|
|
||||||
},
|
|
||||||
tagging::decode_tags,
|
|
||||||
versioning_sys::BucketVersioningSys,
|
|
||||||
};
|
|
||||||
use crate::server::convert_ecstore_object_info;
|
|
||||||
use rustfs_concurrency::GetObjectQueueSnapshot;
|
use rustfs_concurrency::GetObjectQueueSnapshot;
|
||||||
use rustfs_filemeta::{
|
use rustfs_filemeta::{
|
||||||
REPLICATE_INCOMING_DELETE, ReplicateDecision, ReplicateTargetDecision, ReplicationState, ReplicationStatusType,
|
REPLICATE_INCOMING_DELETE, ReplicateDecision, ReplicateTargetDecision, ReplicationState, ReplicationStatusType,
|
||||||
@@ -88,6 +87,7 @@ use rustfs_io_core::{BytesPool, PooledBuffer};
|
|||||||
use rustfs_io_metrics;
|
use rustfs_io_metrics;
|
||||||
use rustfs_lock::NamespaceLockGuard;
|
use rustfs_lock::NamespaceLockGuard;
|
||||||
use rustfs_notify::EventArgsBuilder;
|
use rustfs_notify::EventArgsBuilder;
|
||||||
|
use rustfs_object_capacity::capacity_manager::get_capacity_manager;
|
||||||
use rustfs_policy::policy::action::{Action, S3Action};
|
use rustfs_policy::policy::action::{Action, S3Action};
|
||||||
use rustfs_s3_ops::{S3Operation, delete_event_name_for_marker, put_event_name_for_post_object};
|
use rustfs_s3_ops::{S3Operation, delete_event_name_for_marker, put_event_name_for_post_object};
|
||||||
use rustfs_s3select_api::object_store::bytes_stream;
|
use rustfs_s3select_api::object_store::bytes_stream;
|
||||||
@@ -119,7 +119,7 @@ use s3s::header::{X_AMZ_RESTORE, X_AMZ_RESTORE_OUTPUT_PATH};
|
|||||||
use s3s::{S3Error, S3ErrorCode, S3Request, S3Response, S3Result, s3_error};
|
use s3s::{S3Error, S3ErrorCode, S3Request, S3Response, S3Result, s3_error};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
use std::path::{Component, Path};
|
use std::path::Path;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
@@ -337,14 +337,14 @@ impl MemoryTrackedBytesStream {
|
|||||||
impl futures::Stream for MemoryTrackedBytesStream {
|
impl futures::Stream for MemoryTrackedBytesStream {
|
||||||
type Item = std::io::Result<Bytes>;
|
type Item = std::io::Result<Bytes>;
|
||||||
|
|
||||||
fn poll_next(self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Option<Self::Item>> {
|
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
let this = self.project();
|
let this = self.project();
|
||||||
if *this.emitted {
|
if *this.emitted {
|
||||||
return std::task::Poll::Ready(None);
|
return Poll::Ready(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
*this.emitted = true;
|
*this.emitted = true;
|
||||||
std::task::Poll::Ready(Some(Ok(this.bytes.clone())))
|
Poll::Ready(Some(Ok(this.bytes.clone())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,16 +360,12 @@ impl<R> ExtractArchiveEtagReader<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<R: AsyncRead> AsyncRead for ExtractArchiveEtagReader<R> {
|
impl<R: AsyncRead> AsyncRead for ExtractArchiveEtagReader<R> {
|
||||||
fn poll_read(
|
fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<std::io::Result<()>> {
|
||||||
self: std::pin::Pin<&mut Self>,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
buf: &mut ReadBuf<'_>,
|
|
||||||
) -> std::task::Poll<std::io::Result<()>> {
|
|
||||||
let this = self.project();
|
let this = self.project();
|
||||||
let before = buf.filled().len();
|
let before = buf.filled().len();
|
||||||
match this.inner.poll_read(cx, buf) {
|
match this.inner.poll_read(cx, buf) {
|
||||||
std::task::Poll::Pending => std::task::Poll::Pending,
|
Poll::Pending => Poll::Pending,
|
||||||
std::task::Poll::Ready(Ok(())) => {
|
Poll::Ready(Ok(())) => {
|
||||||
let filled = &buf.filled()[before..];
|
let filled = &buf.filled()[before..];
|
||||||
if !filled.is_empty() {
|
if !filled.is_empty() {
|
||||||
this.md5.consume(filled);
|
this.md5.consume(filled);
|
||||||
@@ -379,9 +375,9 @@ impl<R: AsyncRead> AsyncRead for ExtractArchiveEtagReader<R> {
|
|||||||
*etag = Some(format!("{:x}", this.md5.clone().finalize()));
|
*etag = Some(format!("{:x}", this.md5.clone().finalize()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::task::Poll::Ready(Ok(()))
|
Poll::Ready(Ok(()))
|
||||||
}
|
}
|
||||||
std::task::Poll::Ready(Err(err)) => std::task::Poll::Ready(Err(err)),
|
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -988,21 +984,12 @@ fn snowball_meta_flag(headers: &HeaderMap, exact_keys: &[&str], suffix_lower: &s
|
|||||||
snowball_meta_value(headers, exact_keys, suffix_lower).is_some_and(|value| value.eq_ignore_ascii_case("true"))
|
snowball_meta_value(headers, exact_keys, suffix_lower).is_some_and(|value| value.eq_ignore_ascii_case("true"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn contains_parent_dir_component(path: &str) -> bool {
|
/// Validates that an archive entry path does not escape the target bucket.
|
||||||
path.split(['/', '\\']).any(|component| component == "..")
|
///
|
||||||
}
|
/// Delegates to [`rustfs_utils::path::validate_extract_relative_path`] and wraps
|
||||||
|
/// the result as an S3 error on failure.
|
||||||
pub fn validate_extract_relative_path(path: &str) -> S3Result<()> {
|
pub fn validate_extract_relative_path(path: &str) -> S3Result<()> {
|
||||||
let path = Path::new(path);
|
rustfs_utils::path::validate_extract_relative_path(path).map_err(|msg| s3_error!(InvalidArgument, "{msg}"))
|
||||||
if path
|
|
||||||
.components()
|
|
||||||
.any(|component| matches!(component, Component::Prefix(_) | Component::RootDir | Component::ParentDir))
|
|
||||||
|| contains_parent_dir_component(path.to_string_lossy().as_ref())
|
|
||||||
{
|
|
||||||
return Err(s3_error!(InvalidArgument, "archive entry path must stay within the target bucket"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_snowball_prefix(prefix: &str) -> S3Result<Option<String>> {
|
fn normalize_snowball_prefix(prefix: &str) -> S3Result<Option<String>> {
|
||||||
@@ -1016,22 +1003,13 @@ fn normalize_snowball_prefix(prefix: &str) -> S3Result<Option<String>> {
|
|||||||
Ok(Some(normalized.to_string()))
|
Ok(Some(normalized.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalizes an archive entry key by applying a prefix, trimming slashes,
|
||||||
|
/// and ensuring directory entries end with `/`.
|
||||||
|
///
|
||||||
|
/// Delegates to [`rustfs_utils::path::normalize_extract_entry_key`] and wraps
|
||||||
|
/// the result as an S3 error on failure.
|
||||||
pub fn normalize_extract_entry_key(path: &str, prefix: Option<&str>, is_dir: bool) -> S3Result<String> {
|
pub fn normalize_extract_entry_key(path: &str, prefix: Option<&str>, is_dir: bool) -> S3Result<String> {
|
||||||
validate_extract_relative_path(path)?;
|
rustfs_utils::path::normalize_extract_entry_key(path, prefix, is_dir).map_err(|msg| s3_error!(InvalidArgument, "{msg}"))
|
||||||
let path = path.trim_matches('/');
|
|
||||||
let mut key = match prefix {
|
|
||||||
Some(prefix) if !path.is_empty() => format!("{prefix}/{path}"),
|
|
||||||
Some(prefix) => prefix.to_string(),
|
|
||||||
None => path.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if is_dir && !key.ends_with('/') {
|
|
||||||
key.push('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_extract_relative_path(&key)?;
|
|
||||||
|
|
||||||
Ok(key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_extract_archive_error(err: impl std::fmt::Display) -> S3Error {
|
fn map_extract_archive_error(err: impl std::fmt::Display) -> S3Error {
|
||||||
@@ -1614,7 +1592,7 @@ impl DefaultObjectUsecase {
|
|||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn prepare_get_object_read(
|
async fn prepare_get_object_read(
|
||||||
req: &S3Request<GetObjectInput>,
|
req: &S3Request<GetObjectInput>,
|
||||||
store: &super::ECStore,
|
store: &ECStore,
|
||||||
manager: &ConcurrencyManager,
|
manager: &ConcurrencyManager,
|
||||||
bucket: &str,
|
bucket: &str,
|
||||||
key: &str,
|
key: &str,
|
||||||
@@ -2895,7 +2873,6 @@ impl DefaultObjectUsecase {
|
|||||||
validate_ssec_for_read(&info.user_defined, sse_customer_key.as_ref(), sse_customer_key_md5.as_ref())?;
|
validate_ssec_for_read(&info.user_defined, sse_customer_key.as_ref(), sse_customer_key_md5.as_ref())?;
|
||||||
|
|
||||||
let metadata_map = info.user_defined.clone();
|
let metadata_map = info.user_defined.clone();
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"GetObjectAttributes raw object_attributes={:?}",
|
"GetObjectAttributes raw object_attributes={:?}",
|
||||||
object_attributes.iter().map(|value| value.as_str()).collect::<Vec<_>>()
|
object_attributes.iter().map(|value| value.as_str()).collect::<Vec<_>>()
|
||||||
@@ -2952,7 +2929,6 @@ impl DefaultObjectUsecase {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let object_parts = if requested(ObjectAttributes::OBJECT_PARTS) && info.is_multipart() {
|
let object_parts = if requested(ObjectAttributes::OBJECT_PARTS) && info.is_multipart() {
|
||||||
let params = parse_list_parts_params(part_number_marker, max_parts)?;
|
let params = parse_list_parts_params(part_number_marker, max_parts)?;
|
||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
|
|||||||
+475
-202
@@ -31,11 +31,13 @@ use crate::server::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use crate::storage;
|
use crate::storage;
|
||||||
|
use crate::storage::request_context::{RequestContext, extract_request_id_from_headers};
|
||||||
use crate::storage::rpc::InternodeRpcService;
|
use crate::storage::rpc::InternodeRpcService;
|
||||||
use crate::storage::tonic_service::make_server;
|
use crate::storage::tonic_service::make_server;
|
||||||
use crate::storage::{TONIC_RPC_PREFIX, verify_rpc_signature};
|
use crate::storage::{TONIC_RPC_PREFIX, verify_rpc_signature};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use http::{HeaderMap, Method, Request as HttpRequest, Response};
|
use http::{HeaderMap, Method, Request as HttpRequest, Response};
|
||||||
|
use hyper::body::Incoming;
|
||||||
use hyper_util::{
|
use hyper_util::{
|
||||||
rt::{TokioExecutor, TokioIo, TokioTimer},
|
rt::{TokioExecutor, TokioIo, TokioTimer},
|
||||||
server::conn::auto::Builder as ConnBuilder,
|
server::conn::auto::Builder as ConnBuilder,
|
||||||
@@ -56,12 +58,14 @@ use s3s::{host::MultiDomain, service::S3Service, service::S3ServiceBuilder};
|
|||||||
use socket2::{SockRef, TcpKeepalive};
|
use socket2::{SockRef, TcpKeepalive};
|
||||||
use std::io::{Error, Result};
|
use std::io::{Error, Result};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::task::{Context, Poll};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tonic::{Request, Status};
|
use tonic::{Request, Status};
|
||||||
use tower::ServiceBuilder;
|
use tower::{Service, ServiceBuilder};
|
||||||
use tower_http::add_extension::AddExtensionLayer;
|
use tower_http::add_extension::AddExtensionLayer;
|
||||||
use tower_http::catch_panic::CatchPanicLayer;
|
use tower_http::catch_panic::CatchPanicLayer;
|
||||||
use tower_http::compression::CompressionLayer;
|
use tower_http::compression::CompressionLayer;
|
||||||
@@ -220,6 +224,34 @@ pub(crate) fn active_http_requests() -> u64 {
|
|||||||
ACTIVE_HTTP_REQUESTS.load(Ordering::Relaxed)
|
ACTIVE_HTTP_REQUESTS.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn trace_on_response<ResBody>(response: &Response<ResBody>, latency: Duration, span: &Span) {
|
||||||
|
span.record("status_code", tracing::field::display(response.status()));
|
||||||
|
let _enter = span.enter();
|
||||||
|
let status_class = status_class_label(response.status());
|
||||||
|
record_active_http_requests(-1);
|
||||||
|
histogram!(
|
||||||
|
METRIC_HTTP_SERVER_REQUEST_DURATION_SECONDS,
|
||||||
|
LABEL_HTTP_STATUS_CLASS => status_class
|
||||||
|
)
|
||||||
|
.record(latency.as_secs_f64());
|
||||||
|
if response.status().is_client_error() || response.status().is_server_error() {
|
||||||
|
counter!(
|
||||||
|
METRIC_HTTP_SERVER_FAILURES_TOTAL,
|
||||||
|
LABEL_HTTP_STATUS_CLASS => status_class
|
||||||
|
)
|
||||||
|
.increment(1);
|
||||||
|
}
|
||||||
|
if let Some(cl) = response.headers().get("content-length")
|
||||||
|
&& let Some(len) = cl.to_str().ok().and_then(|s| s.parse::<u64>().ok())
|
||||||
|
{
|
||||||
|
histogram!(
|
||||||
|
METRIC_HTTP_SERVER_RESPONSE_BODY_SIZE_BYTES,
|
||||||
|
LABEL_HTTP_STATUS_CLASS => status_class
|
||||||
|
)
|
||||||
|
.record(len as f64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn start_http_server(config: &config::Config, readiness: Arc<GlobalReadiness>) -> Result<(ShutdownHandle, SocketAddr)> {
|
pub async fn start_http_server(config: &config::Config, readiness: Arc<GlobalReadiness>) -> Result<(ShutdownHandle, SocketAddr)> {
|
||||||
let server_addr = parse_and_resolve_address(config.address.as_str()).map_err(Error::other)?;
|
let server_addr = parse_and_resolve_address(config.address.as_str()).map_err(Error::other)?;
|
||||||
|
|
||||||
@@ -800,6 +832,90 @@ struct ConnectionContext {
|
|||||||
trusted_proxy_layer: Option<rustfs_trusted_proxies::TrustedProxyLayer>,
|
trusted_proxy_layer: Option<rustfs_trusted_proxies::TrustedProxyLayer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PathDispatchService<A, B> {
|
||||||
|
external: A,
|
||||||
|
internode: B,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct InternodeRequestContextLiteLayer;
|
||||||
|
|
||||||
|
impl<S> tower::Layer<S> for InternodeRequestContextLiteLayer {
|
||||||
|
type Service = InternodeRequestContextLiteService<S>;
|
||||||
|
|
||||||
|
fn layer(&self, inner: S) -> Self::Service {
|
||||||
|
InternodeRequestContextLiteService { inner }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct InternodeRequestContextLiteService<S> {
|
||||||
|
inner: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, B> Service<HttpRequest<B>> for InternodeRequestContextLiteService<S>
|
||||||
|
where
|
||||||
|
S: Service<HttpRequest<B>> + Clone,
|
||||||
|
{
|
||||||
|
type Response = S::Response;
|
||||||
|
type Error = S::Error;
|
||||||
|
type Future = S::Future;
|
||||||
|
|
||||||
|
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
|
||||||
|
self.inner.poll_ready(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call(&mut self, mut req: HttpRequest<B>) -> Self::Future {
|
||||||
|
let request_id = extract_request_id_from_headers(req.headers());
|
||||||
|
req.extensions_mut().insert(RequestContext {
|
||||||
|
x_amz_request_id: request_id.clone(),
|
||||||
|
request_id,
|
||||||
|
trace_id: None,
|
||||||
|
span_id: None,
|
||||||
|
start_time: std::time::Instant::now(),
|
||||||
|
});
|
||||||
|
self.inner.call(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A, B> PathDispatchService<A, B> {
|
||||||
|
fn new(external: A, internode: B) -> Self {
|
||||||
|
Self { external, internode }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_internode_path(path: &str) -> bool {
|
||||||
|
crate::server::has_path_prefix(path, crate::server::RPC_PREFIX)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A, B> Service<HttpRequest<Incoming>> for PathDispatchService<A, B>
|
||||||
|
where
|
||||||
|
A: Service<HttpRequest<Incoming>> + Clone + Send + 'static,
|
||||||
|
A::Future: Send + 'static,
|
||||||
|
B: Service<HttpRequest<Incoming>, Response = A::Response, Error = A::Error> + Clone + Send + 'static,
|
||||||
|
B::Future: Send + 'static,
|
||||||
|
{
|
||||||
|
type Response = A::Response;
|
||||||
|
type Error = A::Error;
|
||||||
|
type Future = Pin<Box<dyn Future<Output = std::result::Result<Self::Response, Self::Error>> + Send>>;
|
||||||
|
|
||||||
|
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
|
||||||
|
match self.external.poll_ready(cx)? {
|
||||||
|
Poll::Ready(()) => self.internode.poll_ready(cx),
|
||||||
|
Poll::Pending => Poll::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call(&mut self, req: HttpRequest<Incoming>) -> Self::Future {
|
||||||
|
if Self::is_internode_path(req.uri().path()) {
|
||||||
|
Box::pin(self.internode.call(req))
|
||||||
|
} else {
|
||||||
|
Box::pin(self.external.call(req))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Adapter that implements the OpenTelemetry [`Extractor`] trait for Hyper's
|
/// Adapter that implements the OpenTelemetry [`Extractor`] trait for Hyper's
|
||||||
/// [`HeaderMap`], enabling trace context propagation by extracting
|
/// [`HeaderMap`], enabling trace context propagation by extracting
|
||||||
/// OpenTelemetry headers from incoming HTTP requests.
|
/// OpenTelemetry headers from incoming HTTP requests.
|
||||||
@@ -912,7 +1028,8 @@ fn process_connection(
|
|||||||
let http_service = s3_service;
|
let http_service = s3_service;
|
||||||
let http_service = InternodeRpcService::new(http_service);
|
let http_service = InternodeRpcService::new(http_service);
|
||||||
|
|
||||||
let service = hybrid(http_service, rpc_service);
|
let external_service = hybrid(http_service.clone(), rpc_service.clone());
|
||||||
|
let internode_service = hybrid(http_service, rpc_service);
|
||||||
|
|
||||||
let remote_addr = match socket.peer_addr() {
|
let remote_addr = match socket.peer_addr() {
|
||||||
Ok(addr) => Some(RemoteAddr(addr)),
|
Ok(addr) => Some(RemoteAddr(addr)),
|
||||||
@@ -954,217 +1071,325 @@ fn process_connection(
|
|||||||
// 21. PublicHealthEndpointLayer — handles public health before s3s host parsing
|
// 21. PublicHealthEndpointLayer — handles public health before s3s host parsing
|
||||||
// 22. VirtualHostStyleHintLayer — actionable error for unroutable virtual-hosted-style (conditional)
|
// 22. VirtualHostStyleHintLayer — actionable error for unroutable virtual-hosted-style (conditional)
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
let hybrid_service = ServiceBuilder::new()
|
// Batch 1 intentionally keeps the external and internode stacks behaviorally
|
||||||
// NOTE: Both extension types are intentionally inserted to maintain compatibility:
|
// identical while giving each path family a named construction boundary.
|
||||||
// 1. `Option<RemoteAddr>` - Used by existing admin/storage handlers throughout the codebase
|
// Later batches will trim internode-only middleware without risking drift in
|
||||||
// 2. `std::net::SocketAddr` - Required by TrustedProxyMiddleware for proxy validation
|
// the public HTTP stack.
|
||||||
// This dual insertion is necessary because the middleware expects the raw SocketAddr type
|
let build_external_stack = |service| {
|
||||||
// while our application code uses the RemoteAddr wrapper. Consolidating these would
|
ServiceBuilder::new()
|
||||||
// require either modifying the third-party middleware or refactoring all existing handlers.
|
// NOTE: Both extension types are intentionally inserted to maintain compatibility:
|
||||||
.layer(AddExtensionLayer::new(remote_addr))
|
// 1. `Option<RemoteAddr>` - Used by existing admin/storage handlers throughout the codebase
|
||||||
.option_layer(remote_addr.map(|ra| AddExtensionLayer::new(ra.0)))
|
// 2. `std::net::SocketAddr` - Required by TrustedProxyMiddleware for proxy validation
|
||||||
// Add TrustedProxyLayer to handle X-Forwarded-For and other proxy headers
|
// This dual insertion is necessary because the middleware expects the raw SocketAddr type
|
||||||
// This should be placed before TraceLayer so that logs reflect the real client IP
|
// while our application code uses the RemoteAddr wrapper. Consolidating these would
|
||||||
// Pre-computed in ConnectionContext to avoid per-connection is_enabled() check.
|
// require either modifying the third-party middleware or refactoring all existing handlers.
|
||||||
.option_layer(trusted_proxy_layer)
|
.layer(AddExtensionLayer::new(remote_addr))
|
||||||
.layer(SetRequestIdLayer::x_request_id(MakeRequestUuid))
|
.option_layer(remote_addr.map(|ra| AddExtensionLayer::new(ra.0)))
|
||||||
.layer(RequestContextLayer)
|
// Add TrustedProxyLayer to handle X-Forwarded-For and other proxy headers
|
||||||
.layer(EmptyBodyContentLengthCompatLayer)
|
// This should be placed before TraceLayer so that logs reflect the real client IP
|
||||||
.layer(CatchPanicLayer::new())
|
// Pre-computed in ConnectionContext to avoid per-connection is_enabled() check.
|
||||||
// CRITICAL: Insert ReadinessGateLayer before business logic
|
.option_layer(trusted_proxy_layer.clone())
|
||||||
// This stops requests from hitting IAMAuth or Storage if they are not ready.
|
.layer(SetRequestIdLayer::x_request_id(MakeRequestUuid))
|
||||||
.layer(ReadinessGateLayer::new(readiness))
|
.layer(InternodeRequestContextLiteLayer)
|
||||||
// Add Keystone authentication middleware
|
.layer(EmptyBodyContentLengthCompatLayer)
|
||||||
// This validates X-Auth-Token headers and stores credentials in task-local storage
|
.layer(CatchPanicLayer::new())
|
||||||
// Must be placed AFTER ReadinessGateLayer but BEFORE business logic
|
// CRITICAL: Insert ReadinessGateLayer before business logic
|
||||||
// Pre-computed in ConnectionContext to avoid per-connection OnceLock read.
|
// This stops requests from hitting IAMAuth or Storage if they are not ready.
|
||||||
.layer(KeystoneAuthLayer::new(keystone_auth))
|
.layer(ReadinessGateLayer::new(readiness.clone()))
|
||||||
.layer(
|
// Add Keystone authentication middleware
|
||||||
TraceLayer::new_for_http()
|
// This validates X-Auth-Token headers and stores credentials in task-local storage
|
||||||
.make_span_with(|request: &HttpRequest<_>| {
|
// Must be placed AFTER ReadinessGateLayer but BEFORE business logic
|
||||||
let request_context = request.extensions().get::<crate::storage::request_context::RequestContext>();
|
// Pre-computed in ConnectionContext to avoid per-connection OnceLock read.
|
||||||
let request_id = request_context
|
.layer(KeystoneAuthLayer::new(keystone_auth.clone()))
|
||||||
.map(|ctx| ctx.request_id.as_str())
|
.layer(
|
||||||
.unwrap_or("unknown");
|
TraceLayer::new_for_http()
|
||||||
let trace_id = request_context
|
.make_span_with(|request: &HttpRequest<_>| {
|
||||||
.and_then(|ctx| ctx.trace_id.as_deref())
|
let request_context = request.extensions().get::<crate::storage::request_context::RequestContext>();
|
||||||
.unwrap_or("unknown");
|
let request_id = request_context
|
||||||
let span_id = request_context
|
.map(|ctx| ctx.request_id.as_str())
|
||||||
.and_then(|ctx| ctx.span_id.as_deref())
|
.unwrap_or("unknown");
|
||||||
.unwrap_or("unknown");
|
let trace_id = request_context
|
||||||
|
.and_then(|ctx| ctx.trace_id.as_deref())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
let span_id = request_context
|
||||||
|
.and_then(|ctx| ctx.span_id.as_deref())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
|
||||||
let parent_context = global::get_text_map_propagator(|propagator| {
|
let parent_context = global::get_text_map_propagator(|propagator| {
|
||||||
propagator.extract(&HeaderMapCarrier::new(request.headers()))
|
propagator.extract(&HeaderMapCarrier::new(request.headers()))
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log trace context extraction for debugging distributed tracing
|
if parent_context.has_active_span() {
|
||||||
if parent_context.has_active_span() {
|
let span_ref = parent_context.span();
|
||||||
let span_ref = parent_context.span();
|
trace!(
|
||||||
trace!(
|
otel_trace_id = %span_ref.span_context().trace_id(),
|
||||||
otel_trace_id = %span_ref.span_context().trace_id(),
|
otel_parent_span_id = %span_ref.span_context().span_id(),
|
||||||
otel_parent_span_id = %span_ref.span_context().span_id(),
|
sampled = span_ref.span_context().is_sampled(),
|
||||||
sampled = span_ref.span_context().is_sampled(),
|
"Extracted trace context from incoming request headers"
|
||||||
"Extracted trace context from incoming request headers"
|
);
|
||||||
);
|
} else {
|
||||||
} else {
|
trace!("No trace context found in request headers, will create root span");
|
||||||
trace!("No trace context found in request headers, will create root span");
|
|
||||||
}
|
|
||||||
// Extract real client IP from trusted proxy middleware if available
|
|
||||||
let client_info = request.extensions().get::<ClientInfo>();
|
|
||||||
let peer_addr = client_info
|
|
||||||
.map(|info| info.real_ip.to_string())
|
|
||||||
.or_else(|| request.extensions().get::<RemoteAddr>().map(|addr| addr.0.to_string()))
|
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
|
||||||
|
|
||||||
let span = tracing::info_span!("http-request",
|
|
||||||
request_id = %request_id,
|
|
||||||
trace_id = %trace_id,
|
|
||||||
span_id = %span_id,
|
|
||||||
status_code = tracing::field::Empty,
|
|
||||||
method = %request.method(),
|
|
||||||
peer_addr = %peer_addr,
|
|
||||||
uri = %redact_sensitive_uri_query(request.uri()),
|
|
||||||
version = ?request.version(),
|
|
||||||
user_agent = tracing::field::Empty,
|
|
||||||
content_type = tracing::field::Empty,
|
|
||||||
content_length = tracing::field::Empty,
|
|
||||||
);
|
|
||||||
if span.is_disabled() {
|
|
||||||
return span;
|
|
||||||
}
|
|
||||||
if let Err(e) = span.set_parent(parent_context) {
|
|
||||||
debug!(component = LOG_COMPONENT_SERVER, subsystem = LOG_SUBSYSTEM_HTTP, error = ?e, "Failed to propagate tracing context");
|
|
||||||
}
|
|
||||||
for (header_name, header_value) in request.headers() {
|
|
||||||
let value = header_value.to_str().unwrap_or("invalid");
|
|
||||||
if header_name == "user-agent" {
|
|
||||||
span.record("user_agent", value);
|
|
||||||
} else if header_name == "content-type" {
|
|
||||||
span.record("content_type", value);
|
|
||||||
} else if header_name == "content-length" {
|
|
||||||
span.record("content_length", value);
|
|
||||||
}
|
}
|
||||||
}
|
let client_info = request.extensions().get::<ClientInfo>();
|
||||||
|
let peer_addr = client_info
|
||||||
|
.map(|info| info.real_ip.to_string())
|
||||||
|
.or_else(|| request.extensions().get::<RemoteAddr>().map(|addr| addr.0.to_string()))
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
span
|
let span = tracing::info_span!("http-request",
|
||||||
})
|
request_id = %request_id,
|
||||||
.on_request(|request: &HttpRequest<_>, span: &Span| {
|
trace_id = %trace_id,
|
||||||
let _enter = span.enter();
|
span_id = %span_id,
|
||||||
trace!("HTTP request started");
|
status_code = tracing::field::Empty,
|
||||||
let method = request_method_label(request.method());
|
method = %request.method(),
|
||||||
record_active_http_requests(1);
|
peer_addr = %peer_addr,
|
||||||
counter!(
|
uri = %redact_sensitive_uri_query(request.uri()),
|
||||||
METRIC_HTTP_SERVER_REQUESTS_TOTAL,
|
version = ?request.version(),
|
||||||
LABEL_HTTP_METHOD => method
|
user_agent = tracing::field::Empty,
|
||||||
)
|
content_type = tracing::field::Empty,
|
||||||
.increment(1);
|
content_length = tracing::field::Empty,
|
||||||
|
);
|
||||||
|
if span.is_disabled() {
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
if let Err(e) = span.set_parent(parent_context) {
|
||||||
|
debug!(component = LOG_COMPONENT_SERVER, subsystem = LOG_SUBSYSTEM_HTTP, error = ?e, "Failed to propagate tracing context");
|
||||||
|
}
|
||||||
|
for (header_name, header_value) in request.headers() {
|
||||||
|
let value = header_value.to_str().unwrap_or("invalid");
|
||||||
|
if header_name == "user-agent" {
|
||||||
|
span.record("user_agent", value);
|
||||||
|
} else if header_name == "content-type" {
|
||||||
|
span.record("content_type", value);
|
||||||
|
} else if header_name == "content-length" {
|
||||||
|
span.record("content_length", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(cl) = request.headers().get("content-length")
|
span
|
||||||
&& let Some(len) = cl.to_str().ok().and_then(|s| s.parse::<u64>().ok())
|
})
|
||||||
{
|
.on_request(|request: &HttpRequest<_>, span: &Span| {
|
||||||
counter!(METRIC_HTTP_SERVER_REQUEST_BODY_BYTES_TOTAL).increment(len);
|
let _enter = span.enter();
|
||||||
histogram!(
|
trace!("HTTP request started");
|
||||||
METRIC_HTTP_SERVER_REQUEST_BODY_SIZE_BYTES,
|
let method = request_method_label(request.method());
|
||||||
|
record_active_http_requests(1);
|
||||||
|
counter!(
|
||||||
|
METRIC_HTTP_SERVER_REQUESTS_TOTAL,
|
||||||
LABEL_HTTP_METHOD => method
|
LABEL_HTTP_METHOD => method
|
||||||
)
|
)
|
||||||
.record(len as f64);
|
.increment(1);
|
||||||
}
|
|
||||||
})
|
if let Some(cl) = request.headers().get("content-length")
|
||||||
.on_response(|response: &Response<_>, latency: Duration, span: &Span| {
|
&& let Some(len) = cl.to_str().ok().and_then(|s| s.parse::<u64>().ok())
|
||||||
span.record("status_code", tracing::field::display(response.status()));
|
{
|
||||||
let _enter = span.enter();
|
counter!(METRIC_HTTP_SERVER_REQUEST_BODY_BYTES_TOTAL).increment(len);
|
||||||
let status_class = status_class_label(response.status());
|
histogram!(
|
||||||
record_active_http_requests(-1);
|
METRIC_HTTP_SERVER_REQUEST_BODY_SIZE_BYTES,
|
||||||
histogram!(
|
LABEL_HTTP_METHOD => method
|
||||||
METRIC_HTTP_SERVER_REQUEST_DURATION_SECONDS,
|
)
|
||||||
LABEL_HTTP_STATUS_CLASS => status_class
|
.record(len as f64);
|
||||||
)
|
}
|
||||||
.record(latency.as_secs_f64());
|
})
|
||||||
if response.status().is_client_error() || response.status().is_server_error() {
|
.on_response(trace_on_response)
|
||||||
|
.on_body_chunk(|chunk: &Bytes, latency: Duration, span: &Span| {
|
||||||
|
counter!(METRIC_HTTP_SERVER_RESPONSE_BODY_BYTES_TOTAL).increment(chunk.len() as u64);
|
||||||
|
#[cfg(feature = "tracing-chunk-debug")]
|
||||||
|
{
|
||||||
|
let _enter = span.enter();
|
||||||
|
debug!(chunk_bytes = chunk.len(), duration_ms = duration_ms(latency), "HTTP response body chunk sent");
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "tracing-chunk-debug"))]
|
||||||
|
{
|
||||||
|
let _ = (latency, span);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_eos(|_trailers: Option<&HeaderMap>, stream_duration: Duration, span: &Span| {
|
||||||
|
#[cfg(feature = "tracing-chunk-debug")]
|
||||||
|
{
|
||||||
|
let _enter = span.enter();
|
||||||
|
debug!(duration_ms = duration_ms(stream_duration), "HTTP response stream closed");
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "tracing-chunk-debug"))]
|
||||||
|
{
|
||||||
|
let _ = (_trailers, stream_duration, span);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_failure(|error, latency: Duration, span: &Span| {
|
||||||
|
let _enter = span.enter();
|
||||||
|
record_active_http_requests(-1);
|
||||||
counter!(
|
counter!(
|
||||||
METRIC_HTTP_SERVER_FAILURES_TOTAL,
|
METRIC_HTTP_SERVER_FAILURES_TOTAL,
|
||||||
LABEL_HTTP_STATUS_CLASS => status_class
|
LABEL_HTTP_STATUS_CLASS => "transport"
|
||||||
)
|
)
|
||||||
.increment(1);
|
.increment(1);
|
||||||
}
|
trace!(error = ?error, duration_ms = duration_ms(latency), "HTTP request failure captured by trace layer");
|
||||||
if let Some(cl) = response.headers().get("content-length")
|
}),
|
||||||
&& let Some(len) = cl.to_str().ok().and_then(|s| s.parse::<u64>().ok())
|
)
|
||||||
{
|
.layer(RequestLoggingLayer)
|
||||||
histogram!(
|
.layer(PropagateRequestIdLayer::x_request_id())
|
||||||
METRIC_HTTP_SERVER_RESPONSE_BODY_SIZE_BYTES,
|
.layer(CompressionLayer::new().compress_when(PathAwareHttpCompressionPredicate::new(compression_config.clone())))
|
||||||
LABEL_HTTP_STATUS_CLASS => status_class
|
.layer(PathCategoryInjectionLayer)
|
||||||
|
.layer(S3ErrorMessageCompatLayer)
|
||||||
|
.layer(ObjectAttributesEtagFixLayer)
|
||||||
|
.layer(ConditionalCorsLayer::new())
|
||||||
|
.option_layer(if is_console { Some(RedirectLayer) } else { None })
|
||||||
|
.layer(BodylessStatusFixLayer)
|
||||||
|
.layer(HeadRequestBodyFixLayer)
|
||||||
|
.layer(PublicHealthEndpointLayer)
|
||||||
|
.option_layer((!server_domains_configured && !is_console).then_some(VirtualHostStyleHintLayer))
|
||||||
|
.service(service)
|
||||||
|
};
|
||||||
|
let build_internode_stack = |service| {
|
||||||
|
ServiceBuilder::new()
|
||||||
|
.layer(AddExtensionLayer::new(remote_addr))
|
||||||
|
.option_layer(remote_addr.map(|ra| AddExtensionLayer::new(ra.0)))
|
||||||
|
.option_layer(trusted_proxy_layer.clone())
|
||||||
|
.layer(SetRequestIdLayer::x_request_id(MakeRequestUuid))
|
||||||
|
.layer(RequestContextLayer)
|
||||||
|
.layer(EmptyBodyContentLengthCompatLayer)
|
||||||
|
.layer(CatchPanicLayer::new())
|
||||||
|
.layer(ReadinessGateLayer::new(readiness.clone()))
|
||||||
|
.layer(KeystoneAuthLayer::new(keystone_auth.clone()))
|
||||||
|
.layer(
|
||||||
|
TraceLayer::new_for_http()
|
||||||
|
.make_span_with(|request: &HttpRequest<_>| {
|
||||||
|
let request_context = request.extensions().get::<crate::storage::request_context::RequestContext>();
|
||||||
|
let request_id = request_context
|
||||||
|
.map(|ctx| ctx.request_id.as_str())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
let trace_id = request_context
|
||||||
|
.and_then(|ctx| ctx.trace_id.as_deref())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
let span_id = request_context
|
||||||
|
.and_then(|ctx| ctx.span_id.as_deref())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
|
||||||
|
let parent_context = global::get_text_map_propagator(|propagator| {
|
||||||
|
propagator.extract(&HeaderMapCarrier::new(request.headers()))
|
||||||
|
});
|
||||||
|
|
||||||
|
if parent_context.has_active_span() {
|
||||||
|
let span_ref = parent_context.span();
|
||||||
|
trace!(
|
||||||
|
otel_trace_id = %span_ref.span_context().trace_id(),
|
||||||
|
otel_parent_span_id = %span_ref.span_context().span_id(),
|
||||||
|
sampled = span_ref.span_context().is_sampled(),
|
||||||
|
"Extracted trace context from incoming request headers"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
trace!("No trace context found in request headers, will create root span");
|
||||||
|
}
|
||||||
|
let client_info = request.extensions().get::<ClientInfo>();
|
||||||
|
let peer_addr = client_info
|
||||||
|
.map(|info| info.real_ip.to_string())
|
||||||
|
.or_else(|| request.extensions().get::<RemoteAddr>().map(|addr| addr.0.to_string()))
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
|
let span = tracing::info_span!("http-request",
|
||||||
|
request_id = %request_id,
|
||||||
|
trace_id = %trace_id,
|
||||||
|
span_id = %span_id,
|
||||||
|
status_code = tracing::field::Empty,
|
||||||
|
method = %request.method(),
|
||||||
|
peer_addr = %peer_addr,
|
||||||
|
uri = %redact_sensitive_uri_query(request.uri()),
|
||||||
|
version = ?request.version(),
|
||||||
|
user_agent = tracing::field::Empty,
|
||||||
|
content_type = tracing::field::Empty,
|
||||||
|
content_length = tracing::field::Empty,
|
||||||
|
);
|
||||||
|
if span.is_disabled() {
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
if let Err(e) = span.set_parent(parent_context) {
|
||||||
|
debug!(component = LOG_COMPONENT_SERVER, subsystem = LOG_SUBSYSTEM_HTTP, error = ?e, "Failed to propagate tracing context");
|
||||||
|
}
|
||||||
|
for (header_name, header_value) in request.headers() {
|
||||||
|
let value = header_value.to_str().unwrap_or("invalid");
|
||||||
|
if header_name == "user-agent" {
|
||||||
|
span.record("user_agent", value);
|
||||||
|
} else if header_name == "content-type" {
|
||||||
|
span.record("content_type", value);
|
||||||
|
} else if header_name == "content-length" {
|
||||||
|
span.record("content_length", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span
|
||||||
|
})
|
||||||
|
.on_request(|request: &HttpRequest<_>, span: &Span| {
|
||||||
|
let _enter = span.enter();
|
||||||
|
trace!("HTTP request started");
|
||||||
|
let method = request_method_label(request.method());
|
||||||
|
record_active_http_requests(1);
|
||||||
|
counter!(
|
||||||
|
METRIC_HTTP_SERVER_REQUESTS_TOTAL,
|
||||||
|
LABEL_HTTP_METHOD => method
|
||||||
)
|
)
|
||||||
.record(len as f64);
|
.increment(1);
|
||||||
}
|
|
||||||
})
|
if let Some(cl) = request.headers().get("content-length")
|
||||||
.on_body_chunk(|chunk: &Bytes, latency: Duration, span: &Span| {
|
&& let Some(len) = cl.to_str().ok().and_then(|s| s.parse::<u64>().ok())
|
||||||
counter!(METRIC_HTTP_SERVER_RESPONSE_BODY_BYTES_TOTAL).increment(chunk.len() as u64);
|
{
|
||||||
#[cfg(feature = "tracing-chunk-debug")]
|
counter!(METRIC_HTTP_SERVER_REQUEST_BODY_BYTES_TOTAL).increment(len);
|
||||||
{
|
histogram!(
|
||||||
|
METRIC_HTTP_SERVER_REQUEST_BODY_SIZE_BYTES,
|
||||||
|
LABEL_HTTP_METHOD => method
|
||||||
|
)
|
||||||
|
.record(len as f64);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_response(trace_on_response)
|
||||||
|
.on_body_chunk(|chunk: &Bytes, latency: Duration, span: &Span| {
|
||||||
|
counter!(METRIC_HTTP_SERVER_RESPONSE_BODY_BYTES_TOTAL).increment(chunk.len() as u64);
|
||||||
|
#[cfg(feature = "tracing-chunk-debug")]
|
||||||
|
{
|
||||||
|
let _enter = span.enter();
|
||||||
|
debug!(chunk_bytes = chunk.len(), duration_ms = duration_ms(latency), "HTTP response body chunk sent");
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "tracing-chunk-debug"))]
|
||||||
|
{
|
||||||
|
let _ = (latency, span);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_eos(|_trailers: Option<&HeaderMap>, stream_duration: Duration, span: &Span| {
|
||||||
|
#[cfg(feature = "tracing-chunk-debug")]
|
||||||
|
{
|
||||||
|
let _enter = span.enter();
|
||||||
|
debug!(duration_ms = duration_ms(stream_duration), "HTTP response stream closed");
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "tracing-chunk-debug"))]
|
||||||
|
{
|
||||||
|
let _ = (_trailers, stream_duration, span);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_failure(|error, latency: Duration, span: &Span| {
|
||||||
let _enter = span.enter();
|
let _enter = span.enter();
|
||||||
debug!(chunk_bytes = chunk.len(), duration_ms = duration_ms(latency), "HTTP response body chunk sent");
|
record_active_http_requests(-1);
|
||||||
}
|
counter!(
|
||||||
#[cfg(not(feature = "tracing-chunk-debug"))]
|
METRIC_HTTP_SERVER_FAILURES_TOTAL,
|
||||||
{
|
LABEL_HTTP_STATUS_CLASS => "transport"
|
||||||
let _ = (latency, span);
|
)
|
||||||
}
|
.increment(1);
|
||||||
})
|
trace!(error = ?error, duration_ms = duration_ms(latency), "HTTP request failure captured by trace layer");
|
||||||
.on_eos(|_trailers: Option<&HeaderMap>, stream_duration: Duration, span: &Span| {
|
}),
|
||||||
#[cfg(feature = "tracing-chunk-debug")]
|
)
|
||||||
{
|
.layer(PropagateRequestIdLayer::x_request_id())
|
||||||
let _enter = span.enter();
|
.layer(CompressionLayer::new().compress_when(PathAwareHttpCompressionPredicate::new(compression_config.clone())))
|
||||||
debug!(duration_ms = duration_ms(stream_duration), "HTTP response stream closed");
|
.layer(PathCategoryInjectionLayer)
|
||||||
}
|
.layer(S3ErrorMessageCompatLayer)
|
||||||
#[cfg(not(feature = "tracing-chunk-debug"))]
|
.layer(ObjectAttributesEtagFixLayer)
|
||||||
{
|
.layer(ConditionalCorsLayer::new())
|
||||||
let _ = (_trailers, stream_duration, span);
|
.option_layer(if is_console { Some(RedirectLayer) } else { None })
|
||||||
}
|
.layer(BodylessStatusFixLayer)
|
||||||
})
|
.layer(HeadRequestBodyFixLayer)
|
||||||
.on_failure(|_error, latency: Duration, span: &Span| {
|
.layer(PublicHealthEndpointLayer)
|
||||||
let _enter = span.enter();
|
.option_layer((!server_domains_configured && !is_console).then_some(VirtualHostStyleHintLayer))
|
||||||
record_active_http_requests(-1);
|
.service(service)
|
||||||
counter!(
|
};
|
||||||
METRIC_HTTP_SERVER_FAILURES_TOTAL,
|
let external_stack_service = build_external_stack(external_service);
|
||||||
LABEL_HTTP_STATUS_CLASS => "transport"
|
let internode_stack_service = build_internode_stack(internode_service);
|
||||||
)
|
let hybrid_service = PathDispatchService::new(external_stack_service, internode_stack_service);
|
||||||
.increment(1);
|
|
||||||
trace!(error = ?_error, duration_ms = duration_ms(latency), "HTTP request failure captured by trace layer");
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.layer(RequestLoggingLayer)
|
|
||||||
.layer(PropagateRequestIdLayer::x_request_id())
|
|
||||||
// Compress responses based on whitelist configuration
|
|
||||||
// Only compresses when enabled and matches configured extensions/MIME types
|
|
||||||
.layer(CompressionLayer::new().compress_when(PathAwareHttpCompressionPredicate::new(compression_config)))
|
|
||||||
.layer(PathCategoryInjectionLayer)
|
|
||||||
.layer(S3ErrorMessageCompatLayer)
|
|
||||||
.layer(ObjectAttributesEtagFixLayer)
|
|
||||||
// Conditional CORS layer: only applies to S3 API requests (not Admin, not Console)
|
|
||||||
// Admin has its own CORS handling in router.rs
|
|
||||||
// Console has its own CORS layer in setup_console_middleware_stack()
|
|
||||||
// S3 API uses this system default CORS (RUSTFS_CORS_ALLOWED_ORIGINS)
|
|
||||||
// Bucket-level CORS takes precedence when configured (handled in router.rs for OPTIONS, and in ecfs.rs for actual requests)
|
|
||||||
.layer(ConditionalCorsLayer::new())
|
|
||||||
.option_layer(if is_console { Some(RedirectLayer) } else { None })
|
|
||||||
// Must run before outer response-transforming layers: clear the body and remove
|
|
||||||
// Content-Length, Content-Type, and Transfer-Encoding for statuses
|
|
||||||
// that MUST NOT carry a body (1xx/204/304). Placed inside those
|
|
||||||
// layers so they see the already-bodyless
|
|
||||||
// response and so no layer (e.g. CORS) re-adds body headers afterward.
|
|
||||||
.layer(BodylessStatusFixLayer)
|
|
||||||
// HEAD responses must not send body bytes even when the inner S3 layer
|
|
||||||
// serializes an XML error payload.
|
|
||||||
.layer(HeadRequestBodyFixLayer)
|
|
||||||
// Health probes are public admin routes, but s3s parses virtual-host
|
|
||||||
// buckets before custom routes. Handle them here so SERVER_DOMAINS
|
|
||||||
// cannot turn /health into an S3 bucket request.
|
|
||||||
.layer(PublicHealthEndpointLayer)
|
|
||||||
// Virtual-hosted-style S3 requests (the AWS SDK / Terraform default) cannot be
|
|
||||||
// routed when no server domain is configured: s3s parses them path-style and
|
|
||||||
// returns an opaque 501. When RUSTFS_SERVER_DOMAINS is unset, return an actionable
|
|
||||||
// error pointing at the fix. Inert (not installed) once domains are configured.
|
|
||||||
.option_layer((!server_domains_configured && !is_console).then_some(VirtualHostStyleHintLayer))
|
|
||||||
.service(service);
|
|
||||||
|
|
||||||
let hybrid_service = TowerToHyperService::new(hybrid_service);
|
let hybrid_service = TowerToHyperService::new(hybrid_service);
|
||||||
|
|
||||||
@@ -1397,12 +1622,13 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::server::compress::RequestPathCategory;
|
use crate::server::compress::RequestPathCategory;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use http::HeaderMap;
|
|
||||||
use http::Request as HttpRequest;
|
use http::Request as HttpRequest;
|
||||||
use http_body_util::Empty;
|
use http::{HeaderMap, StatusCode};
|
||||||
|
use http_body_util::{Empty, Full};
|
||||||
use opentelemetry::propagation::Extractor;
|
use opentelemetry::propagation::Extractor;
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::future::Ready;
|
use std::future::Ready;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
use tower::{Layer, Service, ServiceBuilder};
|
use tower::{Layer, Service, ServiceBuilder};
|
||||||
|
|
||||||
@@ -1620,6 +1846,36 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct MarkerService {
|
||||||
|
name: &'static str,
|
||||||
|
hits: Arc<Mutex<Vec<&'static str>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MarkerService {
|
||||||
|
fn new(name: &'static str, hits: Arc<Mutex<Vec<&'static str>>>) -> Self {
|
||||||
|
Self { name, hits }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ReqBody> Service<HttpRequest<ReqBody>> for MarkerService {
|
||||||
|
type Response = Response<Full<Bytes>>;
|
||||||
|
type Error = Infallible;
|
||||||
|
type Future = Ready<std::result::Result<Response<Full<Bytes>>, Infallible>>;
|
||||||
|
|
||||||
|
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call(&mut self, _req: HttpRequest<ReqBody>) -> Self::Future {
|
||||||
|
self.hits.lock().expect("hits").push(self.name);
|
||||||
|
std::future::ready(Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Full::from(Bytes::from_static(self.name.as_bytes())))
|
||||||
|
.expect("response")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_service_builder_order_regression_for_response_extensions() {
|
fn test_service_builder_order_regression_for_response_extensions() {
|
||||||
let request = HttpRequest::builder().uri("/bucket/archive.zip").body(()).expect("request");
|
let request = HttpRequest::builder().uri("/bucket/archive.zip").body(()).expect("request");
|
||||||
@@ -1648,4 +1904,21 @@ mod tests {
|
|||||||
Some("true")
|
Some("true")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn path_dispatch_service_identifies_rpc_prefix() {
|
||||||
|
let hits = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let service =
|
||||||
|
PathDispatchService::new(MarkerService::new("external", Arc::clone(&hits)), MarkerService::new("internode", hits));
|
||||||
|
|
||||||
|
assert!(PathDispatchService::<MarkerService, MarkerService>::is_internode_path(&format!(
|
||||||
|
"{}/put_file_stream",
|
||||||
|
crate::server::RPC_PREFIX
|
||||||
|
)));
|
||||||
|
assert!(!PathDispatchService::<MarkerService, MarkerService>::is_internode_path(
|
||||||
|
"/bucket/object.txt"
|
||||||
|
));
|
||||||
|
assert!(!PathDispatchService::<MarkerService, MarkerService>::is_internode_path("/rustfs/rpcx"));
|
||||||
|
let _ = service;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ use serde_urlencoded::from_bytes;
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
|
use std::time::Instant;
|
||||||
use tokio::io::{self, AsyncWriteExt};
|
use tokio::io::{self, AsyncWriteExt};
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
use tower::Service;
|
use tower::Service;
|
||||||
@@ -280,6 +281,7 @@ fn is_internode_rpc_path(path: &str) -> bool {
|
|||||||
|
|
||||||
async fn handle_internode_rpc(req: Request<Incoming>) -> Response<Body> {
|
async fn handle_internode_rpc(req: Request<Incoming>) -> Response<Body> {
|
||||||
let operation = internode_http_operation(req.uri().path());
|
let operation = internode_http_operation(req.uri().path());
|
||||||
|
let started_at = Instant::now();
|
||||||
if let Err(response) = verify_internode_rpc_signature(req.uri(), req.method(), req.headers()) {
|
if let Err(response) = verify_internode_rpc_signature(req.uri(), req.method(), req.headers()) {
|
||||||
record_internode_rpc_error(operation);
|
record_internode_rpc_error(operation);
|
||||||
return *response;
|
return *response;
|
||||||
@@ -299,6 +301,14 @@ async fn handle_internode_rpc(req: Request<Incoming>) -> Response<Body> {
|
|||||||
record_internode_rpc_error(operation);
|
record_internode_rpc_error(operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(operation) = operation {
|
||||||
|
resolve_internode_metrics().record_duration_for_operation_and_backend(
|
||||||
|
operation,
|
||||||
|
INTERNODE_TRANSPORT_BACKEND_TCP_HTTP,
|
||||||
|
started_at.elapsed(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Executable
+82
@@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Unified fuzz runner script.
|
||||||
|
#
|
||||||
|
# Modes:
|
||||||
|
# ./scripts/fuzz/run.sh # build + run all smoke targets (60s each)
|
||||||
|
# BUILD_ONLY=1 ./scripts/fuzz/run.sh # build only, no fuzz run
|
||||||
|
# FUZZ_TARGET=path_containment ./scripts/fuzz/run.sh # build + run single target
|
||||||
|
# MAX_TOTAL_TIME=300 ./scripts/fuzz/run.sh # nightly-style 300s per target
|
||||||
|
#
|
||||||
|
# Environment variables:
|
||||||
|
# FUZZ_TARGET — run only this target (default: all smoke targets)
|
||||||
|
# MAX_TOTAL_TIME — seconds to fuzz per target (default: 60)
|
||||||
|
# ARTIFACT_ROOT — artifact output directory (default: artifacts)
|
||||||
|
# BUILD_ONLY — set to 1 to skip fuzz runs (default: 0)
|
||||||
|
# SKIP_BUILD — set to 1 to skip build phase (default: 0)
|
||||||
|
# USE_PREBUILT_BINARY — set to 1 to run a prebuilt fuzz binary directly
|
||||||
|
# PREBUILT_BINARY_DIR — directory containing prebuilt fuzz binaries
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
|
||||||
|
FUZZ_DIR="$REPO_ROOT/fuzz"
|
||||||
|
MAX_TOTAL_TIME=${MAX_TOTAL_TIME:-60}
|
||||||
|
ARTIFACT_ROOT=${ARTIFACT_ROOT:-artifacts}
|
||||||
|
FUZZ_TARGET=${FUZZ_TARGET:-}
|
||||||
|
BUILD_ONLY=${BUILD_ONLY:-0}
|
||||||
|
SKIP_BUILD=${SKIP_BUILD:-0}
|
||||||
|
USE_PREBUILT_BINARY=${USE_PREBUILT_BINARY:-0}
|
||||||
|
PREBUILT_BINARY_DIR=${PREBUILT_BINARY_DIR:-}
|
||||||
|
|
||||||
|
cd "$FUZZ_DIR"
|
||||||
|
mkdir -p "$ARTIFACT_ROOT"
|
||||||
|
|
||||||
|
targets="path_containment bucket_validation local_metadata"
|
||||||
|
if [ -n "$FUZZ_TARGET" ]; then
|
||||||
|
targets="$FUZZ_TARGET"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Phase 1: build (unless skipped)
|
||||||
|
if [ "$SKIP_BUILD" != "1" ]; then
|
||||||
|
for target in $targets; do
|
||||||
|
echo "==> cargo +nightly fuzz build $target"
|
||||||
|
cargo +nightly fuzz build "$target"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$BUILD_ONLY" = "1" ]; then
|
||||||
|
echo "==> Build-only mode; skipping fuzz runs."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Phase 2: run each target (incremental — no recompilation if already built)
|
||||||
|
for target in $targets; do
|
||||||
|
artifact_dir="$ARTIFACT_ROOT/$target"
|
||||||
|
corpus_dir="$FUZZ_DIR/corpus/$target"
|
||||||
|
mkdir -p "$artifact_dir"
|
||||||
|
mkdir -p "$corpus_dir"
|
||||||
|
|
||||||
|
if [ "$USE_PREBUILT_BINARY" = "1" ]; then
|
||||||
|
binary_dir="$PREBUILT_BINARY_DIR"
|
||||||
|
if [ -z "$binary_dir" ]; then
|
||||||
|
if [ -n "${CARGO_BUILD_TARGET:-}" ]; then
|
||||||
|
binary_dir="$FUZZ_DIR/target/${CARGO_BUILD_TARGET}/release"
|
||||||
|
else
|
||||||
|
binary_dir="$FUZZ_DIR/target/release"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
binary_path="$binary_dir/$target"
|
||||||
|
if [ ! -x "$binary_path" ]; then
|
||||||
|
echo "Missing executable prebuilt fuzz binary: $binary_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "==> $binary_path (-max_total_time=$MAX_TOTAL_TIME, -artifact_prefix=$artifact_dir/, corpus=$corpus_dir)"
|
||||||
|
"$binary_path" -max_total_time="$MAX_TOTAL_TIME" -artifact_prefix="$artifact_dir/" "$corpus_dir"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> cargo +nightly fuzz run $target (-max_total_time=$MAX_TOTAL_TIME, -artifact_prefix=$artifact_dir/)"
|
||||||
|
cargo +nightly fuzz run "$target" -- -max_total_time="$MAX_TOTAL_TIME" -artifact_prefix="$artifact_dir/"
|
||||||
|
done
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
||||||
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
|
|
||||||
FUZZ_DIR="$REPO_ROOT/fuzz"
|
|
||||||
MAX_TOTAL_TIME=${MAX_TOTAL_TIME:-60}
|
|
||||||
ARTIFACT_ROOT=${ARTIFACT_ROOT:-artifacts}
|
|
||||||
FUZZ_TARGET=${FUZZ_TARGET:-}
|
|
||||||
|
|
||||||
cd "$FUZZ_DIR"
|
|
||||||
mkdir -p "$ARTIFACT_ROOT"
|
|
||||||
|
|
||||||
targets="path_containment bucket_validation local_metadata"
|
|
||||||
if [ -n "$FUZZ_TARGET" ]; then
|
|
||||||
targets="$FUZZ_TARGET"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for target in $targets; do
|
|
||||||
artifact_dir="$ARTIFACT_ROOT/$target"
|
|
||||||
mkdir -p "$artifact_dir"
|
|
||||||
echo "==> cargo +nightly fuzz run $target (-max_total_time=$MAX_TOTAL_TIME, -artifact_prefix=$artifact_dir/)"
|
|
||||||
cargo +nightly fuzz run "$target" -- -max_total_time="$MAX_TOTAL_TIME" -artifact_prefix="$artifact_dir/"
|
|
||||||
done
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
||||||
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
|
|
||||||
FUZZ_DIR="$REPO_ROOT/fuzz"
|
|
||||||
MAX_TOTAL_TIME=${MAX_TOTAL_TIME:-300}
|
|
||||||
ARTIFACT_ROOT=${ARTIFACT_ROOT:-artifacts}
|
|
||||||
FUZZ_TARGET=${FUZZ_TARGET:-}
|
|
||||||
|
|
||||||
cd "$FUZZ_DIR"
|
|
||||||
mkdir -p "$ARTIFACT_ROOT"
|
|
||||||
|
|
||||||
targets="path_containment bucket_validation local_metadata"
|
|
||||||
if [ -n "$FUZZ_TARGET" ]; then
|
|
||||||
targets="$FUZZ_TARGET"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for target in $targets; do
|
|
||||||
artifact_dir="$ARTIFACT_ROOT/$target"
|
|
||||||
mkdir -p "$artifact_dir"
|
|
||||||
echo "==> cargo +nightly fuzz run $target (-max_total_time=$MAX_TOTAL_TIME, -artifact_prefix=$artifact_dir/)"
|
|
||||||
cargo +nightly fuzz run "$target" -- -max_total_time="$MAX_TOTAL_TIME" -artifact_prefix="$artifact_dir/"
|
|
||||||
done
|
|
||||||
Reference in New Issue
Block a user