mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-16 09:58:21 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aa9f77f0f2 | |||
| e4781e763a | |||
| ab7e777e55 |
@@ -252,16 +252,10 @@ test-group = 'ecstore-serial-flaky'
|
|||||||
# cluster, so it keeps the lane's parallel-safe / no-external-dependency
|
# cluster, so it keeps the lane's parallel-safe / no-external-dependency
|
||||||
# properties. The RustFS warm backend has no loopback guard (that guard is
|
# properties. The RustFS warm backend has no loopback guard (that guard is
|
||||||
# replication-only), so it needs no opt-in env for its 127.0.0.1 tier target.
|
# replication-only), so it needs no opt-in env for its 127.0.0.1 tier target.
|
||||||
#
|
|
||||||
# Disk compression (backlog#1848): the `compression` module joins the smoke
|
|
||||||
# lane so the multipart disk-compression roundtrips (restored after
|
|
||||||
# rustfs/rustfs#5169 disabled them) have PR-lane signal, not just merge-gate.
|
|
||||||
# Single-node servers on random ports with isolated temp dirs — meets the
|
|
||||||
# admission criteria unchanged.
|
|
||||||
[profile.e2e-smoke]
|
[profile.e2e-smoke]
|
||||||
default-filter = """
|
default-filter = """
|
||||||
package(e2e_test) & (
|
package(e2e_test) & (
|
||||||
test(/^(delete_marker_migration_semantics|version_id_regression|list_objects_v2_pagination|list_object_versions_regression|list_objects_duplicates|list_buckets_double_slash|list_buckets_auth|list_buckets_iam_filter|leading_slash_key|special_chars|create_bucket_region|delete_objects_versioning|head_object_consistency|head_object_range|copy_object_metadata|copy_object_tagging|copy_source_invalid_date|content_encoding|compression|multipart_storage_class|storage_class_capability|ssec_copy|anonymous_access|bucket_policy_check|presigned_negative|negative_sigv4|admin_auth|notification_webhook|tls_hot_reload|console_smoke|admin_iam_crud|admin_pools|sts_query_compat)_test::|^fake_s3_target::/)
|
test(/^(delete_marker_migration_semantics|version_id_regression|list_objects_v2_pagination|list_object_versions_regression|list_objects_duplicates|list_buckets_double_slash|list_buckets_auth|list_buckets_iam_filter|leading_slash_key|special_chars|create_bucket_region|delete_objects_versioning|head_object_consistency|head_object_range|copy_object_metadata|copy_object_tagging|copy_source_invalid_date|content_encoding|multipart_storage_class|storage_class_capability|ssec_copy|anonymous_access|bucket_policy_check|presigned_negative|negative_sigv4|admin_auth|notification_webhook|tls_hot_reload|console_smoke|admin_iam_crud|admin_pools|sts_query_compat)_test::|^fake_s3_target::/)
|
||||||
| test(/^replication_extension_test::(test_replication_check_succeeds_with_remote_target|test_replication_check_rejects_target_without_object_lock|test_set_remote_target_rejects_unversioned_source_bucket|test_replication_check_rejects_unversioned_source_bucket|test_replication_check_rejects_missing_replication_config|test_replication_check_rejects_invalid_bucket|test_set_remote_target_rejects_same_bucket_on_same_deployment|test_set_remote_target_rejects_unversioned_target_bucket|test_set_remote_target_update_requires_arn|test_set_remote_target_update_rejects_missing_target|test_set_remote_target_rejects_invalid_target_url|test_set_remote_target_rejects_self_signed_https_target_without_skip_tls_verify|test_set_remote_target_rejects_private_ca_https_target_without_ca_cert_pem|test_list_remote_targets_rejects_empty_bucket|test_list_remote_targets_rejects_invalid_bucket|test_remove_remote_target_rejects_missing_target|test_remove_remote_target_rejects_missing_arn|test_remove_remote_target_rejects_invalid_bucket|test_remove_remote_target_rejects_target_used_by_replication|test_delete_bucket_replication_removes_remote_target)$/)
|
| test(/^replication_extension_test::(test_replication_check_succeeds_with_remote_target|test_replication_check_rejects_target_without_object_lock|test_set_remote_target_rejects_unversioned_source_bucket|test_replication_check_rejects_unversioned_source_bucket|test_replication_check_rejects_missing_replication_config|test_replication_check_rejects_invalid_bucket|test_set_remote_target_rejects_same_bucket_on_same_deployment|test_set_remote_target_rejects_unversioned_target_bucket|test_set_remote_target_update_requires_arn|test_set_remote_target_update_rejects_missing_target|test_set_remote_target_rejects_invalid_target_url|test_set_remote_target_rejects_self_signed_https_target_without_skip_tls_verify|test_set_remote_target_rejects_private_ca_https_target_without_ca_cert_pem|test_list_remote_targets_rejects_empty_bucket|test_list_remote_targets_rejects_invalid_bucket|test_remove_remote_target_rejects_missing_target|test_remove_remote_target_rejects_missing_arn|test_remove_remote_target_rejects_invalid_bucket|test_remove_remote_target_rejects_target_used_by_replication|test_delete_bucket_replication_removes_remote_target)$/)
|
||||||
| test(/^reliant::lifecycle::/)
|
| test(/^reliant::lifecycle::/)
|
||||||
| test(/^reliant::tiering::/)
|
| test(/^reliant::tiering::/)
|
||||||
|
|||||||
@@ -182,12 +182,7 @@ jobs:
|
|||||||
echo '```'
|
echo '```'
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
# Readers: test-and-lint-rio-v2 (per-PR), build-rustfs-debug-binary-rio-v2
|
# Readers: test-and-lint-rio-v2, build-rustfs-debug-binary-rio-v2.
|
||||||
# (weekly schedule / manual dispatch only — dormant rio-v2 variant, see
|
|
||||||
# rustfs/backlog#1835 and docs/architecture/minio-file-format-compat.md).
|
|
||||||
# The second build below stays despite the reduced cadence: it warms the
|
|
||||||
# rio-v2,e2e-test-hooks feature resolution the scheduled build restores,
|
|
||||||
# which keeps that lane inside its 30-minute timeout.
|
|
||||||
warm-ci-feat-rio:
|
warm-ci-feat-rio:
|
||||||
name: Warm ci-feat-rio
|
name: Warm ci-feat-rio
|
||||||
runs-on: sm-standard-4
|
runs-on: sm-standard-4
|
||||||
|
|||||||
@@ -533,12 +533,7 @@ jobs:
|
|||||||
|
|
||||||
build-rustfs-debug-binary-rio-v2:
|
build-rustfs-debug-binary-rio-v2:
|
||||||
name: Build RustFS Debug Binary (rio-v2)
|
name: Build RustFS Debug Binary (rio-v2)
|
||||||
# Dormant rio-v2 variant (rustfs/backlog#1835): the feature ships in no
|
if: github.event_name != 'pull_request' || github.event.action != 'closed'
|
||||||
# default build, so this full-suite lane runs only on the weekly schedule
|
|
||||||
# and manual dispatch. Per-PR cfg-seam coverage stays with
|
|
||||||
# test-and-lint-rio-v2. Lifecycle and the promote-or-delete condition:
|
|
||||||
# docs/architecture/minio-file-format-compat.md ("rio-v2 variant lifecycle").
|
|
||||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
|
||||||
needs: [ quick-checks ]
|
needs: [ quick-checks ]
|
||||||
runs-on: sm-standard-4
|
runs-on: sm-standard-4
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
@@ -829,9 +824,6 @@ jobs:
|
|||||||
|
|
||||||
e2e-tests-rio-v2:
|
e2e-tests-rio-v2:
|
||||||
name: End-to-End Tests (rio-v2)
|
name: End-to-End Tests (rio-v2)
|
||||||
# Inherits the schedule/dispatch-only gate through needs: on every other
|
|
||||||
# event build-rustfs-debug-binary-rio-v2 is skipped, so this job skips
|
|
||||||
# with it (see the dormant-variant comment on that job).
|
|
||||||
needs: [ build-rustfs-debug-binary-rio-v2 ]
|
needs: [ build-rustfs-debug-binary-rio-v2 ]
|
||||||
runs-on: sm-standard-2
|
runs-on: sm-standard-2
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ jobs:
|
|||||||
short_sha: ${{ steps.check.outputs.short_sha }}
|
short_sha: ${{ steps.check.outputs.short_sha }}
|
||||||
is_prerelease: ${{ steps.check.outputs.is_prerelease }}
|
is_prerelease: ${{ steps.check.outputs.is_prerelease }}
|
||||||
create_latest: ${{ steps.check.outputs.create_latest }}
|
create_latest: ${{ steps.check.outputs.create_latest }}
|
||||||
source_ref: ${{ steps.check.outputs.source_ref }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
@@ -119,7 +118,6 @@ jobs:
|
|||||||
short_sha=""
|
short_sha=""
|
||||||
is_prerelease=false
|
is_prerelease=false
|
||||||
create_latest=false
|
create_latest=false
|
||||||
source_ref="$GITHUB_SHA"
|
|
||||||
|
|
||||||
if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
|
if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
|
||||||
# Triggered by build workflow completion
|
# Triggered by build workflow completion
|
||||||
@@ -139,7 +137,6 @@ jobs:
|
|||||||
# Extract version info from commit message or use commit SHA
|
# Extract version info from commit message or use commit SHA
|
||||||
# Use Git to generate consistent short SHA (ensures uniqueness like build.yml)
|
# Use Git to generate consistent short SHA (ensures uniqueness like build.yml)
|
||||||
short_sha=$(git rev-parse --short "$HEAD_SHA")
|
short_sha=$(git rev-parse --short "$HEAD_SHA")
|
||||||
source_ref="$HEAD_SHA"
|
|
||||||
|
|
||||||
# Determine build type based on triggering workflow event and ref
|
# Determine build type based on triggering workflow event and ref
|
||||||
triggering_event="$TRIGGERING_EVENT"
|
triggering_event="$TRIGGERING_EVENT"
|
||||||
@@ -264,23 +261,6 @@ jobs:
|
|||||||
echo "⚠️ Only release versions (latest, v1.0.0, 1.0.0) and prereleases (v1.0.0-alpha1, 1.0.0-beta2) are supported"
|
echo "⚠️ Only release versions (latest, v1.0.0, 1.0.0) and prereleases (v1.0.0-alpha1, 1.0.0-beta2) are supported"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [[ "$should_build" == true && "$input_version" != "latest" ]]; then
|
|
||||||
tag_ref="refs/tags/$input_version"
|
|
||||||
if ! git ls-remote --exit-code origin "$tag_ref" >/dev/null 2>&1; then
|
|
||||||
if [[ "$input_version" == v* ]]; then
|
|
||||||
tag_ref="refs/tags/${input_version#v}"
|
|
||||||
else
|
|
||||||
tag_ref="refs/tags/v$input_version"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! git ls-remote --exit-code origin "$tag_ref" >/dev/null 2>&1; then
|
|
||||||
echo "❌ Release tag not found for Docker build: $input_version"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
source_ref="$tag_ref"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -291,7 +271,6 @@ jobs:
|
|||||||
echo "short_sha=$short_sha"
|
echo "short_sha=$short_sha"
|
||||||
echo "is_prerelease=$is_prerelease"
|
echo "is_prerelease=$is_prerelease"
|
||||||
echo "create_latest=$create_latest"
|
echo "create_latest=$create_latest"
|
||||||
echo "source_ref=$source_ref"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
echo "🐳 Docker Build Summary:"
|
echo "🐳 Docker Build Summary:"
|
||||||
@@ -302,7 +281,6 @@ jobs:
|
|||||||
echo " - Short SHA: $short_sha"
|
echo " - Short SHA: $short_sha"
|
||||||
echo " - Is prerelease: $is_prerelease"
|
echo " - Is prerelease: $is_prerelease"
|
||||||
echo " - Create latest: $create_latest"
|
echo " - Create latest: $create_latest"
|
||||||
echo " - Source ref: $source_ref"
|
|
||||||
|
|
||||||
# Build multi-arch Docker images
|
# Build multi-arch Docker images
|
||||||
# Strategy: Build images using pre-built binaries from dl.rustfs.com
|
# Strategy: Build images using pre-built binaries from dl.rustfs.com
|
||||||
@@ -330,7 +308,6 @@ jobs:
|
|||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: ${{ needs.build-check.outputs.source_ref }}
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||||
@@ -420,8 +397,7 @@ jobs:
|
|||||||
LABELS="org.opencontainers.image.title=RustFS"
|
LABELS="org.opencontainers.image.title=RustFS"
|
||||||
LABELS="$LABELS,org.opencontainers.image.description=RustFS distributed object storage system"
|
LABELS="$LABELS,org.opencontainers.image.description=RustFS distributed object storage system"
|
||||||
LABELS="$LABELS,org.opencontainers.image.version=$VERSION"
|
LABELS="$LABELS,org.opencontainers.image.version=$VERSION"
|
||||||
SOURCE_REVISION="$(git rev-parse HEAD)"
|
LABELS="$LABELS,org.opencontainers.image.revision=${{ github.sha }}"
|
||||||
LABELS="$LABELS,org.opencontainers.image.revision=$SOURCE_REVISION"
|
|
||||||
LABELS="$LABELS,org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}"
|
LABELS="$LABELS,org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}"
|
||||||
LABELS="$LABELS,org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
LABELS="$LABELS,org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||||
LABELS="$LABELS,org.opencontainers.image.build-type=$BUILD_TYPE"
|
LABELS="$LABELS,org.opencontainers.image.build-type=$BUILD_TYPE"
|
||||||
|
|||||||
+1
-4
@@ -101,10 +101,7 @@ refactors.
|
|||||||
|
|
||||||
The `rustfs` binary crate composes these libraries into the running server.
|
The `rustfs` binary crate composes these libraries into the running server.
|
||||||
`ecstore` remains the storage engine at the architectural center; its internal
|
`ecstore` remains the storage engine at the architectural center; its internal
|
||||||
module split is tracked under `docs/architecture/`. `rio-v2` is the
|
module split is tracked under `docs/architecture/`.
|
||||||
feature-gated MinIO on-disk format compatibility I/O layer; it ships in no
|
|
||||||
default build (lifecycle:
|
|
||||||
[docs/architecture/minio-file-format-compat.md](docs/architecture/minio-file-format-compat.md)).
|
|
||||||
|
|
||||||
## Architecture Invariants
|
## Architecture Invariants
|
||||||
|
|
||||||
|
|||||||
Generated
+47
-51
@@ -278,7 +278,6 @@ checksum = "312c1ea69e5fe9966e0029fb95aca8790100b85aff4f0d3b00a9337c74069a9c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"bon",
|
"bon",
|
||||||
"crc32fast",
|
|
||||||
"digest 0.11.3",
|
"digest 0.11.3",
|
||||||
"log",
|
"log",
|
||||||
"miniz_oxide 0.9.1",
|
"miniz_oxide 0.9.1",
|
||||||
@@ -290,11 +289,9 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_bytes",
|
"serde_bytes",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"snap",
|
|
||||||
"strum",
|
"strum",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"uuid",
|
"uuid",
|
||||||
"zstd",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3764,7 +3761,7 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "e2e_test"
|
name = "e2e_test"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"astral-tokio-tar",
|
"astral-tokio-tar",
|
||||||
@@ -9093,7 +9090,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs"
|
name = "rustfs"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -9203,7 +9200,6 @@ dependencies = [
|
|||||||
"serial_test",
|
"serial_test",
|
||||||
"sha2 0.11.0",
|
"sha2 0.11.0",
|
||||||
"shadow-rs",
|
"shadow-rs",
|
||||||
"snap",
|
|
||||||
"socket2",
|
"socket2",
|
||||||
"subtle",
|
"subtle",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
@@ -9231,7 +9227,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-audit"
|
name = "rustfs-audit"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"const-str",
|
"const-str",
|
||||||
@@ -9254,7 +9250,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-checksums"
|
name = "rustfs-checksums"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64-simd",
|
"base64-simd",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -9270,7 +9266,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-common"
|
name = "rustfs-common"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"hotpath",
|
"hotpath",
|
||||||
@@ -9288,7 +9284,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-concurrency"
|
name = "rustfs-concurrency"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hotpath",
|
"hotpath",
|
||||||
"insta",
|
"insta",
|
||||||
@@ -9301,7 +9297,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-config"
|
name = "rustfs-config"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"const-str",
|
"const-str",
|
||||||
"hotpath",
|
"hotpath",
|
||||||
@@ -9311,7 +9307,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-credentials"
|
name = "rustfs-credentials"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64-simd",
|
"base64-simd",
|
||||||
"hmac 0.13.0",
|
"hmac 0.13.0",
|
||||||
@@ -9325,7 +9321,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-crypto"
|
name = "rustfs-crypto"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -9346,7 +9342,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-data-usage"
|
name = "rustfs-data-usage"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hotpath",
|
"hotpath",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
@@ -9356,7 +9352,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-ecstore"
|
name = "rustfs-ecstore"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-channel",
|
"async-channel",
|
||||||
@@ -9495,7 +9491,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-extension-schema"
|
name = "rustfs-extension-schema"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hotpath",
|
"hotpath",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -9505,7 +9501,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-filemeta"
|
name = "rustfs-filemeta"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
@@ -9532,7 +9528,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-heal"
|
name = "rustfs-heal"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.23.1",
|
"base64 0.23.1",
|
||||||
@@ -9563,7 +9559,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-iam"
|
name = "rustfs-iam"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -9604,7 +9600,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-io-core"
|
name = "rustfs-io-core"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"hotpath",
|
"hotpath",
|
||||||
@@ -9617,7 +9613,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-io-metrics"
|
name = "rustfs-io-metrics"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"criterion",
|
"criterion",
|
||||||
"hotpath",
|
"hotpath",
|
||||||
@@ -9681,7 +9677,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-keystone"
|
name = "rustfs-keystone"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -9708,7 +9704,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-kms"
|
name = "rustfs-kms"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -9757,7 +9753,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-lifecycle"
|
name = "rustfs-lifecycle"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"hotpath",
|
"hotpath",
|
||||||
@@ -9780,7 +9776,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-lock"
|
name = "rustfs-lock"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"compact_str",
|
"compact_str",
|
||||||
@@ -9803,7 +9799,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-log-analyzer"
|
name = "rustfs-log-analyzer"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -9822,7 +9818,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-madmin"
|
name = "rustfs-madmin"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hotpath",
|
"hotpath",
|
||||||
"humantime",
|
"humantime",
|
||||||
@@ -9837,7 +9833,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-notify"
|
name = "rustfs-notify"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -9872,7 +9868,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-object-capacity"
|
name = "rustfs-object-capacity"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"criterion",
|
"criterion",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -9892,7 +9888,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-object-data-cache"
|
name = "rustfs-object-data-cache"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"criterion",
|
"criterion",
|
||||||
@@ -9909,7 +9905,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-obs"
|
name = "rustfs-obs"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
@@ -9964,7 +9960,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-policy"
|
name = "rustfs-policy"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64-simd",
|
"base64-simd",
|
||||||
@@ -9995,7 +9991,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-protocols"
|
name = "rustfs-protocols"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"astral-tokio-tar",
|
"astral-tokio-tar",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
@@ -10057,7 +10053,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-protos"
|
name = "rustfs-protos"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
"hotpath",
|
"hotpath",
|
||||||
@@ -10081,7 +10077,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-replication"
|
name = "rustfs-replication"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -10099,7 +10095,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-rio"
|
name = "rustfs-rio"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
@@ -10137,7 +10133,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-rio-v2"
|
name = "rustfs-rio-v2"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -10160,7 +10156,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-s3-ops"
|
name = "rustfs-s3-ops"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hotpath",
|
"hotpath",
|
||||||
"rustfs-s3-types",
|
"rustfs-s3-types",
|
||||||
@@ -10168,7 +10164,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-s3-types"
|
name = "rustfs-s3-types"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hotpath",
|
"hotpath",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -10177,7 +10173,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-s3select-api"
|
name = "rustfs-s3select-api"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -10207,7 +10203,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-s3select-query"
|
name = "rustfs-s3select-query"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -10226,7 +10222,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-scanner"
|
name = "rustfs-scanner"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -10266,7 +10262,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-security-governance"
|
name = "rustfs-security-governance"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hotpath",
|
"hotpath",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
@@ -10274,7 +10270,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-signer"
|
name = "rustfs-signer"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64-simd",
|
"base64-simd",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -10292,7 +10288,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-storage-api"
|
name = "rustfs-storage-api"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"hotpath",
|
"hotpath",
|
||||||
@@ -10307,7 +10303,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-targets"
|
name = "rustfs-targets"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-nats",
|
"async-nats",
|
||||||
@@ -10361,7 +10357,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-test-utils"
|
name = "rustfs-test-utils"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hotpath",
|
"hotpath",
|
||||||
"rustfs-data-usage",
|
"rustfs-data-usage",
|
||||||
@@ -10377,7 +10373,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-tls-runtime"
|
name = "rustfs-tls-runtime"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"hotpath",
|
"hotpath",
|
||||||
@@ -10398,7 +10394,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-trusted-proxies"
|
name = "rustfs-trusted-proxies"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -10435,7 +10431,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-utils"
|
name = "rustfs-utils"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64-simd",
|
"base64-simd",
|
||||||
"blake2",
|
"blake2",
|
||||||
@@ -10477,7 +10473,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-zip"
|
name = "rustfs-zip"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"astral-tokio-tar",
|
"astral-tokio-tar",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
|
|||||||
+49
-49
@@ -41,7 +41,7 @@ members = [
|
|||||||
"crates/protocols", # Protocol implementations (FTPS, SFTP, etc.)
|
"crates/protocols", # Protocol implementations (FTPS, SFTP, etc.)
|
||||||
"crates/protos", # Protocol buffer definitions
|
"crates/protos", # Protocol buffer definitions
|
||||||
"crates/rio", # Rust I/O utilities and abstractions
|
"crates/rio", # Rust I/O utilities and abstractions
|
||||||
"crates/rio-v2", # MinIO on-disk format compatibility I/O layer (feature-gated, ships in no default build)
|
"crates/rio-v2", # Next-generation Rust I/O compatibility layer
|
||||||
"crates/replication", # Replication contracts and wire formats
|
"crates/replication", # Replication contracts and wire formats
|
||||||
"crates/concurrency", # Concurrency management for RustFS - timeout, locking, backpressure, and I/O scheduling
|
"crates/concurrency", # Concurrency management for RustFS - timeout, locking, backpressure, and I/O scheduling
|
||||||
"crates/s3-types", # S3 event type definitions
|
"crates/s3-types", # S3 event type definitions
|
||||||
@@ -69,7 +69,7 @@ edition = "2024"
|
|||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
repository = "https://github.com/rustfs/rustfs"
|
repository = "https://github.com/rustfs/rustfs"
|
||||||
rust-version = "1.97.1"
|
rust-version = "1.97.1"
|
||||||
version = "1.0.0-rc.2"
|
version = "1.0.0-rc.1"
|
||||||
homepage = "https://rustfs.com"
|
homepage = "https://rustfs.com"
|
||||||
description = "RustFS is a high-performance distributed object storage software built using Rust, one of the most popular languages worldwide. "
|
description = "RustFS is a high-performance distributed object storage software built using Rust, one of the most popular languages worldwide. "
|
||||||
keywords = ["RustFS", "Minio", "object-storage", "filesystem", "s3"]
|
keywords = ["RustFS", "Minio", "object-storage", "filesystem", "s3"]
|
||||||
@@ -86,52 +86,52 @@ redundant_clone = "warn"
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# RustFS Internal Crates
|
# RustFS Internal Crates
|
||||||
rustfs = { path = "./rustfs", version = "1.0.0-rc.2" }
|
rustfs = { path = "./rustfs", version = "1.0.0-rc.1" }
|
||||||
rustfs-heal = { path = "crates/heal", version = "1.0.0-rc.2" }
|
rustfs-heal = { path = "crates/heal", version = "1.0.0-rc.1" }
|
||||||
rustfs-audit = { path = "crates/audit", version = "1.0.0-rc.2" }
|
rustfs-audit = { path = "crates/audit", version = "1.0.0-rc.1" }
|
||||||
rustfs-checksums = { path = "crates/checksums", version = "1.0.0-rc.2" }
|
rustfs-checksums = { path = "crates/checksums", version = "1.0.0-rc.1" }
|
||||||
rustfs-common = { path = "crates/common", version = "1.0.0-rc.2" }
|
rustfs-common = { path = "crates/common", version = "1.0.0-rc.1" }
|
||||||
rustfs-data-usage = { path = "crates/data-usage", version = "1.0.0-rc.2" }
|
rustfs-data-usage = { path = "crates/data-usage", version = "1.0.0-rc.1" }
|
||||||
rustfs-config = { path = "./crates/config", version = "1.0.0-rc.2" }
|
rustfs-config = { path = "./crates/config", version = "1.0.0-rc.1" }
|
||||||
rustfs-concurrency = { path = "./crates/concurrency", version = "1.0.0-rc.2" }
|
rustfs-concurrency = { path = "./crates/concurrency", version = "1.0.0-rc.1" }
|
||||||
rustfs-credentials = { path = "crates/credentials", version = "1.0.0-rc.2" }
|
rustfs-credentials = { path = "crates/credentials", version = "1.0.0-rc.1" }
|
||||||
rustfs-crypto = { path = "crates/crypto", version = "1.0.0-rc.2" }
|
rustfs-crypto = { path = "crates/crypto", version = "1.0.0-rc.1" }
|
||||||
rustfs-ecstore = { path = "crates/ecstore", version = "1.0.0-rc.2" }
|
rustfs-ecstore = { path = "crates/ecstore", version = "1.0.0-rc.1" }
|
||||||
rustfs-filemeta = { path = "crates/filemeta", version = "1.0.0-rc.2" }
|
rustfs-filemeta = { path = "crates/filemeta", version = "1.0.0-rc.1" }
|
||||||
rustfs-iam = { path = "crates/iam", version = "1.0.0-rc.2" }
|
rustfs-iam = { path = "crates/iam", version = "1.0.0-rc.1" }
|
||||||
rustfs-keystone = { path = "crates/keystone", version = "1.0.0-rc.2" }
|
rustfs-keystone = { path = "crates/keystone", version = "1.0.0-rc.1" }
|
||||||
rustfs-lifecycle = { path = "crates/lifecycle", version = "1.0.0-rc.2" }
|
rustfs-lifecycle = { path = "crates/lifecycle", version = "1.0.0-rc.1" }
|
||||||
rustfs-kms = { path = "crates/kms", version = "1.0.0-rc.2" }
|
rustfs-kms = { path = "crates/kms", version = "1.0.0-rc.1" }
|
||||||
rustfs-lock = { path = "crates/lock", version = "1.0.0-rc.2" }
|
rustfs-lock = { path = "crates/lock", version = "1.0.0-rc.1" }
|
||||||
rustfs-madmin = { path = "crates/madmin", version = "1.0.0-rc.2" }
|
rustfs-madmin = { path = "crates/madmin", version = "1.0.0-rc.1" }
|
||||||
rustfs-notify = { path = "crates/notify", version = "1.0.0-rc.2" }
|
rustfs-notify = { path = "crates/notify", version = "1.0.0-rc.1" }
|
||||||
rustfs-io-metrics = { path = "crates/io-metrics", version = "1.0.0-rc.2" }
|
rustfs-io-metrics = { path = "crates/io-metrics", version = "1.0.0-rc.1" }
|
||||||
rustfs-io-core = { path = "crates/io-core", version = "1.0.0-rc.2" }
|
rustfs-io-core = { path = "crates/io-core", version = "1.0.0-rc.1" }
|
||||||
rustfs-object-capacity = { path = "crates/object-capacity", version = "1.0.0-rc.2" }
|
rustfs-object-capacity = { path = "crates/object-capacity", version = "1.0.0-rc.1" }
|
||||||
rustfs-object-data-cache = { path = "crates/object-data-cache", version = "1.0.0-rc.2", default-features = false }
|
rustfs-object-data-cache = { path = "crates/object-data-cache", version = "1.0.0-rc.1", default-features = false }
|
||||||
rustfs-log-analyzer = { path = "crates/log-analyzer", version = "1.0.0-rc.2" }
|
rustfs-log-analyzer = { path = "crates/log-analyzer", version = "1.0.0-rc.1" }
|
||||||
rustfs-obs = { path = "crates/obs", version = "1.0.0-rc.2" }
|
rustfs-obs = { path = "crates/obs", version = "1.0.0-rc.1" }
|
||||||
rustfs-policy = { path = "crates/policy", version = "1.0.0-rc.2" }
|
rustfs-policy = { path = "crates/policy", version = "1.0.0-rc.1" }
|
||||||
rustfs-protos = { path = "crates/protos", version = "1.0.0-rc.2" }
|
rustfs-protos = { path = "crates/protos", version = "1.0.0-rc.1" }
|
||||||
rustfs-protocols = { path = "crates/protocols", version = "1.0.0-rc.2" }
|
rustfs-protocols = { path = "crates/protocols", version = "1.0.0-rc.1" }
|
||||||
rustfs-replication = { path = "crates/replication", version = "1.0.0-rc.2" }
|
rustfs-replication = { path = "crates/replication", version = "1.0.0-rc.1" }
|
||||||
rustfs-rio = { path = "crates/rio", version = "1.0.0-rc.2" }
|
rustfs-rio = { path = "crates/rio", version = "1.0.0-rc.1" }
|
||||||
rustfs-rio-v2 = { path = "crates/rio-v2", version = "1.0.0-rc.2" }
|
rustfs-rio-v2 = { path = "crates/rio-v2", version = "1.0.0-rc.1" }
|
||||||
rustfs-s3-types = { path = "crates/s3-types", version = "1.0.0-rc.2" }
|
rustfs-s3-types = { path = "crates/s3-types", version = "1.0.0-rc.1" }
|
||||||
rustfs-s3-ops = { path = "crates/s3-ops", version = "1.0.0-rc.2" }
|
rustfs-s3-ops = { path = "crates/s3-ops", version = "1.0.0-rc.1" }
|
||||||
rustfs-s3select-api = { path = "crates/s3select-api", version = "1.0.0-rc.2" }
|
rustfs-s3select-api = { path = "crates/s3select-api", version = "1.0.0-rc.1" }
|
||||||
rustfs-s3select-query = { path = "crates/s3select-query", version = "1.0.0-rc.2" }
|
rustfs-s3select-query = { path = "crates/s3select-query", version = "1.0.0-rc.1" }
|
||||||
rustfs-scanner = { path = "crates/scanner", version = "1.0.0-rc.2" }
|
rustfs-scanner = { path = "crates/scanner", version = "1.0.0-rc.1" }
|
||||||
rustfs-security-governance = { path = "crates/security-governance", version = "1.0.0-rc.2" }
|
rustfs-security-governance = { path = "crates/security-governance", version = "1.0.0-rc.1" }
|
||||||
rustfs-extension-schema = { path = "crates/extension-schema", version = "1.0.0-rc.2" }
|
rustfs-extension-schema = { path = "crates/extension-schema", version = "1.0.0-rc.1" }
|
||||||
rustfs-signer = { path = "crates/signer", version = "1.0.0-rc.2" }
|
rustfs-signer = { path = "crates/signer", version = "1.0.0-rc.1" }
|
||||||
rustfs-storage-api = { path = "crates/storage-api", version = "1.0.0-rc.2" }
|
rustfs-storage-api = { path = "crates/storage-api", version = "1.0.0-rc.1" }
|
||||||
rustfs-trusted-proxies = { path = "crates/trusted-proxies", version = "1.0.0-rc.2" }
|
rustfs-trusted-proxies = { path = "crates/trusted-proxies", version = "1.0.0-rc.1" }
|
||||||
rustfs-targets = { path = "crates/targets", version = "1.0.0-rc.2" }
|
rustfs-targets = { path = "crates/targets", version = "1.0.0-rc.1" }
|
||||||
rustfs-test-utils = { path = "crates/test-utils", version = "1.0.0-rc.2" }
|
rustfs-test-utils = { path = "crates/test-utils", version = "1.0.0-rc.1" }
|
||||||
rustfs-tls-runtime = { path = "crates/tls-runtime", version = "1.0.0-rc.2" }
|
rustfs-tls-runtime = { path = "crates/tls-runtime", version = "1.0.0-rc.1" }
|
||||||
rustfs-utils = { path = "crates/utils", version = "1.0.0-rc.2" }
|
rustfs-utils = { path = "crates/utils", version = "1.0.0-rc.1" }
|
||||||
rustfs-zip = { path = "./crates/zip", version = "1.0.0-rc.2" }
|
rustfs-zip = { path = "./crates/zip", version = "1.0.0-rc.1" }
|
||||||
|
|
||||||
# Async Runtime and Networking
|
# Async Runtime and Networking
|
||||||
async-channel = "2.5.0"
|
async-channel = "2.5.0"
|
||||||
@@ -171,7 +171,7 @@ tower = { version = "0.5.3" }
|
|||||||
tower-http = { version = "0.7.0" }
|
tower-http = { version = "0.7.0" }
|
||||||
|
|
||||||
# Serialization and Data Formats
|
# Serialization and Data Formats
|
||||||
apache-avro = { version = "0.22.0", features = ["snappy", "zstandard"] }
|
apache-avro = "0.22.0"
|
||||||
bytes = { version = "1.12.1" }
|
bytes = { version = "1.12.1" }
|
||||||
bytesize = "2.7.0"
|
bytesize = "2.7.0"
|
||||||
byteorder = "1.5.0"
|
byteorder = "1.5.0"
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ chown -R 10001:10001 data logs
|
|||||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
||||||
|
|
||||||
# Using specific version
|
# Using specific version
|
||||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:1.0.0-rc.2
|
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:1.0.0-rc.1
|
||||||
```
|
```
|
||||||
|
|
||||||
If you use [podman](https://github.com/containers/podman) instead of docker, you can install the RustFS with the below command
|
If you use [podman](https://github.com/containers/podman) instead of docker, you can install the RustFS with the below command
|
||||||
|
|||||||
+1
-1
@@ -113,7 +113,7 @@ chown -R 10001:10001 data logs
|
|||||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
||||||
|
|
||||||
# 使用指定版本运行
|
# 使用指定版本运行
|
||||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:1.0.0-rc.2
|
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:1.0.0-rc.1
|
||||||
```
|
```
|
||||||
|
|
||||||
如果您通过绑定挂载启用 TLS 证书目录,也请用同样方式准备该目录:
|
如果您通过绑定挂载启用 TLS 证书目录,也请用同样方式准备该目录:
|
||||||
|
|||||||
@@ -137,22 +137,6 @@ pub const DEFAULT_TIER_REMOTE_VERSION_STATE_FLEET_CONFIRMED: bool = false;
|
|||||||
const _: () = assert!(!DEFAULT_TIER_REMOTE_VERSION_STATE_WRITE);
|
const _: () = assert!(!DEFAULT_TIER_REMOTE_VERSION_STATE_WRITE);
|
||||||
const _: () = assert!(!DEFAULT_TIER_REMOTE_VERSION_STATE_FLEET_CONFIRMED);
|
const _: () = assert!(!DEFAULT_TIER_REMOTE_VERSION_STATE_FLEET_CONFIRMED);
|
||||||
|
|
||||||
/// Request the object-transaction fencing contract used by storage-owned
|
|
||||||
/// cleanup receipts and lock-window optimizations.
|
|
||||||
///
|
|
||||||
/// This is fail-closed: enabling the writer without a live fleet proof rejects
|
|
||||||
/// the commit rather than silently using a legacy-safe path.
|
|
||||||
pub const ENV_OBJECT_TRANSACTION_FENCING_WRITE: &str = "RUSTFS_OBJECT_TRANSACTION_FENCING_WRITE";
|
|
||||||
pub const DEFAULT_OBJECT_TRANSACTION_FENCING_WRITE: bool = false;
|
|
||||||
|
|
||||||
/// Operator-attested confirmation that every serving node understands the
|
|
||||||
/// object transaction fencing contract.
|
|
||||||
pub const ENV_OBJECT_TRANSACTION_FENCING_FLEET_CONFIRMED: &str = "RUSTFS_OBJECT_TRANSACTION_FENCING_FLEET_CONFIRMED";
|
|
||||||
pub const DEFAULT_OBJECT_TRANSACTION_FENCING_FLEET_CONFIRMED: bool = false;
|
|
||||||
|
|
||||||
const _: () = assert!(!DEFAULT_OBJECT_TRANSACTION_FENCING_WRITE);
|
|
||||||
const _: () = assert!(!DEFAULT_OBJECT_TRANSACTION_FENCING_FLEET_CONFIRMED);
|
|
||||||
|
|
||||||
/// Request preserving legacy per-part checksum metadata during data movement.
|
/// Request preserving legacy per-part checksum metadata during data movement.
|
||||||
///
|
///
|
||||||
/// This remains ineffective until
|
/// This remains ineffective until
|
||||||
@@ -689,13 +673,4 @@ mod remote_version_state_tests {
|
|||||||
"RUSTFS_DATA_MOVEMENT_PART_CHECKSUMS_FLEET_CONFIRMED"
|
"RUSTFS_DATA_MOVEMENT_PART_CHECKSUMS_FLEET_CONFIRMED"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn object_transaction_fencing_gate_uses_stable_environment_names() {
|
|
||||||
assert_eq!(super::ENV_OBJECT_TRANSACTION_FENCING_WRITE, "RUSTFS_OBJECT_TRANSACTION_FENCING_WRITE");
|
|
||||||
assert_eq!(
|
|
||||||
super::ENV_OBJECT_TRANSACTION_FENCING_FLEET_CONFIRMED,
|
|
||||||
"RUSTFS_OBJECT_TRANSACTION_FENCING_FLEET_CONFIRMED"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,10 +67,7 @@ fn configured_capture_log_path(temp_dir: &str) -> Option<String> {
|
|||||||
capture_log_path(Path::new(&log_dir), temp_dir).map(|path| path.to_string_lossy().into_owned())
|
capture_log_path(Path::new(&log_dir), temp_dir).map(|path| path.to_string_lossy().into_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn capture_command_logs(
|
fn capture_command_logs(command: &mut Command, log_path: Option<&str>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
command: &mut Command,
|
|
||||||
log_path: Option<&str>,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
let Some(log_path) = log_path else {
|
let Some(log_path) = log_path else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
use crate::common::{RustFSTestEnvironment, init_logging, rustfs_binary_path};
|
use crate::common::{RustFSTestEnvironment, init_logging, rustfs_binary_path};
|
||||||
use aws_sdk_s3::primitives::ByteStream;
|
use aws_sdk_s3::primitives::ByteStream;
|
||||||
use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart};
|
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -26,15 +25,6 @@ fn generate_compressible_data(size: usize) -> Vec<u8> {
|
|||||||
data
|
data
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deterministic 2048-byte-period binary pattern that compresses extremely well: every part
|
|
||||||
/// yields many compressed blocks, which is exactly the shape that reproduced the mid-payload
|
|
||||||
/// Pending truncation (rustfs/rustfs#5957).
|
|
||||||
fn generate_high_ratio_binary_data(size: usize, seed: u8) -> Vec<u8> {
|
|
||||||
(0..size)
|
|
||||||
.map(|i| ((i as u64).wrapping_mul(2_654_435_761).wrapping_add(seed as u64) >> 3) as u8)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_part_files(temp_dir: &str, bucket: &str, object_key: &str) -> Vec<PathBuf> {
|
fn find_part_files(temp_dir: &str, bucket: &str, object_key: &str) -> Vec<PathBuf> {
|
||||||
let bucket_path = PathBuf::from(temp_dir).join(bucket);
|
let bucket_path = PathBuf::from(temp_dir).join(bucket);
|
||||||
let mut part_files = Vec::new();
|
let mut part_files = Vec::new();
|
||||||
@@ -65,14 +55,9 @@ async fn start_rustfs_with_compression(env: &mut RustFSTestEnvironment) -> Resul
|
|||||||
env.cleanup_existing_processes().await?;
|
env.cleanup_existing_processes().await?;
|
||||||
|
|
||||||
let binary_path = rustfs_binary_path();
|
let binary_path = rustfs_binary_path();
|
||||||
// Route the child's stdout/stderr through the shared RUSTFS_E2E_LOG_DIR
|
let process = Command::new(&binary_path)
|
||||||
// capture (survives the temp-dir cleanup on Drop and is uploaded as a CI
|
|
||||||
// artifact); without the env var the child inherits stdio as before.
|
|
||||||
let mut command = Command::new(&binary_path);
|
|
||||||
command
|
|
||||||
.env("RUSTFS_CONSOLE_ENABLE", "false")
|
.env("RUSTFS_CONSOLE_ENABLE", "false")
|
||||||
.env("RUSTFS_COMPRESSION_ENABLED", "true")
|
.env("RUSTFS_COMPRESSION_ENABLED", "true")
|
||||||
.env("RUSTFS_COMPRESSION_MULTIPART_ENABLED", "true")
|
|
||||||
.args([
|
.args([
|
||||||
"--address",
|
"--address",
|
||||||
&env.address,
|
&env.address,
|
||||||
@@ -81,9 +66,8 @@ async fn start_rustfs_with_compression(env: &mut RustFSTestEnvironment) -> Resul
|
|||||||
"--secret-key",
|
"--secret-key",
|
||||||
&env.secret_key,
|
&env.secret_key,
|
||||||
&env.temp_dir,
|
&env.temp_dir,
|
||||||
]);
|
])
|
||||||
crate::common::capture_command_logs(&mut command, env.capture_log_path.as_deref())?;
|
.spawn()?;
|
||||||
let process = command.spawn()?;
|
|
||||||
|
|
||||||
env.process = Some(process);
|
env.process = Some(process);
|
||||||
|
|
||||||
@@ -170,647 +154,3 @@ async fn test_compression_roundtrip() -> Result<(), Box<dyn std::error::Error +
|
|||||||
env.stop_server();
|
env.stop_server();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
const MULTIPART_COMPRESSION_BUCKET: &str = "compression-multipart-bucket";
|
|
||||||
const MPU_PART1_SIZE: usize = 5 * 1024 * 1024;
|
|
||||||
const MPU_PART2_SIZE: usize = 1024 * 1024;
|
|
||||||
|
|
||||||
async fn multipart_upload(
|
|
||||||
client: &aws_sdk_s3::Client,
|
|
||||||
bucket: &str,
|
|
||||||
key: &str,
|
|
||||||
parts: &[&[u8]],
|
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
let create = client.create_multipart_upload().bucket(bucket).key(key).send().await?;
|
|
||||||
let upload_id = create.upload_id().ok_or("missing upload id")?.to_string();
|
|
||||||
|
|
||||||
let mut completed_parts = Vec::with_capacity(parts.len());
|
|
||||||
for (i, part) in parts.iter().enumerate() {
|
|
||||||
let part_number = (i + 1) as i32;
|
|
||||||
let upload = client
|
|
||||||
.upload_part()
|
|
||||||
.bucket(bucket)
|
|
||||||
.key(key)
|
|
||||||
.upload_id(&upload_id)
|
|
||||||
.part_number(part_number)
|
|
||||||
.body(ByteStream::from(part.to_vec()))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
completed_parts.push(
|
|
||||||
CompletedPart::builder()
|
|
||||||
.part_number(part_number)
|
|
||||||
.e_tag(upload.e_tag().unwrap_or_default())
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
client
|
|
||||||
.complete_multipart_upload()
|
|
||||||
.bucket(bucket)
|
|
||||||
.key(key)
|
|
||||||
.upload_id(&upload_id)
|
|
||||||
.multipart_upload(CompletedMultipartUpload::builder().set_parts(Some(completed_parts)).build())
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_range(
|
|
||||||
client: &aws_sdk_s3::Client,
|
|
||||||
bucket: &str,
|
|
||||||
key: &str,
|
|
||||||
range: &str,
|
|
||||||
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
let response = client.get_object().bucket(bucket).key(key).range(range).send().await?;
|
|
||||||
Ok(response.body.collect().await?.into_bytes().to_vec())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Multipart disk compression roundtrip: parts are written as independent
|
|
||||||
/// compressed streams and every GET shape must reassemble the original bytes
|
|
||||||
/// (rustfs/rustfs#5957: multipart uploads previously bypassed disk compression
|
|
||||||
/// entirely).
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_compression_multipart_roundtrip() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
init_logging();
|
|
||||||
info!("Starting multipart compression roundtrip test");
|
|
||||||
|
|
||||||
let mut env = RustFSTestEnvironment::new().await?;
|
|
||||||
start_rustfs_with_compression(&mut env).await?;
|
|
||||||
|
|
||||||
let client = env.create_s3_client();
|
|
||||||
env.create_test_bucket(MULTIPART_COMPRESSION_BUCKET).await?;
|
|
||||||
|
|
||||||
let object_key = "multipart-compressible.txt";
|
|
||||||
let part1 = generate_compressible_data(MPU_PART1_SIZE);
|
|
||||||
let part2 = generate_compressible_data(MPU_PART2_SIZE);
|
|
||||||
let mut original_data = part1.clone();
|
|
||||||
original_data.extend_from_slice(&part2);
|
|
||||||
let total_size = original_data.len();
|
|
||||||
|
|
||||||
multipart_upload(&client, MULTIPART_COMPRESSION_BUCKET, object_key, &[&part1, &part2]).await?;
|
|
||||||
|
|
||||||
let head_response = client
|
|
||||||
.head_object()
|
|
||||||
.bucket(MULTIPART_COMPRESSION_BUCKET)
|
|
||||||
.key(object_key)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
head_response.content_length().unwrap_or(0) as usize,
|
|
||||||
total_size,
|
|
||||||
"Content-Length should be the logical object size"
|
|
||||||
);
|
|
||||||
|
|
||||||
let part_files = find_part_files(&env.temp_dir, MULTIPART_COMPRESSION_BUCKET, object_key);
|
|
||||||
assert!(!part_files.is_empty(), "expected on-disk part files for the multipart object");
|
|
||||||
let total_physical_size: u64 = part_files.iter().filter_map(|p| fs::metadata(p).ok()).map(|m| m.len()).sum();
|
|
||||||
assert!(
|
|
||||||
total_physical_size < (total_size / 2) as u64,
|
|
||||||
"Physical size {total_physical_size} should be well below original size {total_size} (multipart compression applied)"
|
|
||||||
);
|
|
||||||
info!("Multipart physical storage size: {total_physical_size} bytes (compressed from {total_size} bytes)");
|
|
||||||
|
|
||||||
// Full GET must reassemble both independently compressed parts.
|
|
||||||
let get_response = client
|
|
||||||
.get_object()
|
|
||||||
.bucket(MULTIPART_COMPRESSION_BUCKET)
|
|
||||||
.key(object_key)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let downloaded = get_response.body.collect().await?.into_bytes();
|
|
||||||
assert_eq!(downloaded.len(), total_size);
|
|
||||||
assert_eq!(&downloaded[..], &original_data[..], "full GET data mismatch");
|
|
||||||
|
|
||||||
// Range fully inside part 1.
|
|
||||||
let range_inside_part1 = fetch_range(&client, MULTIPART_COMPRESSION_BUCKET, object_key, "bytes=1024-999423").await?;
|
|
||||||
assert_eq!(&range_inside_part1[..], &original_data[1024..999424], "part-1 range mismatch");
|
|
||||||
|
|
||||||
// Range crossing the part boundary.
|
|
||||||
let boundary_start = MPU_PART1_SIZE - 128 * 1024;
|
|
||||||
let boundary_end = MPU_PART1_SIZE + 128 * 1024 - 1;
|
|
||||||
let range_crossing = fetch_range(
|
|
||||||
&client,
|
|
||||||
MULTIPART_COMPRESSION_BUCKET,
|
|
||||||
object_key,
|
|
||||||
&format!("bytes={boundary_start}-{boundary_end}"),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
&range_crossing[..],
|
|
||||||
&original_data[boundary_start..boundary_end + 1],
|
|
||||||
"boundary-crossing range mismatch"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Range fully inside part 2.
|
|
||||||
let part2_start = MPU_PART1_SIZE + 4096;
|
|
||||||
let part2_end = MPU_PART1_SIZE + 256 * 1024 - 1;
|
|
||||||
let range_inside_part2 = fetch_range(
|
|
||||||
&client,
|
|
||||||
MULTIPART_COMPRESSION_BUCKET,
|
|
||||||
object_key,
|
|
||||||
&format!("bytes={part2_start}-{part2_end}"),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
&range_inside_part2[..],
|
|
||||||
&original_data[part2_start..part2_end + 1],
|
|
||||||
"part-2 range mismatch"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Suffix range (last 128 KiB, entirely in part 2).
|
|
||||||
let suffix_len = 128 * 1024;
|
|
||||||
let suffix = fetch_range(&client, MULTIPART_COMPRESSION_BUCKET, object_key, &format!("bytes=-{suffix_len}")).await?;
|
|
||||||
assert_eq!(&suffix[..], &original_data[total_size - suffix_len..], "suffix range mismatch");
|
|
||||||
|
|
||||||
// partNumber GETs must return each original part.
|
|
||||||
for (part_number, expected) in [(1, &part1), (2, &part2)] {
|
|
||||||
let response = client
|
|
||||||
.get_object()
|
|
||||||
.bucket(MULTIPART_COMPRESSION_BUCKET)
|
|
||||||
.key(object_key)
|
|
||||||
.part_number(part_number)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let body = response.body.collect().await?.into_bytes();
|
|
||||||
assert_eq!(&body[..], &expected[..], "partNumber={part_number} GET mismatch");
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Multipart compression roundtrip test passed");
|
|
||||||
env.delete_test_bucket(MULTIPART_COMPRESSION_BUCKET).await?;
|
|
||||||
env.stop_server();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
const MPU_HIGH_RATIO_BUCKET: &str = "compression-mpu-high-ratio-bucket";
|
|
||||||
|
|
||||||
/// High-ratio binary multipart payload: the object key is on the compression allow-list, so the
|
|
||||||
/// disk-compression path runs and each part is stored as many compressed blocks — the shape that
|
|
||||||
/// reproduced the mid-payload Pending truncation (rustfs/rustfs#5957). Every GET shape must return
|
|
||||||
/// the exact original bytes, and the stored size must show the data really was compressed.
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_compression_multipart_high_ratio_binary_roundtrip() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
init_logging();
|
|
||||||
info!("Starting multipart high-ratio binary compression roundtrip test");
|
|
||||||
|
|
||||||
let mut env = RustFSTestEnvironment::new().await?;
|
|
||||||
start_rustfs_with_compression(&mut env).await?;
|
|
||||||
|
|
||||||
let client = env.create_s3_client();
|
|
||||||
env.create_test_bucket(MPU_HIGH_RATIO_BUCKET).await?;
|
|
||||||
|
|
||||||
let object_key = "multipart-high-ratio.txt";
|
|
||||||
let part1 = generate_high_ratio_binary_data(MPU_PART1_SIZE, 7);
|
|
||||||
let part2 = generate_high_ratio_binary_data(MPU_PART2_SIZE, 61);
|
|
||||||
let mut original_data = part1.clone();
|
|
||||||
original_data.extend_from_slice(&part2);
|
|
||||||
let total_size = original_data.len();
|
|
||||||
|
|
||||||
multipart_upload(&client, MPU_HIGH_RATIO_BUCKET, object_key, &[&part1, &part2]).await?;
|
|
||||||
|
|
||||||
let head_response = client
|
|
||||||
.head_object()
|
|
||||||
.bucket(MPU_HIGH_RATIO_BUCKET)
|
|
||||||
.key(object_key)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
head_response.content_length().unwrap_or(0) as usize,
|
|
||||||
total_size,
|
|
||||||
"Content-Length should be the logical object size"
|
|
||||||
);
|
|
||||||
|
|
||||||
// This pattern compresses to roughly 1/50 of its logical size, so a comfortably loose 2x
|
|
||||||
// margin still proves the parts were stored compressed rather than raw or double-encoded.
|
|
||||||
let part_files = find_part_files(&env.temp_dir, MPU_HIGH_RATIO_BUCKET, object_key);
|
|
||||||
assert!(!part_files.is_empty(), "expected on-disk part files for the multipart object");
|
|
||||||
let total_physical_size: u64 = part_files.iter().filter_map(|p| fs::metadata(p).ok()).map(|m| m.len()).sum();
|
|
||||||
assert!(
|
|
||||||
total_physical_size < (total_size as u64) / 2,
|
|
||||||
"Physical size {total_physical_size} should be far below the logical size {total_size} for high-ratio data"
|
|
||||||
);
|
|
||||||
info!("High-ratio multipart physical storage size: {total_physical_size} bytes (logical {total_size} bytes)");
|
|
||||||
|
|
||||||
info!("step: full GET");
|
|
||||||
let get_response = client
|
|
||||||
.get_object()
|
|
||||||
.bucket(MPU_HIGH_RATIO_BUCKET)
|
|
||||||
.key(object_key)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let downloaded = get_response.body.collect().await?.into_bytes();
|
|
||||||
assert_eq!(downloaded.len(), total_size);
|
|
||||||
assert_eq!(&downloaded[..], &original_data[..], "full GET data mismatch");
|
|
||||||
|
|
||||||
// Range crossing the part boundary.
|
|
||||||
info!("step: boundary range GET");
|
|
||||||
let boundary_start = MPU_PART1_SIZE - 128 * 1024;
|
|
||||||
let boundary_end = MPU_PART1_SIZE + 128 * 1024 - 1;
|
|
||||||
let range_crossing = fetch_range(
|
|
||||||
&client,
|
|
||||||
MPU_HIGH_RATIO_BUCKET,
|
|
||||||
object_key,
|
|
||||||
&format!("bytes={boundary_start}-{boundary_end}"),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
&range_crossing[..],
|
|
||||||
&original_data[boundary_start..boundary_end + 1],
|
|
||||||
"boundary-crossing range mismatch"
|
|
||||||
);
|
|
||||||
|
|
||||||
// partNumber GET for the trailing part.
|
|
||||||
info!("step: partNumber GET");
|
|
||||||
let part2_response = client
|
|
||||||
.get_object()
|
|
||||||
.bucket(MPU_HIGH_RATIO_BUCKET)
|
|
||||||
.key(object_key)
|
|
||||||
.part_number(2)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let part2_body = part2_response.body.collect().await?.into_bytes();
|
|
||||||
assert_eq!(&part2_body[..], &part2[..], "partNumber=2 GET mismatch");
|
|
||||||
|
|
||||||
info!("Multipart high-ratio binary compression roundtrip test passed");
|
|
||||||
env.delete_test_bucket(MPU_HIGH_RATIO_BUCKET).await?;
|
|
||||||
env.stop_server();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
const MPU_COPY_COMPRESSION_BUCKET: &str = "compression-mpu-copy-bucket";
|
|
||||||
const MPU_COPY_SOURCE_SIZE: usize = 6 * 1024 * 1024;
|
|
||||||
const MPU_COPY_RANGE_LEN: usize = 5 * 1024 * 1024;
|
|
||||||
|
|
||||||
/// UploadPartCopy feeds a part from an already stored (and already compressed) object. The copied
|
|
||||||
/// range must be decompressed on read and re-compressed into the destination part, so the final
|
|
||||||
/// object has to match "source prefix + uploaded tail" byte for byte.
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_compression_multipart_upload_part_copy_roundtrip() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
init_logging();
|
|
||||||
info!("Starting multipart upload-part-copy compression roundtrip test");
|
|
||||||
|
|
||||||
let mut env = RustFSTestEnvironment::new().await?;
|
|
||||||
start_rustfs_with_compression(&mut env).await?;
|
|
||||||
|
|
||||||
let client = env.create_s3_client();
|
|
||||||
env.create_test_bucket(MPU_COPY_COMPRESSION_BUCKET).await?;
|
|
||||||
|
|
||||||
// Source object: a plain PUT that goes through the single-stream compression path.
|
|
||||||
let source_key = "copy-source.txt";
|
|
||||||
let source_data = generate_compressible_data(MPU_COPY_SOURCE_SIZE);
|
|
||||||
client
|
|
||||||
.put_object()
|
|
||||||
.bucket(MPU_COPY_COMPRESSION_BUCKET)
|
|
||||||
.key(source_key)
|
|
||||||
.body(ByteStream::from(source_data.clone()))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Destination object: part 1 copied from the source, part 2 uploaded directly.
|
|
||||||
let target_key = "copy-target.txt";
|
|
||||||
let part2 = generate_compressible_data(MPU_PART2_SIZE);
|
|
||||||
let mut expected_data = source_data[..MPU_COPY_RANGE_LEN].to_vec();
|
|
||||||
expected_data.extend_from_slice(&part2);
|
|
||||||
let total_size = expected_data.len();
|
|
||||||
|
|
||||||
let create = client
|
|
||||||
.create_multipart_upload()
|
|
||||||
.bucket(MPU_COPY_COMPRESSION_BUCKET)
|
|
||||||
.key(target_key)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let upload_id = create.upload_id().ok_or("missing upload id")?.to_string();
|
|
||||||
|
|
||||||
let copy_part = client
|
|
||||||
.upload_part_copy()
|
|
||||||
.bucket(MPU_COPY_COMPRESSION_BUCKET)
|
|
||||||
.key(target_key)
|
|
||||||
.upload_id(&upload_id)
|
|
||||||
.part_number(1)
|
|
||||||
.copy_source(format!("{MPU_COPY_COMPRESSION_BUCKET}/{source_key}"))
|
|
||||||
.copy_source_range(format!("bytes=0-{}", MPU_COPY_RANGE_LEN - 1))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let copy_etag = copy_part
|
|
||||||
.copy_part_result()
|
|
||||||
.and_then(|r| r.e_tag())
|
|
||||||
.ok_or("missing copy part etag")?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let uploaded_part = client
|
|
||||||
.upload_part()
|
|
||||||
.bucket(MPU_COPY_COMPRESSION_BUCKET)
|
|
||||||
.key(target_key)
|
|
||||||
.upload_id(&upload_id)
|
|
||||||
.part_number(2)
|
|
||||||
.body(ByteStream::from(part2.clone()))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
client
|
|
||||||
.complete_multipart_upload()
|
|
||||||
.bucket(MPU_COPY_COMPRESSION_BUCKET)
|
|
||||||
.key(target_key)
|
|
||||||
.upload_id(&upload_id)
|
|
||||||
.multipart_upload(
|
|
||||||
CompletedMultipartUpload::builder()
|
|
||||||
.parts(CompletedPart::builder().part_number(1).e_tag(copy_etag).build())
|
|
||||||
.parts(
|
|
||||||
CompletedPart::builder()
|
|
||||||
.part_number(2)
|
|
||||||
.e_tag(uploaded_part.e_tag().unwrap_or_default())
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let head_response = client
|
|
||||||
.head_object()
|
|
||||||
.bucket(MPU_COPY_COMPRESSION_BUCKET)
|
|
||||||
.key(target_key)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
head_response.content_length().unwrap_or(0) as usize,
|
|
||||||
total_size,
|
|
||||||
"Content-Length should be the logical object size"
|
|
||||||
);
|
|
||||||
|
|
||||||
let part_files = find_part_files(&env.temp_dir, MPU_COPY_COMPRESSION_BUCKET, target_key);
|
|
||||||
assert!(!part_files.is_empty(), "expected on-disk part files for the copied object");
|
|
||||||
let total_physical_size: u64 = part_files.iter().filter_map(|p| fs::metadata(p).ok()).map(|m| m.len()).sum();
|
|
||||||
assert!(
|
|
||||||
total_physical_size < (total_size / 2) as u64,
|
|
||||||
"Physical size {total_physical_size} should be well below original size {total_size} (copied part compression applied)"
|
|
||||||
);
|
|
||||||
|
|
||||||
let get_response = client
|
|
||||||
.get_object()
|
|
||||||
.bucket(MPU_COPY_COMPRESSION_BUCKET)
|
|
||||||
.key(target_key)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let downloaded = get_response.body.collect().await?.into_bytes();
|
|
||||||
assert_eq!(downloaded.len(), total_size);
|
|
||||||
assert_eq!(&downloaded[..], &expected_data[..], "copied multipart GET data mismatch");
|
|
||||||
|
|
||||||
info!("Multipart upload-part-copy compression roundtrip test passed");
|
|
||||||
env.delete_test_bucket(MPU_COPY_COMPRESSION_BUCKET).await?;
|
|
||||||
env.stop_server();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
const MPU_THREE_PARTS_BUCKET: &str = "compression-mpu-three-parts-bucket";
|
|
||||||
const MPU_THREE_PARTS_TAIL_SIZE: usize = 512 * 1024;
|
|
||||||
|
|
||||||
/// Three-part upload with uneven part sizes: each partNumber GET must map back to exactly one
|
|
||||||
/// compressed part stream, and a suffix range must resolve inside the trailing part.
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_compression_multipart_three_parts_part_number_gets() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
init_logging();
|
|
||||||
info!("Starting three-part multipart compression partNumber test");
|
|
||||||
|
|
||||||
let mut env = RustFSTestEnvironment::new().await?;
|
|
||||||
start_rustfs_with_compression(&mut env).await?;
|
|
||||||
|
|
||||||
let client = env.create_s3_client();
|
|
||||||
env.create_test_bucket(MPU_THREE_PARTS_BUCKET).await?;
|
|
||||||
|
|
||||||
let object_key = "multipart-three-parts.txt";
|
|
||||||
let part1 = generate_compressible_data(MPU_PART1_SIZE);
|
|
||||||
let part2 = generate_compressible_data(MPU_PART1_SIZE);
|
|
||||||
let part3 = generate_compressible_data(MPU_THREE_PARTS_TAIL_SIZE);
|
|
||||||
let mut original_data = part1.clone();
|
|
||||||
original_data.extend_from_slice(&part2);
|
|
||||||
original_data.extend_from_slice(&part3);
|
|
||||||
let total_size = original_data.len();
|
|
||||||
|
|
||||||
multipart_upload(&client, MPU_THREE_PARTS_BUCKET, object_key, &[&part1, &part2, &part3]).await?;
|
|
||||||
|
|
||||||
let head_response = client
|
|
||||||
.head_object()
|
|
||||||
.bucket(MPU_THREE_PARTS_BUCKET)
|
|
||||||
.key(object_key)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
head_response.content_length().unwrap_or(0) as usize,
|
|
||||||
total_size,
|
|
||||||
"Content-Length should be the logical object size"
|
|
||||||
);
|
|
||||||
|
|
||||||
let part_files = find_part_files(&env.temp_dir, MPU_THREE_PARTS_BUCKET, object_key);
|
|
||||||
assert!(!part_files.is_empty(), "expected on-disk part files for the multipart object");
|
|
||||||
let total_physical_size: u64 = part_files.iter().filter_map(|p| fs::metadata(p).ok()).map(|m| m.len()).sum();
|
|
||||||
assert!(
|
|
||||||
total_physical_size < (total_size / 2) as u64,
|
|
||||||
"Physical size {total_physical_size} should be well below original size {total_size} (multipart compression applied)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Every partNumber GET must return exactly the bytes of the corresponding uploaded part.
|
|
||||||
for (part_number, expected) in [(1, &part1), (2, &part2), (3, &part3)] {
|
|
||||||
let response = client
|
|
||||||
.get_object()
|
|
||||||
.bucket(MPU_THREE_PARTS_BUCKET)
|
|
||||||
.key(object_key)
|
|
||||||
.part_number(part_number)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let body = response.body.collect().await?.into_bytes();
|
|
||||||
assert_eq!(&body[..], &expected[..], "partNumber={part_number} GET mismatch");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suffix range (last 64 KiB) resolves inside the trailing part.
|
|
||||||
let suffix_len = 64 * 1024;
|
|
||||||
let suffix = fetch_range(&client, MPU_THREE_PARTS_BUCKET, object_key, &format!("bytes=-{suffix_len}")).await?;
|
|
||||||
assert_eq!(&suffix[..], &original_data[total_size - suffix_len..], "suffix range mismatch");
|
|
||||||
|
|
||||||
info!("Three-part multipart compression partNumber test passed");
|
|
||||||
env.delete_test_bucket(MPU_THREE_PARTS_BUCKET).await?;
|
|
||||||
env.stop_server();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
const MPU_SSE_COMPRESSION_BUCKET: &str = "compression-mpu-sse-bucket";
|
|
||||||
|
|
||||||
async fn start_rustfs_with_compression_and_sse(
|
|
||||||
env: &mut RustFSTestEnvironment,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
use base64::Engine;
|
|
||||||
env.cleanup_existing_processes().await?;
|
|
||||||
|
|
||||||
let binary_path = rustfs_binary_path();
|
|
||||||
let master_key = base64::engine::general_purpose::STANDARD.encode([0x42u8; 32]);
|
|
||||||
// Server output goes to a file inside the per-test temp dir so a failing
|
|
||||||
// run can be diagnosed from the child's logs.
|
|
||||||
let server_log = std::fs::File::create(format!("{}/server.log", env.temp_dir))?;
|
|
||||||
let server_log_err = server_log.try_clone()?;
|
|
||||||
let process = Command::new(&binary_path)
|
|
||||||
.env("RUSTFS_CONSOLE_ENABLE", "false")
|
|
||||||
.env("RUSTFS_COMPRESSION_ENABLED", "true")
|
|
||||||
.env("RUSTFS_COMPRESSION_MULTIPART_ENABLED", "true")
|
|
||||||
.env("RUSTFS_SSE_S3_MASTER_KEY", master_key)
|
|
||||||
.env("RUST_LOG", "rustfs=info,rustfs_ecstore=info")
|
|
||||||
.stdout(std::process::Stdio::from(server_log))
|
|
||||||
.stderr(std::process::Stdio::from(server_log_err))
|
|
||||||
.args([
|
|
||||||
"--address",
|
|
||||||
&env.address,
|
|
||||||
"--access-key",
|
|
||||||
&env.access_key,
|
|
||||||
"--secret-key",
|
|
||||||
&env.secret_key,
|
|
||||||
&env.temp_dir,
|
|
||||||
])
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
env.process = Some(process);
|
|
||||||
|
|
||||||
info!("Waiting for RustFS server with compression + SSE-S3 enabled on {}", env.address);
|
|
||||||
for i in 0..30 {
|
|
||||||
if TcpStream::connect(&env.address).await.is_ok() {
|
|
||||||
info!("RustFS server is ready after {} attempts", i + 1);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if i == 29 {
|
|
||||||
return Err("RustFS server failed to become ready".into());
|
|
||||||
}
|
|
||||||
sleep(Duration::from_secs(1)).await;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// SSE-S3 + disk compression multipart: each part is compressed and then encrypted, and every GET
|
|
||||||
/// shape must still return the original plaintext bytes. Physical size must shrink because the
|
|
||||||
/// compression runs before encryption.
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_compression_multipart_sse_s3_roundtrip() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
use aws_sdk_s3::types::ServerSideEncryption;
|
|
||||||
|
|
||||||
init_logging();
|
|
||||||
info!("Starting SSE-S3 multipart compression roundtrip test");
|
|
||||||
|
|
||||||
let mut env = RustFSTestEnvironment::new().await?;
|
|
||||||
start_rustfs_with_compression_and_sse(&mut env).await?;
|
|
||||||
|
|
||||||
let client = env.create_s3_client();
|
|
||||||
env.create_test_bucket(MPU_SSE_COMPRESSION_BUCKET).await?;
|
|
||||||
|
|
||||||
let object_key = "multipart-sse-compressible.txt";
|
|
||||||
let part1 = generate_compressible_data(MPU_PART1_SIZE);
|
|
||||||
let part2 = generate_compressible_data(MPU_PART2_SIZE);
|
|
||||||
let mut original_data = part1.clone();
|
|
||||||
original_data.extend_from_slice(&part2);
|
|
||||||
let total_size = original_data.len();
|
|
||||||
|
|
||||||
let create = client
|
|
||||||
.create_multipart_upload()
|
|
||||||
.bucket(MPU_SSE_COMPRESSION_BUCKET)
|
|
||||||
.key(object_key)
|
|
||||||
.server_side_encryption(ServerSideEncryption::Aes256)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let upload_id = create.upload_id().ok_or("missing upload id")?.to_string();
|
|
||||||
|
|
||||||
let mut completed_parts = Vec::new();
|
|
||||||
for (i, part) in [&part1, &part2].into_iter().enumerate() {
|
|
||||||
let part_number = (i + 1) as i32;
|
|
||||||
let upload = client
|
|
||||||
.upload_part()
|
|
||||||
.bucket(MPU_SSE_COMPRESSION_BUCKET)
|
|
||||||
.key(object_key)
|
|
||||||
.upload_id(&upload_id)
|
|
||||||
.part_number(part_number)
|
|
||||||
.body(ByteStream::from(part.clone()))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
completed_parts.push(
|
|
||||||
CompletedPart::builder()
|
|
||||||
.part_number(part_number)
|
|
||||||
.e_tag(upload.e_tag().unwrap_or_default())
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
client
|
|
||||||
.complete_multipart_upload()
|
|
||||||
.bucket(MPU_SSE_COMPRESSION_BUCKET)
|
|
||||||
.key(object_key)
|
|
||||||
.upload_id(&upload_id)
|
|
||||||
.multipart_upload(CompletedMultipartUpload::builder().set_parts(Some(completed_parts)).build())
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let head_response = client
|
|
||||||
.head_object()
|
|
||||||
.bucket(MPU_SSE_COMPRESSION_BUCKET)
|
|
||||||
.key(object_key)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
head_response.content_length().unwrap_or(0) as usize,
|
|
||||||
total_size,
|
|
||||||
"Content-Length should be the logical object size"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
head_response.server_side_encryption(),
|
|
||||||
Some(&ServerSideEncryption::Aes256),
|
|
||||||
"HEAD must report SSE-S3"
|
|
||||||
);
|
|
||||||
|
|
||||||
let part_files = find_part_files(&env.temp_dir, MPU_SSE_COMPRESSION_BUCKET, object_key);
|
|
||||||
assert!(!part_files.is_empty(), "expected on-disk part files for the multipart object");
|
|
||||||
let total_physical_size: u64 = part_files.iter().filter_map(|p| fs::metadata(p).ok()).map(|m| m.len()).sum();
|
|
||||||
assert!(
|
|
||||||
total_physical_size < (total_size / 2) as u64,
|
|
||||||
"Physical size {total_physical_size} should be well below original size {total_size} (compress-then-encrypt applied)"
|
|
||||||
);
|
|
||||||
|
|
||||||
let get_response = client
|
|
||||||
.get_object()
|
|
||||||
.bucket(MPU_SSE_COMPRESSION_BUCKET)
|
|
||||||
.key(object_key)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let downloaded = get_response.body.collect().await?.into_bytes();
|
|
||||||
assert_eq!(downloaded.len(), total_size);
|
|
||||||
assert_eq!(&downloaded[..], &original_data[..], "SSE-S3 multipart full GET data mismatch");
|
|
||||||
|
|
||||||
// Range crossing the part boundary must decrypt and decompress across parts.
|
|
||||||
let boundary_start = MPU_PART1_SIZE - 64 * 1024;
|
|
||||||
let boundary_end = MPU_PART1_SIZE + 64 * 1024 - 1;
|
|
||||||
let range_crossing = fetch_range(
|
|
||||||
&client,
|
|
||||||
MPU_SSE_COMPRESSION_BUCKET,
|
|
||||||
object_key,
|
|
||||||
&format!("bytes={boundary_start}-{boundary_end}"),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
&range_crossing[..],
|
|
||||||
&original_data[boundary_start..boundary_end + 1],
|
|
||||||
"SSE-S3 boundary-crossing range mismatch"
|
|
||||||
);
|
|
||||||
|
|
||||||
// partNumber GET for the trailing part.
|
|
||||||
let part2_response = client
|
|
||||||
.get_object()
|
|
||||||
.bucket(MPU_SSE_COMPRESSION_BUCKET)
|
|
||||||
.key(object_key)
|
|
||||||
.part_number(2)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let part2_body = part2_response.body.collect().await?.into_bytes();
|
|
||||||
assert_eq!(&part2_body[..], &part2[..], "SSE-S3 partNumber=2 GET mismatch");
|
|
||||||
|
|
||||||
info!("SSE-S3 multipart compression roundtrip test passed");
|
|
||||||
env.delete_test_bucket(MPU_SSE_COMPRESSION_BUCKET).await?;
|
|
||||||
env.stop_server();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -76,18 +76,6 @@ const SOURCE_MTIME_HEADERS: [&str; 2] = ["x-rustfs-source-mtime", "x-minio-sourc
|
|||||||
const SOURCE_REPLICATION_REQUEST_HEADERS: [&str; 2] =
|
const SOURCE_REPLICATION_REQUEST_HEADERS: [&str; 2] =
|
||||||
["x-rustfs-source-replication-request", "x-minio-source-replication-request"];
|
["x-rustfs-source-replication-request", "x-minio-source-replication-request"];
|
||||||
const SOURCE_ETAG_HEADERS: [&str; 2] = ["x-rustfs-source-etag", "x-minio-source-etag"];
|
const SOURCE_ETAG_HEADERS: [&str; 2] = ["x-rustfs-source-etag", "x-minio-source-etag"];
|
||||||
const SOURCE_TAGGING_TIMESTAMP_HEADERS: [&str; 2] = [
|
|
||||||
"x-rustfs-source-replication-tagging-timestamp",
|
|
||||||
"x-minio-source-replication-tagging-timestamp",
|
|
||||||
];
|
|
||||||
const SOURCE_RETENTION_TIMESTAMP_HEADERS: [&str; 2] = [
|
|
||||||
"x-rustfs-source-replication-retention-timestamp",
|
|
||||||
"x-minio-source-replication-retention-timestamp",
|
|
||||||
];
|
|
||||||
const SOURCE_LEGALHOLD_TIMESTAMP_HEADERS: [&str; 2] = [
|
|
||||||
"x-rustfs-source-replication-legalhold-timestamp",
|
|
||||||
"x-minio-source-replication-legalhold-timestamp",
|
|
||||||
];
|
|
||||||
const RESERVED_BUCKET_PREFIXES: [&str; 3] = ["xn--", "sthree-", "amzn-s3-demo-"];
|
const RESERVED_BUCKET_PREFIXES: [&str; 3] = ["xn--", "sthree-", "amzn-s3-demo-"];
|
||||||
const RESERVED_BUCKET_SUFFIXES: [&str; 6] = ["-s3alias", "--ol-s3", ".mrap", "--x-s3", "--table-s3", "-an"];
|
const RESERVED_BUCKET_SUFFIXES: [&str; 6] = ["-s3alias", "--ol-s3", ".mrap", "--x-s3", "--table-s3", "-an"];
|
||||||
|
|
||||||
@@ -130,25 +118,6 @@ pub enum FaultAction {
|
|||||||
WrongEtag,
|
WrongEtag,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replication LWW timestamp headers observed on a request, journaled so
|
|
||||||
/// sender-side tests can assert what a real target would receive.
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
||||||
pub struct ReplicationTimestampHeaders {
|
|
||||||
pub tagging: Option<String>,
|
|
||||||
pub retention: Option<String>,
|
|
||||||
pub legalhold: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ReplicationTimestampHeaders {
|
|
||||||
fn from_headers(headers: &HeaderMap) -> Self {
|
|
||||||
Self {
|
|
||||||
tagging: header_value(headers, &SOURCE_TAGGING_TIMESTAMP_HEADERS).map(bounded_journal_value),
|
|
||||||
retention: header_value(headers, &SOURCE_RETENTION_TIMESTAMP_HEADERS).map(bounded_journal_value),
|
|
||||||
legalhold: header_value(headers, &SOURCE_LEGALHOLD_TIMESTAMP_HEADERS).map(bounded_journal_value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Credential-free request metadata retained for deterministic assertions.
|
/// Credential-free request metadata retained for deterministic assertions.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct RequestRecord {
|
pub struct RequestRecord {
|
||||||
@@ -162,7 +131,6 @@ pub struct RequestRecord {
|
|||||||
pub part_number: Option<i32>,
|
pub part_number: Option<i32>,
|
||||||
pub content_length: Option<u64>,
|
pub content_length: Option<u64>,
|
||||||
pub consumed_bytes: Option<usize>,
|
pub consumed_bytes: Option<usize>,
|
||||||
pub replication_timestamps: ReplicationTimestampHeaders,
|
|
||||||
pub fault: Option<FaultAction>,
|
pub fault: Option<FaultAction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,15 +536,7 @@ impl S3Access for FaultAccess {
|
|||||||
.get(CONTENT_LENGTH)
|
.get(CONTENT_LENGTH)
|
||||||
.and_then(|value| value.to_str().ok())
|
.and_then(|value| value.to_str().ok())
|
||||||
.and_then(|value| value.parse().ok());
|
.and_then(|value| value.parse().ok());
|
||||||
let replication_timestamps = ReplicationTimestampHeaders::from_headers(context.headers());
|
let fault = record_request(&self.control, operation, context.method().clone(), parsed, content_length);
|
||||||
let fault = record_request(
|
|
||||||
&self.control,
|
|
||||||
operation,
|
|
||||||
context.method().clone(),
|
|
||||||
parsed,
|
|
||||||
content_length,
|
|
||||||
replication_timestamps,
|
|
||||||
);
|
|
||||||
if let Some(RequestFault {
|
if let Some(RequestFault {
|
||||||
action: FaultAction::Status(status),
|
action: FaultAction::Status(status),
|
||||||
..
|
..
|
||||||
@@ -629,7 +589,6 @@ fn record_request(
|
|||||||
method: Method,
|
method: Method,
|
||||||
parsed: ParsedRequest,
|
parsed: ParsedRequest,
|
||||||
content_length: Option<u64>,
|
content_length: Option<u64>,
|
||||||
replication_timestamps: ReplicationTimestampHeaders,
|
|
||||||
) -> Option<RequestFault> {
|
) -> Option<RequestFault> {
|
||||||
let mut state = lock(control);
|
let mut state = lock(control);
|
||||||
let action = parsed
|
let action = parsed
|
||||||
@@ -654,7 +613,6 @@ fn record_request(
|
|||||||
part_number: parsed.part_number,
|
part_number: parsed.part_number,
|
||||||
content_length,
|
content_length,
|
||||||
consumed_bytes: None,
|
consumed_bytes: None,
|
||||||
replication_timestamps,
|
|
||||||
fault: action.clone(),
|
fault: action.clone(),
|
||||||
});
|
});
|
||||||
action.map(|action| RequestFault { sequence, action })
|
action.map(|action| RequestFault { sequence, action })
|
||||||
@@ -1741,52 +1699,6 @@ mod tests {
|
|||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn journals_replication_timestamp_headers() -> Result<(), BoxError> {
|
|
||||||
let target = FakeS3Target::start().await?;
|
|
||||||
target.create_bucket("target-bucket");
|
|
||||||
let client = client(&target);
|
|
||||||
|
|
||||||
client
|
|
||||||
.put_object()
|
|
||||||
.bucket("target-bucket")
|
|
||||||
.key("plain")
|
|
||||||
.body(ByteStream::from_static(b"plain"))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
client
|
|
||||||
.put_object()
|
|
||||||
.bucket("target-bucket")
|
|
||||||
.key("stamped")
|
|
||||||
.body(ByteStream::from_static(b"stamped"))
|
|
||||||
.customize()
|
|
||||||
.map_request(move |mut request| {
|
|
||||||
let headers = request.headers_mut();
|
|
||||||
headers.insert("x-rustfs-source-replication-tagging-timestamp", "2026-01-02T03:04:05Z");
|
|
||||||
headers.insert("x-minio-source-replication-retention-timestamp", "2026-01-02T03:04:06Z");
|
|
||||||
headers.insert("x-rustfs-source-replication-legalhold-timestamp", "2026-01-02T03:04:07Z");
|
|
||||||
Ok::<_, std::convert::Infallible>(request)
|
|
||||||
})
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let requests = target.requests();
|
|
||||||
let plain = requests
|
|
||||||
.iter()
|
|
||||||
.find(|record| record.operation == Operation::PutObject && record.key.as_deref() == Some("plain"))
|
|
||||||
.expect("plain PUT must be journaled");
|
|
||||||
assert_eq!(plain.replication_timestamps, ReplicationTimestampHeaders::default());
|
|
||||||
|
|
||||||
let stamped = requests
|
|
||||||
.iter()
|
|
||||||
.find(|record| record.operation == Operation::PutObject && record.key.as_deref() == Some("stamped"))
|
|
||||||
.expect("stamped PUT must be journaled");
|
|
||||||
assert_eq!(stamped.replication_timestamps.tagging.as_deref(), Some("2026-01-02T03:04:05Z"));
|
|
||||||
assert_eq!(stamped.replication_timestamps.retention.as_deref(), Some("2026-01-02T03:04:06Z"));
|
|
||||||
assert_eq!(stamped.replication_timestamps.legalhold.as_deref(), Some("2026-01-02T03:04:07Z"));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! assert_sdk_error {
|
macro_rules! assert_sdk_error {
|
||||||
($error:expr, $status:expr, $code:expr) => {{
|
($error:expr, $status:expr, $code:expr) => {{
|
||||||
let error = &$error;
|
let error = &$error;
|
||||||
@@ -3073,7 +2985,6 @@ mod tests {
|
|||||||
part_number: None,
|
part_number: None,
|
||||||
},
|
},
|
||||||
Some(0),
|
Some(0),
|
||||||
ReplicationTimestampHeaders::default(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let records = lock(&control).requests.clone();
|
let records = lock(&control).requests.clone();
|
||||||
@@ -3095,7 +3006,6 @@ mod tests {
|
|||||||
part_number: None,
|
part_number: None,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
ReplicationTimestampHeaders::default(),
|
|
||||||
);
|
);
|
||||||
{
|
{
|
||||||
let bounded_records = lock(&bounded_control);
|
let bounded_records = lock(&bounded_control);
|
||||||
|
|||||||
@@ -1828,36 +1828,33 @@ async fn four_node_compressed_inline_fallback() -> TestResult {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Multipart disk compression is live again, so a compression-enabled cluster classifies multipart objects as compressed and the roundtrip (full GET plus partNumber GET) must still return the original bytes.
|
|
||||||
/// Reverting the multipart compression fix must fail this test.
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn four_node_multipart_disk_compression_roundtrip() -> TestResult {
|
async fn four_node_multipart_ignores_disk_compression_fallback() -> TestResult {
|
||||||
init_logging();
|
init_logging();
|
||||||
|
|
||||||
let collector = OtlpMetricCollector::start().await?;
|
let collector = OtlpMetricCollector::start().await?;
|
||||||
let mut cluster = RustFSTestClusterEnvironment::new(4).await?;
|
let mut cluster = RustFSTestClusterEnvironment::new(4).await?;
|
||||||
configure_reader_metric_cluster(&mut cluster, &collector);
|
configure_reader_metric_cluster(&mut cluster, &collector);
|
||||||
cluster.set_env("RUSTFS_COMPRESSION_ENABLED", "true");
|
cluster.set_env("RUSTFS_COMPRESSION_ENABLED", "true");
|
||||||
cluster.set_env("RUSTFS_COMPRESSION_MULTIPART_ENABLED", "true");
|
|
||||||
cluster.start().await?;
|
cluster.start().await?;
|
||||||
|
|
||||||
let bucket = "inline-multipart-compression-roundtrip";
|
let bucket = "inline-multipart-compression-fallback";
|
||||||
cluster.create_test_bucket(bucket).await?;
|
cluster.create_test_bucket(bucket).await?;
|
||||||
let client = cluster.create_s3_client(0)?;
|
let client = cluster.create_s3_client(0)?;
|
||||||
let key = "multipart/compressed.txt";
|
let key = "multipart/compression-disabled.txt";
|
||||||
let (body, second_part, etag) = put_two_part_multipart(&client, bucket, key).await?;
|
let (body, second_part, etag) = put_two_part_multipart(&client, bucket, key).await?;
|
||||||
|
|
||||||
assert_reader_path(
|
assert_reader_path(
|
||||||
&collector,
|
&collector,
|
||||||
&client,
|
&client,
|
||||||
ReaderPathExpectation::for_class(ReaderObject::new(bucket, key, &body, etag.as_deref(), None), LEGACY_DUPLEX, COMPRESSED),
|
ReaderPathExpectation::for_class(ReaderObject::new(bucket, key, &body, etag.as_deref(), None), LEGACY_DUPLEX, MULTIPART),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_part_number_reader_path(
|
assert_part_number_reader_path(
|
||||||
&collector,
|
&collector,
|
||||||
&client,
|
&client,
|
||||||
PartNumberReaderPathExpectation::new(bucket, key, &second_part, body.len(), COMPRESSED, LEGACY_DUPLEX),
|
PartNumberReaderPathExpectation::new(bucket, key, &second_part, body.len(), MULTIPART, LEGACY_DUPLEX),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -1874,7 +1871,6 @@ async fn four_node_mixed_msgpack_compat_mode_preserves_fallback_controls() -> Te
|
|||||||
let sse_master_key = base64::engine::general_purpose::STANDARD.encode([0x42u8; 32]);
|
let sse_master_key = base64::engine::general_purpose::STANDARD.encode([0x42u8; 32]);
|
||||||
cluster.set_env("RUSTFS_SSE_S3_MASTER_KEY", sse_master_key);
|
cluster.set_env("RUSTFS_SSE_S3_MASTER_KEY", sse_master_key);
|
||||||
cluster.set_env("RUSTFS_COMPRESSION_ENABLED", "true");
|
cluster.set_env("RUSTFS_COMPRESSION_ENABLED", "true");
|
||||||
cluster.set_env("RUSTFS_COMPRESSION_MULTIPART_ENABLED", "true");
|
|
||||||
configure_mixed_msgpack_cluster(&mut cluster, &collector)?;
|
configure_mixed_msgpack_cluster(&mut cluster, &collector)?;
|
||||||
cluster.start().await?;
|
cluster.start().await?;
|
||||||
|
|
||||||
@@ -1894,21 +1890,14 @@ async fn four_node_mixed_msgpack_compat_mode_preserves_fallback_controls() -> Te
|
|||||||
ReaderPathExpectation::for_class(
|
ReaderPathExpectation::for_class(
|
||||||
ReaderObject::new(bucket, multipart_key, &multipart_body, multipart_etag.as_deref(), None),
|
ReaderObject::new(bucket, multipart_key, &multipart_body, multipart_etag.as_deref(), None),
|
||||||
LEGACY_DUPLEX,
|
LEGACY_DUPLEX,
|
||||||
COMPRESSED,
|
MULTIPART,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_part_number_reader_path(
|
assert_part_number_reader_path(
|
||||||
&collector,
|
&collector,
|
||||||
&client,
|
&client,
|
||||||
PartNumberReaderPathExpectation::new(
|
PartNumberReaderPathExpectation::new(bucket, multipart_key, &second_part, multipart_body.len(), MULTIPART, LEGACY_DUPLEX),
|
||||||
bucket,
|
|
||||||
multipart_key,
|
|
||||||
&second_part,
|
|
||||||
multipart_body.len(),
|
|
||||||
COMPRESSED,
|
|
||||||
LEGACY_DUPLEX,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_msgpack_decode_observed(&collector, &decode_before).await?;
|
assert_msgpack_decode_observed(&collector, &decode_before).await?;
|
||||||
@@ -2364,11 +2353,7 @@ async fn four_node_mixed_msgpack_compat_mode_preserves_fallback_controls_during_
|
|||||||
hot_client.create_bucket().bucket(bucket).send().await?;
|
hot_client.create_bucket().bucket(bucket).send().await?;
|
||||||
put_lifecycle_with_transition_retry(&hot_client, bucket, &tier_name).await?;
|
put_lifecycle_with_transition_retry(&hot_client, bucket, &tier_name).await?;
|
||||||
|
|
||||||
// `.zip` sits on the disk-compression exclusion list: this test pins
|
let key = "transition/mixed-multipart.bin";
|
||||||
// msgpack compat controls across ILM transition, and a compressed object
|
|
||||||
// would classify as `compressed` instead of `remote` (and the warm-tier
|
|
||||||
// read path does not decode compression — tracked separately).
|
|
||||||
let key = "transition/mixed-multipart.zip";
|
|
||||||
let (body, second_part, etag) = put_two_part_multipart(&hot_client, bucket, key).await?;
|
let (body, second_part, etag) = put_two_part_multipart(&hot_client, bucket, key).await?;
|
||||||
wait_for_transition(&hot_client, bucket, key, &tier_name).await?;
|
wait_for_transition(&hot_client, bucket, key, &tier_name).await?;
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ async fn start_enforcing_ilm_server(env: &mut LocalKMSTestEnvironment) -> TestRe
|
|||||||
|
|
||||||
let envs = [
|
let envs = [
|
||||||
("RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS", "true"),
|
("RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS", "true"),
|
||||||
("RUSTFS_KMS_ENFORCE_SSE_KEY_POLICY", "true"),
|
("RUSTFS_KMS_ENFORCE_SSE_KEY_POLICY", "false"),
|
||||||
("RUSTFS_SCANNER_CYCLE", "1"),
|
("RUSTFS_SCANNER_CYCLE", "1"),
|
||||||
("RUSTFS_ILM_PROCESS_TIME", "1"),
|
("RUSTFS_ILM_PROCESS_TIME", "1"),
|
||||||
("RUSTFS_ILM_DEBUG_DAY_SECS", "2"),
|
("RUSTFS_ILM_DEBUG_DAY_SECS", "2"),
|
||||||
@@ -486,6 +486,7 @@ async fn ilm_expiration_on_sse_kms_bucket_under_enforcement() -> TestResult {
|
|||||||
/// depend on scanner scheduling; the 1s scanner cycle stays on as a backstop.
|
/// depend on scanner scheduling; the 1s scanner cycle stays on as a backstop.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
|
#[ignore = "pins rustfs/rustfs#6025: GET on a transitioned managed-SSE object silently returns corrupt bytes (fails with enforcement on AND off, so it is not an authorization regression); un-ignore with the fix"]
|
||||||
async fn ilm_transition_on_sse_kms_bucket_under_enforcement_reads_back() -> TestResult {
|
async fn ilm_transition_on_sse_kms_bucket_under_enforcement_reads_back() -> TestResult {
|
||||||
init_logging();
|
init_logging();
|
||||||
|
|
||||||
|
|||||||
@@ -131,19 +131,16 @@ pub mod bucket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod metadata_sys {
|
pub mod metadata_sys {
|
||||||
#[cfg(feature = "test-util")]
|
|
||||||
pub use crate::bucket::metadata_sys::ConfigWriteLockProbe;
|
|
||||||
pub use crate::bucket::metadata_sys::{
|
pub use crate::bucket::metadata_sys::{
|
||||||
BucketMetadataMutationGuard, BucketMetadataSys, ObjectLockConfigState, acquire_bucket_metadata_transaction_lock,
|
BucketMetadataMutationGuard, BucketMetadataSys, ObjectLockConfigState, acquire_bucket_metadata_transaction_lock,
|
||||||
acquire_bucket_metadata_transaction_lock_for_incarnation, capture_bucket_metadata_incarnation, delete,
|
capture_bucket_metadata_incarnation, delete, delete_if_incarnation, get, get_accelerate_config, get_bucket_policy,
|
||||||
delete_if_incarnation, delete_under_transaction_lock, get, get_accelerate_config, get_bucket_policy,
|
|
||||||
get_bucket_policy_raw, get_bucket_targets_config, get_config_from_disk, get_cors_config, get_durability_config,
|
get_bucket_policy_raw, get_bucket_targets_config, get_config_from_disk, get_cors_config, get_durability_config,
|
||||||
get_global_bucket_metadata_sys, get_lifecycle_config, get_logging_config, get_notification_config,
|
get_global_bucket_metadata_sys, get_lifecycle_config, get_logging_config, get_notification_config,
|
||||||
get_object_lock_config, get_object_lock_config_state, get_public_access_block_config, get_quota_config,
|
get_object_lock_config, get_object_lock_config_state, get_public_access_block_config, get_quota_config,
|
||||||
get_replication_config, get_request_payment_config, get_sse_config, get_tagging_config, get_versioning_config,
|
get_replication_config, get_request_payment_config, get_sse_config, get_tagging_config, get_versioning_config,
|
||||||
get_website_config, init_bucket_metadata_sys, list_bucket_targets, reload_bucket_metadata, remove_bucket_metadata,
|
get_website_config, init_bucket_metadata_sys, list_bucket_targets, reload_bucket_metadata, remove_bucket_metadata,
|
||||||
set_bucket_metadata, update, update_bucket_targets_under_transaction_lock, update_config_with, update_if_incarnation,
|
set_bucket_metadata, update, update_bucket_targets_under_transaction_lock, update_config_with, update_if_incarnation,
|
||||||
update_quota_if_incarnation, update_under_transaction_lock,
|
update_under_transaction_lock,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,18 +182,17 @@ pub mod bucket {
|
|||||||
mrf_backlog_observability_snapshot,
|
mrf_backlog_observability_snapshot,
|
||||||
};
|
};
|
||||||
pub use crate::bucket::replication::{
|
pub use crate::bucket::replication::{
|
||||||
BucketReplicationResyncStatus, BucketReplicationStat, BucketReplicationStats, BucketStats,
|
BucketReplicationResyncStatus, BucketReplicationStats, BucketStats, DeleteReplicationConfigSnapshot,
|
||||||
DeleteReplicationConfigSnapshot, DeletedObjectReplicationInfo, DurableMrfBacklog, DynReplicationPool, InQueueMetric,
|
DeletedObjectReplicationInfo, DurableMrfBacklog, DynReplicationPool, MrfOpKind, MrfReplicateEntry,
|
||||||
MrfOpKind, MrfReplicateEntry, MustReplicateOptions, ObjectOpts, REMOTE_TARGET_CAPABILITY_CONTRACT_VERSION,
|
MustReplicateOptions, ObjectOpts, REMOTE_TARGET_CAPABILITY_CONTRACT_VERSION, REMOTE_TARGET_UNSUPPORTED_FIELDS,
|
||||||
REMOTE_TARGET_UNSUPPORTED_FIELDS, REMOTE_TARGET_WRITABLE_FIELDS, REPLICATE_INCOMING_DELETE,
|
REMOTE_TARGET_WRITABLE_FIELDS, REPLICATE_INCOMING_DELETE, REPLICATION_CAPABILITY_CONTRACT_VERSION,
|
||||||
REPLICATION_CAPABILITY_CONTRACT_VERSION, REPLICATION_READ_ONLY_HISTORICAL_FIELDS, REPLICATION_WRITABLE_FIELDS,
|
REPLICATION_READ_ONLY_HISTORICAL_FIELDS, REPLICATION_WRITABLE_FIELDS, ReplicateDecision, ReplicateObjectInfo,
|
||||||
ReplicateDecision, ReplicateObjectInfo, ReplicationBatchAdmission, ReplicationConfig,
|
ReplicationBatchAdmission, ReplicationConfig, ReplicationConfigStructureError, ReplicationConfigurationExt,
|
||||||
ReplicationConfigStructureError, ReplicationConfigurationExt, ReplicationDeleteScheduleInput,
|
ReplicationDeleteScheduleInput, ReplicationDeleteStateSource, ReplicationHealQueueResult, ReplicationObjectBridge,
|
||||||
ReplicationDeleteStateSource, ReplicationHealQueueResult, ReplicationObjectBridge, ReplicationObjectIO,
|
ReplicationObjectIO, ReplicationOperation, ReplicationPoolTrait, ReplicationPriority, ReplicationQueueAdmission,
|
||||||
ReplicationOperation, ReplicationPoolTrait, ReplicationPriority, ReplicationQueueAdmission, ReplicationScannerBridge,
|
ReplicationScannerBridge, ReplicationState, ReplicationStats, ReplicationStatusType, ReplicationStorage,
|
||||||
ReplicationState, ReplicationStats, ReplicationStatusType, ReplicationStorage, ReplicationTargetValidationError,
|
ReplicationTargetValidationError, ReplicationType, ResyncOpts, ResyncStatusType, RuntimeReplicationTargetBacklog,
|
||||||
ReplicationType, ResyncOpts, ResyncStatusType, RuntimeReplicationTargetBacklog, TargetReplicationResyncStatus,
|
TargetReplicationResyncStatus, VersionPurgeStatusType, commit_force_delete_intent, complete_force_delete_intent,
|
||||||
VersionPurgeStatusType, XferStats, commit_force_delete_intent, complete_force_delete_intent,
|
|
||||||
delete_replication_state_from_config, delete_replication_version_id, get_global_replication_pool,
|
delete_replication_state_from_config, delete_replication_version_id, get_global_replication_pool,
|
||||||
get_global_replication_stats, init_background_replication, invalid_replication_config_status_field,
|
get_global_replication_stats, init_background_replication, invalid_replication_config_status_field,
|
||||||
persist_force_delete_intent, read_durable_mrf_backlog, replication_state_to_filemeta, replication_status_to_filemeta,
|
persist_force_delete_intent, read_durable_mrf_backlog, replication_state_to_filemeta, replication_status_to_filemeta,
|
||||||
@@ -280,9 +276,7 @@ pub mod cluster {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod compression {
|
pub mod compression {
|
||||||
pub use crate::io_support::compress::{
|
pub use crate::io_support::compress::{MIN_DISK_COMPRESSIBLE_SIZE, is_disk_compressible, is_disk_compression_enabled};
|
||||||
MIN_DISK_COMPRESSIBLE_SIZE, is_disk_compressible, is_disk_compression_enabled, is_multipart_disk_compression_enabled,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod config {
|
pub mod config {
|
||||||
@@ -322,7 +316,7 @@ pub mod data_usage {
|
|||||||
DATA_USAGE_CACHE_NAME, apply_bucket_usage_memory_overlay, compute_bucket_usage,
|
DATA_USAGE_CACHE_NAME, apply_bucket_usage_memory_overlay, compute_bucket_usage,
|
||||||
init_compression_total_memory_from_backend, invalidate_admin_data_usage_snapshot_cache,
|
init_compression_total_memory_from_backend, invalidate_admin_data_usage_snapshot_cache,
|
||||||
invalidate_data_usage_snapshot_cache, live_bucket_usage_computations, load_admin_data_usage_from_backend_cached,
|
invalidate_data_usage_snapshot_cache, live_bucket_usage_computations, load_admin_data_usage_from_backend_cached,
|
||||||
load_compression_total_from_memory, load_data_usage_from_backend, load_data_usage_from_backend_cached, quota_object_size,
|
load_compression_total_from_memory, load_data_usage_from_backend, load_data_usage_from_backend_cached,
|
||||||
record_bucket_delete_marker_memory, record_bucket_object_delete_memory, record_bucket_object_version_write_memory,
|
record_bucket_delete_marker_memory, record_bucket_object_delete_memory, record_bucket_object_version_write_memory,
|
||||||
record_bucket_object_write_memory, record_bucket_object_write_unknown_previous_memory, record_compression_total_memory,
|
record_bucket_object_write_memory, record_bucket_object_write_unknown_previous_memory, record_compression_total_memory,
|
||||||
refresh_bucket_usage_from_object_layer, refresh_versioned_bucket_usage_from_object_layer,
|
refresh_bucket_usage_from_object_layer, refresh_versioned_bucket_usage_from_object_layer,
|
||||||
@@ -409,11 +403,8 @@ pub mod metrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod notification {
|
pub mod notification {
|
||||||
#[cfg(any(test, feature = "test-util"))]
|
|
||||||
pub use crate::services::notification_sys::rotate_cross_pool_fence_fleet_proof_for_test;
|
|
||||||
pub use crate::services::notification_sys::{
|
pub use crate::services::notification_sys::{
|
||||||
CrossPoolFenceFleetProofToken, NotificationPeerErr, NotificationSys, acquire_cross_pool_fence_fleet_proof,
|
NotificationPeerErr, NotificationSys, get_global_notification_sys, new_global_notification_sys,
|
||||||
cross_pool_fence_fleet_proof_matches, get_global_notification_sys, new_global_notification_sys,
|
|
||||||
start_remote_version_state_fleet_probe,
|
start_remote_version_state_fleet_probe,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -473,8 +464,7 @@ pub mod set_disk {
|
|||||||
|
|
||||||
#[cfg(feature = "test-util")]
|
#[cfg(feature = "test-util")]
|
||||||
pub mod test_util {
|
pub mod test_util {
|
||||||
pub use crate::bucket::quota::reservation::fail_next_quota_ledger_save_for_test;
|
pub use crate::set_disk::{PutObjectCommitBarrier, PutObjectCommitPause};
|
||||||
pub use crate::set_disk::{MultipartCommitBarrier, MultipartCommitPause, PutObjectCommitBarrier, PutObjectCommitPause};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,9 +58,7 @@ use rustfs_utils::http::{
|
|||||||
};
|
};
|
||||||
use rustfs_utils::http::{
|
use rustfs_utils::http::{
|
||||||
SUFFIX_FORCE_DELETE, SUFFIX_SOURCE_DELETEMARKER, SUFFIX_SOURCE_ETAG, SUFFIX_SOURCE_MTIME, SUFFIX_SOURCE_REPLICATION_CHECK,
|
SUFFIX_FORCE_DELETE, SUFFIX_SOURCE_DELETEMARKER, SUFFIX_SOURCE_ETAG, SUFFIX_SOURCE_MTIME, SUFFIX_SOURCE_REPLICATION_CHECK,
|
||||||
SUFFIX_SOURCE_REPLICATION_LEGALHOLD_TIMESTAMP, SUFFIX_SOURCE_REPLICATION_REQUEST,
|
SUFFIX_SOURCE_REPLICATION_REQUEST, SUFFIX_SOURCE_VERSION_ID, insert_header,
|
||||||
SUFFIX_SOURCE_REPLICATION_RETENTION_TIMESTAMP, SUFFIX_SOURCE_REPLICATION_TAGGING_TIMESTAMP, SUFFIX_SOURCE_VERSION_ID,
|
|
||||||
insert_header,
|
|
||||||
};
|
};
|
||||||
use rustls_pki_types::pem::PemObject;
|
use rustls_pki_types::pem::PemObject;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -1478,12 +1476,9 @@ impl Default for AdvancedPutOptions {
|
|||||||
replication_status: ReplicationStatusType::Pending,
|
replication_status: ReplicationStatusType::Pending,
|
||||||
source_mtime: OffsetDateTime::now_utc(),
|
source_mtime: OffsetDateTime::now_utc(),
|
||||||
replication_request: false,
|
replication_request: false,
|
||||||
// UNIX_EPOCH means "never modified": header() must not emit a
|
retention_timestamp: OffsetDateTime::now_utc(),
|
||||||
// timestamp header for it, otherwise a receiver would treat an
|
tagging_timestamp: OffsetDateTime::now_utc(),
|
||||||
// unset category as a modification made right now.
|
legalhold_timestamp: OffsetDateTime::now_utc(),
|
||||||
retention_timestamp: OffsetDateTime::UNIX_EPOCH,
|
|
||||||
tagging_timestamp: OffsetDateTime::UNIX_EPOCH,
|
|
||||||
legalhold_timestamp: OffsetDateTime::UNIX_EPOCH,
|
|
||||||
replication_validity_check: false,
|
replication_validity_check: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1680,16 +1675,6 @@ impl PutObjectOptions {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (suffix, timestamp) in [
|
|
||||||
(SUFFIX_SOURCE_REPLICATION_TAGGING_TIMESTAMP, self.internal.tagging_timestamp),
|
|
||||||
(SUFFIX_SOURCE_REPLICATION_RETENTION_TIMESTAMP, self.internal.retention_timestamp),
|
|
||||||
(SUFFIX_SOURCE_REPLICATION_LEGALHOLD_TIMESTAMP, self.internal.legalhold_timestamp),
|
|
||||||
] {
|
|
||||||
if timestamp.unix_timestamp() != 0 {
|
|
||||||
insert_header(&mut header, suffix, timestamp.format(&Rfc3339).unwrap_or_default());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.internal.replication_request {
|
if self.internal.replication_request {
|
||||||
insert_header(&mut header, SUFFIX_SOURCE_REPLICATION_REQUEST, "true");
|
insert_header(&mut header, SUFFIX_SOURCE_REPLICATION_REQUEST, "true");
|
||||||
}
|
}
|
||||||
@@ -2857,57 +2842,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn put_object_headers_carry_replication_timestamp_headers() {
|
|
||||||
// MinIO receivers resolve concurrent tag/retention/legal-hold edits by
|
|
||||||
// last-writer-wins on these headers (object-api-options.go parses them
|
|
||||||
// as RFC3339); a replica without them loses every conflict resolution.
|
|
||||||
let mut opts = PutObjectOptions::default();
|
|
||||||
opts.internal.replication_request = true;
|
|
||||||
let tagging = OffsetDateTime::from_unix_timestamp(1_700_000_001).expect("valid timestamp");
|
|
||||||
let retention = OffsetDateTime::from_unix_timestamp(1_700_000_002).expect("valid timestamp");
|
|
||||||
let legalhold = OffsetDateTime::from_unix_timestamp(1_700_000_003).expect("valid timestamp");
|
|
||||||
opts.internal.tagging_timestamp = tagging;
|
|
||||||
opts.internal.retention_timestamp = retention;
|
|
||||||
opts.internal.legalhold_timestamp = legalhold;
|
|
||||||
|
|
||||||
let header = opts.header();
|
|
||||||
for (suffix, expected) in [
|
|
||||||
("source-replication-tagging-timestamp", tagging),
|
|
||||||
("source-replication-retention-timestamp", retention),
|
|
||||||
("source-replication-legalhold-timestamp", legalhold),
|
|
||||||
] {
|
|
||||||
assert_eq!(
|
|
||||||
rustfs_utils::http::get_header(&header, suffix).as_deref(),
|
|
||||||
Some(expected.format(&Rfc3339).expect("RFC3339 timestamp").as_str()),
|
|
||||||
"replication put requests must carry the {suffix} header"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn put_object_headers_omit_unset_replication_timestamps() {
|
|
||||||
// UNIX_EPOCH means "never modified on the source"; sending it would
|
|
||||||
// make the receiver treat an unset category as a fresh modification.
|
|
||||||
let mut opts = PutObjectOptions::default();
|
|
||||||
opts.internal.replication_request = true;
|
|
||||||
opts.internal.tagging_timestamp = OffsetDateTime::UNIX_EPOCH;
|
|
||||||
opts.internal.retention_timestamp = OffsetDateTime::UNIX_EPOCH;
|
|
||||||
opts.internal.legalhold_timestamp = OffsetDateTime::UNIX_EPOCH;
|
|
||||||
|
|
||||||
let header = opts.header();
|
|
||||||
for suffix in [
|
|
||||||
"source-replication-tagging-timestamp",
|
|
||||||
"source-replication-retention-timestamp",
|
|
||||||
"source-replication-legalhold-timestamp",
|
|
||||||
] {
|
|
||||||
assert!(
|
|
||||||
rustfs_utils::http::get_header(&header, suffix).is_none(),
|
|
||||||
"unset {suffix} must not be sent to replication targets"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn get_remote_target_client_internal_rejects_loopback_endpoint() {
|
async fn get_remote_target_client_internal_rejects_loopback_endpoint() {
|
||||||
let sys = BucketTargetSys::default();
|
let sys = BucketTargetSys::default();
|
||||||
|
|||||||
@@ -46,13 +46,15 @@ use crate::bucket::lifecycle::transition_transaction::run_transition_transaction
|
|||||||
use crate::bucket::object_lock::ObjectLockApi;
|
use crate::bucket::object_lock::ObjectLockApi;
|
||||||
use crate::bucket::versioning::VersioningApi as _;
|
use crate::bucket::versioning::VersioningApi as _;
|
||||||
use crate::bucket::versioning_sys::BucketVersioningSys;
|
use crate::bucket::versioning_sys::BucketVersioningSys;
|
||||||
|
use crate::client::object_api_utils::new_getobjectreader;
|
||||||
use crate::disk::error::DiskError;
|
use crate::disk::error::DiskError;
|
||||||
use crate::disk::{DeleteOptions, Disk, DiskAPI, RUSTFS_META_BUCKET, RUSTFS_META_MULTIPART_BUCKET, STORAGE_FORMAT_FILE};
|
use crate::disk::{DeleteOptions, Disk, DiskAPI, RUSTFS_META_BUCKET, RUSTFS_META_MULTIPART_BUCKET, STORAGE_FORMAT_FILE};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::error::StorageError;
|
use crate::error::StorageError;
|
||||||
use crate::error::{is_err_object_not_found, is_err_read_quorum, is_err_version_not_found, is_network_or_host_down};
|
use crate::error::{
|
||||||
|
error_resp_to_object_err, is_err_object_not_found, is_err_read_quorum, is_err_version_not_found, is_network_or_host_down,
|
||||||
|
};
|
||||||
use crate::object_api::{GetObjectReader, ObjectInfo, ObjectOptions};
|
use crate::object_api::{GetObjectReader, ObjectInfo, ObjectOptions};
|
||||||
use crate::object_api::{ObjectEncryptionResolver, ReadPlan};
|
|
||||||
use crate::services::tier::{
|
use crate::services::tier::{
|
||||||
tier::{TierConfigMgr, TierOperationLease, tier_destination_id_from_metadata},
|
tier::{TierConfigMgr, TierOperationLease, tier_destination_id_from_metadata},
|
||||||
warm_backend::WarmBackendGetOpts,
|
warm_backend::WarmBackendGetOpts,
|
||||||
@@ -4398,10 +4400,9 @@ pub async fn get_transitioned_object_reader(
|
|||||||
h: &HeaderMap,
|
h: &HeaderMap,
|
||||||
oi: &ObjectInfo,
|
oi: &ObjectInfo,
|
||||||
opts: &ObjectOptions,
|
opts: &ObjectOptions,
|
||||||
resolver: Option<&dyn ObjectEncryptionResolver>,
|
|
||||||
) -> Result<GetObjectReader, std::io::Error> {
|
) -> Result<GetObjectReader, std::io::Error> {
|
||||||
let tier_config_mgr = runtime_sources::tier_config_mgr_handle();
|
let tier_config_mgr = runtime_sources::tier_config_mgr_handle();
|
||||||
get_transitioned_object_reader_with_tier_manager(bucket, object, rs, h, oi, opts, &tier_config_mgr, resolver).await
|
get_transitioned_object_reader_with_tier_manager(bucket, object, rs, h, oi, opts, &tier_config_mgr).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_transition_remote_version(oi: &ObjectInfo) -> Result<bool, std::io::Error> {
|
fn validate_transition_remote_version(oi: &ObjectInfo) -> Result<bool, std::io::Error> {
|
||||||
@@ -4421,10 +4422,6 @@ fn validate_transition_remote_version(oi: &ObjectInfo) -> Result<bool, std::io::
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The resolver joins the tier manager as the second injected port this read
|
|
||||||
// needs; grouping the request half into a struct would churn every call site of
|
|
||||||
// a bug fix.
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub(crate) async fn get_transitioned_object_reader_with_tier_manager(
|
pub(crate) async fn get_transitioned_object_reader_with_tier_manager(
|
||||||
bucket: &str,
|
bucket: &str,
|
||||||
object: &str,
|
object: &str,
|
||||||
@@ -4433,7 +4430,6 @@ pub(crate) async fn get_transitioned_object_reader_with_tier_manager(
|
|||||||
oi: &ObjectInfo,
|
oi: &ObjectInfo,
|
||||||
opts: &ObjectOptions,
|
opts: &ObjectOptions,
|
||||||
tier_config_mgr: &Arc<RwLock<TierConfigMgr>>,
|
tier_config_mgr: &Arc<RwLock<TierConfigMgr>>,
|
||||||
resolver: Option<&dyn ObjectEncryptionResolver>,
|
|
||||||
) -> Result<GetObjectReader, std::io::Error> {
|
) -> Result<GetObjectReader, std::io::Error> {
|
||||||
validate_transition_remote_version(oi)?;
|
validate_transition_remote_version(oi)?;
|
||||||
let expected_identity = tier_destination_id_from_metadata(&oi.user_defined)?;
|
let expected_identity = tier_destination_id_from_metadata(&oi.user_defined)?;
|
||||||
@@ -4451,16 +4447,11 @@ pub(crate) async fn get_transitioned_object_reader_with_tier_manager(
|
|||||||
|
|
||||||
tgt_client.validate_remote_version_id(&oi.transitioned_object.version_id)?;
|
tgt_client.validate_remote_version_id(&oi.transitioned_object.version_id)?;
|
||||||
|
|
||||||
// The same read plan the local path uses, so the tier fetch is positioned in
|
let ret = new_getobjectreader(rs, oi, opts, h);
|
||||||
// the object's *stored* coordinate system and the stream is handed the same
|
if let Err(err) = ret {
|
||||||
// decrypt/decompress transforms. Reading an encrypted object's ciphertext
|
return Err(error_resp_to_object_err(err, vec![bucket, object]));
|
||||||
// through a plaintext-coordinate range and skipping the transform is how a
|
}
|
||||||
// transitioned SSE object used to come back as silently corrupt bytes of the
|
let (get_fn, off, length) = ret.expect("get_transitioned_object_reader should succeed after error check");
|
||||||
// right length (rustfs/rustfs#6025).
|
|
||||||
let plan = ReadPlan::build_for_request(rs.clone(), oi, opts, h, resolver)
|
|
||||||
.await
|
|
||||||
.map_err(|err| std::io::Error::other(format!("building the read plan for {bucket}/{object} failed: {err}")))?;
|
|
||||||
let (off, length) = (plan.storage_offset() as i64, plan.storage_length());
|
|
||||||
let mut gopts = WarmBackendGetOpts::default();
|
let mut gopts = WarmBackendGetOpts::default();
|
||||||
|
|
||||||
if off >= 0 && length >= 0 {
|
if off >= 0 && length >= 0 {
|
||||||
@@ -4497,10 +4488,7 @@ pub(crate) async fn get_transitioned_object_reader_with_tier_manager(
|
|||||||
);
|
);
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
let object_reader = plan
|
Ok(attach_tier_operation_lease(get_fn(reader, h.clone()), tgt_client))
|
||||||
.into_object_reader(Box::new(reader), oi)
|
|
||||||
.map_err(|err| std::io::Error::other(format!("wrapping the tier stream for {bucket}/{object} failed: {err}")))?;
|
|
||||||
Ok(attach_tier_operation_lease(object_reader, tgt_client))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TierOperationLeaseReader {
|
struct TierOperationLeaseReader {
|
||||||
@@ -5788,7 +5776,6 @@ mod tests {
|
|||||||
&object_info,
|
&object_info,
|
||||||
&ObjectOptions::default(),
|
&ObjectOptions::default(),
|
||||||
&manager,
|
&manager,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("transitioned reader should open");
|
.expect("transitioned reader should open");
|
||||||
@@ -5853,7 +5840,6 @@ mod tests {
|
|||||||
&object_info,
|
&object_info,
|
||||||
&ObjectOptions::default(),
|
&ObjectOptions::default(),
|
||||||
&manager,
|
&manager,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -5894,7 +5880,6 @@ mod tests {
|
|||||||
&object_info,
|
&object_info,
|
||||||
&ObjectOptions::default(),
|
&ObjectOptions::default(),
|
||||||
&manager,
|
&manager,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -6132,7 +6117,6 @@ mod tests {
|
|||||||
&oi,
|
&oi,
|
||||||
&ObjectOptions::default(),
|
&ObjectOptions::default(),
|
||||||
&manager,
|
&manager,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -6156,7 +6140,6 @@ mod tests {
|
|||||||
&oi,
|
&oi,
|
||||||
&ObjectOptions::default(),
|
&ObjectOptions::default(),
|
||||||
&manager,
|
&manager,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,72 +50,6 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
const BUCKET_METADATA_REFRESH_INTERVAL: Duration = Duration::from_secs(15 * 60);
|
const BUCKET_METADATA_REFRESH_INTERVAL: Duration = Duration::from_secs(15 * 60);
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-util"))]
|
|
||||||
struct ConfigWriteLockProbeState {
|
|
||||||
bucket: String,
|
|
||||||
arrived: tokio::sync::Notify,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-util"))]
|
|
||||||
static CONFIG_WRITE_LOCK_PROBES: std::sync::OnceLock<StdMutex<Vec<Arc<ConfigWriteLockProbeState>>>> = std::sync::OnceLock::new();
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-util"))]
|
|
||||||
pub struct ConfigWriteLockProbe {
|
|
||||||
state: Arc<ConfigWriteLockProbeState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-util"))]
|
|
||||||
impl ConfigWriteLockProbe {
|
|
||||||
pub fn install(bucket: &str) -> Self {
|
|
||||||
let state = Arc::new(ConfigWriteLockProbeState {
|
|
||||||
bucket: bucket.to_string(),
|
|
||||||
arrived: tokio::sync::Notify::new(),
|
|
||||||
});
|
|
||||||
let mut probes = CONFIG_WRITE_LOCK_PROBES
|
|
||||||
.get_or_init(|| StdMutex::new(Vec::new()))
|
|
||||||
.lock()
|
|
||||||
.expect("config write lock probe mutex should not poison");
|
|
||||||
assert!(
|
|
||||||
!probes.iter().any(|current| current.bucket == state.bucket),
|
|
||||||
"config write lock probe must be unique for a bucket"
|
|
||||||
);
|
|
||||||
probes.push(Arc::clone(&state));
|
|
||||||
drop(probes);
|
|
||||||
Self { state }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn wait_until_attempted(&self) {
|
|
||||||
tokio::time::timeout(Duration::from_secs(30), self.state.arrived.notified())
|
|
||||||
.await
|
|
||||||
.expect("bucket config update should attempt the transaction lock");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-util"))]
|
|
||||||
impl Drop for ConfigWriteLockProbe {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let mut probes = CONFIG_WRITE_LOCK_PROBES
|
|
||||||
.get_or_init(|| StdMutex::new(Vec::new()))
|
|
||||||
.lock()
|
|
||||||
.expect("config write lock probe mutex should not poison");
|
|
||||||
probes.retain(|state| !Arc::ptr_eq(state, &self.state));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-util"))]
|
|
||||||
fn notify_config_write_lock_attempt(bucket: &str) {
|
|
||||||
let probe = CONFIG_WRITE_LOCK_PROBES
|
|
||||||
.get_or_init(|| StdMutex::new(Vec::new()))
|
|
||||||
.lock()
|
|
||||||
.expect("config write lock probe mutex should not poison")
|
|
||||||
.iter()
|
|
||||||
.find(|probe| probe.bucket == bucket)
|
|
||||||
.cloned();
|
|
||||||
if let Some(probe) = probe {
|
|
||||||
probe.arrived.notify_one();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum MetadataLoadMode {
|
enum MetadataLoadMode {
|
||||||
Initial,
|
Initial,
|
||||||
@@ -656,41 +590,6 @@ pub async fn update_under_transaction_lock(
|
|||||||
update_under_config_write_guard(get_bucket_metadata_sys()?, guard, config_file, data).await
|
update_under_config_write_guard(get_bucket_metadata_sys()?, guard, config_file, data).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear one config file while the caller holds this bucket's transaction lock.
|
|
||||||
pub async fn delete_under_transaction_lock(
|
|
||||||
guard: &BucketMetadataMutationGuard,
|
|
||||||
bucket: &str,
|
|
||||||
config_file: &str,
|
|
||||||
) -> Result<OffsetDateTime> {
|
|
||||||
guard.ensure_valid(bucket)?;
|
|
||||||
delete_under_config_write_guard(get_bucket_metadata_sys()?, guard, config_file).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_quota_if_incarnation(
|
|
||||||
bucket: &str,
|
|
||||||
data: Vec<u8>,
|
|
||||||
expected_incarnation_id: Uuid,
|
|
||||||
proof: &crate::services::notification_sys::CrossPoolFenceFleetProofToken,
|
|
||||||
) -> Result<OffsetDateTime> {
|
|
||||||
let sys = get_bucket_metadata_sys()?;
|
|
||||||
let guard = Box::pin(acquire_config_write_guard_for_incarnation(
|
|
||||||
sys.clone(),
|
|
||||||
bucket,
|
|
||||||
Some(expected_incarnation_id),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
if !crate::services::notification_sys::cross_pool_fence_fleet_proof_matches(proof) {
|
|
||||||
return Err(Error::NamespaceLockQuorumUnavailable {
|
|
||||||
mode: "quota_capability",
|
|
||||||
bucket: bucket.to_string(),
|
|
||||||
object: rustfs_config::QUOTA_CONFIG_FILE.to_string(),
|
|
||||||
required: 1,
|
|
||||||
achieved: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
update_under_config_write_guard(sys, &guard, rustfs_config::QUOTA_CONFIG_FILE, data).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_bucket_targets_under_transaction_lock(
|
pub async fn update_bucket_targets_under_transaction_lock(
|
||||||
guard: &BucketMetadataMutationGuard,
|
guard: &BucketMetadataMutationGuard,
|
||||||
bucket: &str,
|
bucket: &str,
|
||||||
@@ -805,14 +704,6 @@ pub async fn acquire_bucket_metadata_transaction_lock(bucket: &str) -> Result<Bu
|
|||||||
acquire_config_write_guard(get_bucket_metadata_sys()?, bucket).await
|
acquire_config_write_guard(get_bucket_metadata_sys()?, bucket).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Acquire the bucket transaction lock only if its incarnation still matches.
|
|
||||||
pub async fn acquire_bucket_metadata_transaction_lock_for_incarnation(
|
|
||||||
bucket: &str,
|
|
||||||
expected_incarnation_id: Uuid,
|
|
||||||
) -> Result<BucketMetadataMutationGuard> {
|
|
||||||
acquire_config_write_guard_for_incarnation(get_bucket_metadata_sys()?, bucket, Some(expected_incarnation_id)).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn acquire_bucket_metadata_transaction_lock_in(
|
pub(crate) async fn acquire_bucket_metadata_transaction_lock_in(
|
||||||
ctx: &crate::runtime::instance::InstanceContext,
|
ctx: &crate::runtime::instance::InstanceContext,
|
||||||
bucket: &str,
|
bucket: &str,
|
||||||
@@ -843,26 +734,7 @@ async fn acquire_transaction_lock_with_sys(
|
|||||||
let lock = api
|
let lock = api
|
||||||
.new_ns_lock(RUSTFS_META_BUCKET, &bucket_metadata_transaction_lock_key(bucket))
|
.new_ns_lock(RUSTFS_META_BUCKET, &bucket_metadata_transaction_lock_key(bucket))
|
||||||
.await?;
|
.await?;
|
||||||
let acquire = lock.get_write_lock(crate::set_disk::get_lock_acquire_timeout());
|
Ok(lock.get_write_lock(crate::set_disk::get_lock_acquire_timeout()).await?)
|
||||||
#[cfg(any(test, feature = "test-util"))]
|
|
||||||
{
|
|
||||||
tokio::pin!(acquire);
|
|
||||||
let mut notified = false;
|
|
||||||
let guard = futures::future::poll_fn(|cx| match std::future::Future::poll(acquire.as_mut(), cx) {
|
|
||||||
std::task::Poll::Pending => {
|
|
||||||
if !notified {
|
|
||||||
notify_config_write_lock_attempt(bucket);
|
|
||||||
notified = true;
|
|
||||||
}
|
|
||||||
std::task::Poll::Pending
|
|
||||||
}
|
|
||||||
std::task::Poll::Ready(result) => std::task::Poll::Ready(result),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(guard)
|
|
||||||
}
|
|
||||||
#[cfg(not(any(test, feature = "test-util")))]
|
|
||||||
Ok(acquire.await?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The lock resource name is deliberately still the `bucket-targets` one it
|
/// The lock resource name is deliberately still the `bucket-targets` one it
|
||||||
@@ -1017,37 +889,6 @@ pub(crate) async fn get_object_lock_config_and_incarnation_from_disk_in(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Re-read the quota configuration and bucket incarnation from the same
|
|
||||||
/// authoritative metadata blob while the caller holds the bucket metadata
|
|
||||||
/// transaction read lock.
|
|
||||||
pub(crate) async fn get_quota_config_and_incarnation_from_disk_in(
|
|
||||||
ctx: &crate::runtime::instance::InstanceContext,
|
|
||||||
bucket: &str,
|
|
||||||
) -> Result<(Option<BucketQuota>, Uuid, OffsetDateTime)> {
|
|
||||||
let bucket_meta_sys_lock = bucket_metadata_sys_of(ctx)?;
|
|
||||||
let bucket_meta_sys = bucket_meta_sys_lock.read().await.clone();
|
|
||||||
|
|
||||||
match bucket_meta_sys
|
|
||||||
.read_authoritative_metadata_from_disk_under_transaction_lock(bucket)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
BucketMetadataAuthority::Authoritative(metadata)
|
|
||||||
if metadata.bucket_incarnation_sidecar && !metadata.bucket_incarnation_id.is_nil() =>
|
|
||||||
{
|
|
||||||
Ok((
|
|
||||||
metadata.quota_config.clone(),
|
|
||||||
metadata.bucket_incarnation_id,
|
|
||||||
metadata.quota_config_updated_at,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
BucketMetadataAuthority::Authoritative(_) => {
|
|
||||||
Err(Error::other(format!("bucket incarnation metadata is not authoritative: {bucket}")))
|
|
||||||
}
|
|
||||||
BucketMetadataAuthority::MissingBucket => Err(Error::BucketNotFound(bucket.to_string())),
|
|
||||||
BucketMetadataAuthority::Fabricated => Err(Error::other(format!("bucket quota metadata is not authoritative: {bucket}"))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_replication_config(bucket: &str) -> Result<(ReplicationConfiguration, OffsetDateTime)> {
|
pub async fn get_replication_config(bucket: &str) -> Result<(ReplicationConfiguration, OffsetDateTime)> {
|
||||||
let bucket_meta_sys_lock = get_bucket_metadata_sys()?;
|
let bucket_meta_sys_lock = get_bucket_metadata_sys()?;
|
||||||
let bucket_meta_sys = bucket_meta_sys_lock.read().await;
|
let bucket_meta_sys = bucket_meta_sys_lock.read().await;
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ impl QuotaChecker {
|
|||||||
) -> Result<QuotaCheckResult, QuotaError> {
|
) -> Result<QuotaCheckResult, QuotaError> {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let quota_config = self.get_quota_config(bucket).await?;
|
let quota_config = self.get_quota_config(bucket).await?;
|
||||||
let uses_durable_reservations = quota_config.uses_durable_reservations();
|
|
||||||
|
|
||||||
// If no quota limit is set, allow operation
|
// If no quota limit is set, allow operation
|
||||||
let quota_limit = match quota_config.quota {
|
let quota_limit = match quota_config.quota {
|
||||||
@@ -68,7 +67,6 @@ impl QuotaChecker {
|
|||||||
quota_limit: None,
|
quota_limit: None,
|
||||||
operation_size,
|
operation_size,
|
||||||
remaining: None,
|
remaining: None,
|
||||||
uses_durable_reservations,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Some(q) => q,
|
Some(q) => q,
|
||||||
@@ -76,17 +74,14 @@ impl QuotaChecker {
|
|||||||
|
|
||||||
let current_usage = self.get_real_time_usage(bucket).await?;
|
let current_usage = self.get_real_time_usage(bucket).await?;
|
||||||
|
|
||||||
let admission_size = if uses_durable_reservations { 0 } else { operation_size };
|
|
||||||
let expected_usage = match operation {
|
let expected_usage = match operation {
|
||||||
QuotaOperation::PutObject | QuotaOperation::PostObject | QuotaOperation::CopyObject => {
|
QuotaOperation::PutObject | QuotaOperation::PostObject | QuotaOperation::CopyObject => current_usage + operation_size,
|
||||||
current_usage.saturating_add(admission_size)
|
|
||||||
}
|
|
||||||
QuotaOperation::DeleteObject => current_usage.saturating_sub(operation_size),
|
QuotaOperation::DeleteObject => current_usage.saturating_sub(operation_size),
|
||||||
};
|
};
|
||||||
|
|
||||||
let allowed = match operation {
|
let allowed = match operation {
|
||||||
QuotaOperation::PutObject | QuotaOperation::PostObject | QuotaOperation::CopyObject => {
|
QuotaOperation::PutObject | QuotaOperation::PostObject | QuotaOperation::CopyObject => {
|
||||||
quota_config.check_operation_allowed(current_usage, admission_size)
|
quota_config.check_operation_allowed(current_usage, operation_size)
|
||||||
}
|
}
|
||||||
QuotaOperation::DeleteObject => true,
|
QuotaOperation::DeleteObject => true,
|
||||||
};
|
};
|
||||||
@@ -110,7 +105,6 @@ impl QuotaChecker {
|
|||||||
quota_limit: Some(quota_limit),
|
quota_limit: Some(quota_limit),
|
||||||
operation_size,
|
operation_size,
|
||||||
remaining,
|
remaining,
|
||||||
uses_durable_reservations,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let duration = start_time.elapsed();
|
let duration = start_time.elapsed();
|
||||||
@@ -164,26 +158,6 @@ impl QuotaChecker {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_durable_quota_config_if_incarnation(
|
|
||||||
&mut self,
|
|
||||||
bucket: &str,
|
|
||||||
quota: BucketQuota,
|
|
||||||
expected_incarnation_id: uuid::Uuid,
|
|
||||||
proof: &crate::services::notification_sys::CrossPoolFenceFleetProofToken,
|
|
||||||
) -> Result<OffsetDateTime, QuotaError> {
|
|
||||||
let json_data = serde_json::to_vec("a).map_err(|e| QuotaError::InvalidConfig {
|
|
||||||
reason: format!("Failed to serialize quota config: {}", e),
|
|
||||||
})?;
|
|
||||||
let start_time = Instant::now();
|
|
||||||
let updated_at =
|
|
||||||
crate::bucket::metadata_sys::update_quota_if_incarnation(bucket, json_data, expected_incarnation_id, proof)
|
|
||||||
.await
|
|
||||||
.map_err(QuotaError::StorageError)?;
|
|
||||||
|
|
||||||
rustfs_common::metrics::Metrics::inc_time(Metric::QuotaSync, start_time.elapsed());
|
|
||||||
Ok(updated_at)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_quota_config_for_incarnation(
|
async fn set_quota_config_for_incarnation(
|
||||||
&mut self,
|
&mut self,
|
||||||
bucket: &str,
|
bucket: &str,
|
||||||
@@ -381,7 +355,6 @@ mod tests {
|
|||||||
quota_limit: None,
|
quota_limit: None,
|
||||||
operation_size: 1024,
|
operation_size: 1024,
|
||||||
remaining: None,
|
remaining: None,
|
||||||
uses_durable_reservations: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(result.allowed);
|
assert!(result.allowed);
|
||||||
@@ -405,13 +378,4 @@ mod tests {
|
|||||||
let allowed = quota.check_operation_allowed(512, 1024);
|
let allowed = quota.check_operation_allowed(512, 1024);
|
||||||
assert!(!allowed);
|
assert!(!allowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn legacy_quota_rejects_full_operation_while_v1_defers_net_growth() {
|
|
||||||
let legacy: BucketQuota = serde_json::from_str(r#"{"quota":5}"#).expect("legacy quota should parse");
|
|
||||||
let durable = BucketQuota::new(Some(5));
|
|
||||||
|
|
||||||
assert!(!legacy.check_operation_allowed(4, 2));
|
|
||||||
assert!(durable.uses_durable_reservations());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,98 +13,38 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
pub mod checker;
|
pub mod checker;
|
||||||
pub(crate) mod reservation;
|
|
||||||
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use rustfs_config::{
|
use rustfs_config::{
|
||||||
QUOTA_API_PATH, QUOTA_EXCEEDED_ERROR_CODE, QUOTA_INTERNAL_ERROR_CODE, QUOTA_INVALID_CONFIG_ERROR_CODE,
|
QUOTA_API_PATH, QUOTA_EXCEEDED_ERROR_CODE, QUOTA_INTERNAL_ERROR_CODE, QUOTA_INVALID_CONFIG_ERROR_CODE,
|
||||||
QUOTA_NOT_FOUND_ERROR_CODE,
|
QUOTA_NOT_FOUND_ERROR_CODE,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
pub enum QuotaType {
|
pub enum QuotaType {
|
||||||
/// Hard quota accounting.
|
/// Hard quota: reject immediately when exceeded
|
||||||
#[default]
|
#[default]
|
||||||
#[serde(alias = "HARD", alias = "hard")]
|
#[serde(alias = "HARD", alias = "hard")]
|
||||||
Hard,
|
Hard,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const QUOTA_RESERVATION_PROTOCOL_V1: u32 = 1;
|
|
||||||
|
|
||||||
/// Bucket quota configuration. quota_type defaults to Hard when omitted.
|
/// Bucket quota configuration. quota_type defaults to Hard when omitted.
|
||||||
#[derive(Debug, Default, Clone, PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
|
||||||
pub struct BucketQuota {
|
pub struct BucketQuota {
|
||||||
|
#[serde(default)]
|
||||||
pub quota: Option<u64>,
|
pub quota: Option<u64>,
|
||||||
/// Defaults to Hard when missing.
|
/// Defaults to Hard when missing.
|
||||||
|
#[serde(default)]
|
||||||
pub quota_type: QuotaType,
|
pub quota_type: QuotaType,
|
||||||
/// Optional durable reservation protocol. The wire format gives older
|
|
||||||
/// nodes a zero hard quota so a mixed-version fleet fails closed.
|
|
||||||
pub reservation_protocol: Option<u32>,
|
|
||||||
/// Timestamp when this quota configuration was set (for audit purposes)
|
/// Timestamp when this quota configuration was set (for audit purposes)
|
||||||
|
#[serde(default, with = "time::serde::rfc3339::option")]
|
||||||
pub created_at: Option<OffsetDateTime>,
|
pub created_at: Option<OffsetDateTime>,
|
||||||
/// Accept updated_at for compatibility; not used.
|
/// Accept updated_at for compatibility; not used.
|
||||||
pub updated_at: Option<OffsetDateTime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
struct BucketQuotaWire {
|
|
||||||
#[serde(default)]
|
|
||||||
quota: Option<u64>,
|
|
||||||
#[serde(default)]
|
|
||||||
quota_type: QuotaType,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
reservation_protocol: Option<u32>,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
reservation_quota: Option<u64>,
|
|
||||||
#[serde(default, with = "time::serde::rfc3339::option")]
|
|
||||||
created_at: Option<OffsetDateTime>,
|
|
||||||
#[serde(default, with = "time::serde::rfc3339::option", skip_serializing_if = "Option::is_none")]
|
#[serde(default, with = "time::serde::rfc3339::option", skip_serializing_if = "Option::is_none")]
|
||||||
updated_at: Option<OffsetDateTime>,
|
pub updated_at: Option<OffsetDateTime>,
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for BucketQuota {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
let durable = self.uses_durable_reservations();
|
|
||||||
BucketQuotaWire {
|
|
||||||
quota: if durable { Some(0) } else { self.quota },
|
|
||||||
quota_type: self.quota_type.clone(),
|
|
||||||
reservation_protocol: self.reservation_protocol,
|
|
||||||
reservation_quota: if durable { self.quota } else { None },
|
|
||||||
created_at: self.created_at,
|
|
||||||
updated_at: self.updated_at,
|
|
||||||
}
|
|
||||||
.serialize(serializer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for BucketQuota {
|
|
||||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let wire = BucketQuotaWire::deserialize(deserializer)?;
|
|
||||||
let quota = if wire.reservation_protocol == Some(QUOTA_RESERVATION_PROTOCOL_V1) {
|
|
||||||
Some(
|
|
||||||
wire.reservation_quota
|
|
||||||
.ok_or_else(|| D::Error::custom("reservation_quota is required for reservation protocol v1"))?,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
wire.quota
|
|
||||||
};
|
|
||||||
Ok(Self {
|
|
||||||
quota,
|
|
||||||
quota_type: wire.quota_type,
|
|
||||||
reservation_protocol: wire.reservation_protocol,
|
|
||||||
created_at: wire.created_at,
|
|
||||||
updated_at: wire.updated_at,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BucketQuota {
|
impl BucketQuota {
|
||||||
@@ -123,7 +63,6 @@ impl BucketQuota {
|
|||||||
Self {
|
Self {
|
||||||
quota,
|
quota,
|
||||||
quota_type: QuotaType::Hard,
|
quota_type: QuotaType::Hard,
|
||||||
reservation_protocol: quota.map(|_| QUOTA_RESERVATION_PROTOCOL_V1),
|
|
||||||
created_at: Some(now),
|
created_at: Some(now),
|
||||||
updated_at: None,
|
updated_at: None,
|
||||||
}
|
}
|
||||||
@@ -133,19 +72,7 @@ impl BucketQuota {
|
|||||||
self.quota
|
self.quota
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uses_durable_reservations(&self) -> bool {
|
|
||||||
self.reservation_protocol == Some(QUOTA_RESERVATION_PROTOCOL_V1)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_unsupported_reservation_protocol(&self) -> bool {
|
|
||||||
self.reservation_protocol
|
|
||||||
.is_some_and(|version| version != QUOTA_RESERVATION_PROTOCOL_V1)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn check_operation_allowed(&self, current_usage: u64, operation_size: u64) -> bool {
|
pub fn check_operation_allowed(&self, current_usage: u64, operation_size: u64) -> bool {
|
||||||
if operation_size == 0 {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if let Some(quota_limit) = self.quota {
|
if let Some(quota_limit) = self.quota {
|
||||||
current_usage.saturating_add(operation_size) <= quota_limit
|
current_usage.saturating_add(operation_size) <= quota_limit
|
||||||
} else {
|
} else {
|
||||||
@@ -167,7 +94,6 @@ pub struct QuotaCheckResult {
|
|||||||
pub quota_limit: Option<u64>,
|
pub quota_limit: Option<u64>,
|
||||||
pub operation_size: u64,
|
pub operation_size: u64,
|
||||||
pub remaining: Option<u64>,
|
pub remaining: Option<u64>,
|
||||||
pub uses_durable_reservations: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -284,59 +210,7 @@ mod tests {
|
|||||||
let buf = q.marshal_msg().expect("marshal");
|
let buf = q.marshal_msg().expect("marshal");
|
||||||
let restored = BucketQuota::unmarshal(&buf).expect("unmarshal");
|
let restored = BucketQuota::unmarshal(&buf).expect("unmarshal");
|
||||||
assert_eq!(q.quota, restored.quota);
|
assert_eq!(q.quota, restored.quota);
|
||||||
assert_eq!(restored.quota_type, QuotaType::Hard);
|
assert_eq!(q.quota_type, restored.quota_type);
|
||||||
assert_eq!(restored.reservation_protocol, Some(QUOTA_RESERVATION_PROTOCOL_V1));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn clearing_quota_keeps_the_legacy_compatible_type() {
|
|
||||||
let quota = BucketQuota::new(None);
|
|
||||||
|
|
||||||
assert_eq!(quota.quota_type, QuotaType::Hard);
|
|
||||||
assert_eq!(quota.reservation_protocol, None);
|
|
||||||
assert!(!quota.uses_durable_reservations());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn durable_quota_makes_legacy_nodes_fail_closed() {
|
|
||||||
let json = serde_json::to_vec(&BucketQuota::new(Some(2048))).expect("durable quota should serialize");
|
|
||||||
let quota: BucketQuota = serde_json::from_slice(&json).expect("current quota version should parse");
|
|
||||||
assert!(quota.uses_durable_reservations());
|
|
||||||
assert_eq!(quota.quota, Some(2048));
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
enum LegacyQuotaType {
|
|
||||||
Hard,
|
|
||||||
}
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct LegacyBucketQuota {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
quota: Option<u64>,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
quota_type: LegacyQuotaType,
|
|
||||||
}
|
|
||||||
let legacy = serde_json::from_slice::<LegacyBucketQuota>(&json)
|
|
||||||
.expect("legacy readers should ignore the reservation protocol field");
|
|
||||||
assert_eq!(legacy.quota, Some(0));
|
|
||||||
assert!(matches!(legacy.quota_type, LegacyQuotaType::Hard));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unknown_reservation_protocol_does_not_activate_v1() {
|
|
||||||
let quota: BucketQuota =
|
|
||||||
serde_json::from_str(r#"{"quota":0,"quota_type":"Hard","reservation_protocol":2,"reservation_quota":2048}"#)
|
|
||||||
.expect("future protocol should remain parseable");
|
|
||||||
|
|
||||||
assert!(!quota.uses_durable_reservations());
|
|
||||||
assert!(quota.has_unsupported_reservation_protocol());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reservation_protocol_v1_requires_reservation_quota() {
|
|
||||||
let err = serde_json::from_str::<BucketQuota>(r#"{"quota":0,"quota_type":"Hard","reservation_protocol":1}"#)
|
|
||||||
.expect_err("v1 without its authoritative quota must fail closed");
|
|
||||||
|
|
||||||
assert!(err.to_string().contains("reservation_quota is required"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// unmarshal accepts format without quota_type
|
/// unmarshal accepts format without quota_type
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -81,6 +81,6 @@ pub use replication_queue_boundary::{
|
|||||||
pub use replication_resync_boundary::{BucketReplicationResyncStatus, ResyncOpts, TargetReplicationResyncStatus};
|
pub use replication_resync_boundary::{BucketReplicationResyncStatus, ResyncOpts, TargetReplicationResyncStatus};
|
||||||
pub use replication_scanner_bridge::ReplicationScannerBridge;
|
pub use replication_scanner_bridge::ReplicationScannerBridge;
|
||||||
pub use replication_state::{ReplicationStats, RuntimeReplicationTargetBacklog};
|
pub use replication_state::{ReplicationStats, RuntimeReplicationTargetBacklog};
|
||||||
pub use replication_stats_boundary::{BucketReplicationStat, BucketReplicationStats, BucketStats, InQueueMetric, XferStats};
|
pub use replication_stats_boundary::{BucketReplicationStats, BucketStats};
|
||||||
pub use replication_storage_boundary::{ReplicationObjectIO, ReplicationStorage};
|
pub use replication_storage_boundary::{ReplicationObjectIO, ReplicationStorage};
|
||||||
pub(crate) use replication_target_config_bridge::ReplicationTargetConfigBridge;
|
pub(crate) use replication_target_config_bridge::ReplicationTargetConfigBridge;
|
||||||
|
|||||||
@@ -704,12 +704,6 @@ impl ReplicationStats {
|
|||||||
} else {
|
} else {
|
||||||
BucketReplicationStats::new()
|
BucketReplicationStats::new()
|
||||||
};
|
};
|
||||||
// Stamp the serializable failure windows from the live samples: the
|
|
||||||
// samples themselves do not cross the peer-RPC wire, so this snapshot
|
|
||||||
// is what cluster aggregation and the metrics endpoints see.
|
|
||||||
for stat in replication_stats.stats.values_mut() {
|
|
||||||
stat.fail_stats.refresh_windows();
|
|
||||||
}
|
|
||||||
let uptime = if cache.contains_key(bucket) {
|
let uptime = if cache.contains_key(bucket) {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
|||||||
@@ -15,9 +15,7 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) use rustfs_replication::FailStats;
|
pub(crate) use rustfs_replication::FailStats;
|
||||||
pub(crate) use rustfs_replication::{
|
pub(crate) use rustfs_replication::{
|
||||||
ActiveWorkerStat, ProxyMetric, ProxyStatsCache, QueueCache, ReplicationMetricScope, SRMetricsSummary,
|
ActiveWorkerStat, BucketReplicationStat, InQueueMetric, ProxyMetric, ProxyStatsCache, QueueCache, ReplicationMetricScope,
|
||||||
|
SRMetricsSummary, XferStats,
|
||||||
};
|
};
|
||||||
// Public so the admin wire DTOs (rustfs/src/admin/replication_metrics_wire.rs)
|
pub use rustfs_replication::{BucketReplicationStats, BucketStats};
|
||||||
// can project the internal stats onto the minio-go response shapes through
|
|
||||||
// the storage_api facade chain.
|
|
||||||
pub use rustfs_replication::{BucketReplicationStat, BucketReplicationStats, BucketStats, InQueueMetric, XferStats};
|
|
||||||
|
|||||||
@@ -27,10 +27,8 @@ use rustfs_utils::http::{
|
|||||||
AMZ_OBJECT_TAGGING, AMZ_SERVER_SIDE_ENCRYPTION, AMZ_SERVER_SIDE_ENCRYPTION_KMS_CONTEXT, AMZ_SERVER_SIDE_ENCRYPTION_KMS_ID,
|
AMZ_OBJECT_TAGGING, AMZ_SERVER_SIDE_ENCRYPTION, AMZ_SERVER_SIDE_ENCRYPTION_KMS_CONTEXT, AMZ_SERVER_SIDE_ENCRYPTION_KMS_ID,
|
||||||
AMZ_STORAGE_CLASS, AMZ_TAG_COUNT, CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_TYPE,
|
AMZ_STORAGE_CLASS, AMZ_TAG_COUNT, CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_TYPE,
|
||||||
HeaderExt as _, SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP,
|
HeaderExt as _, SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP,
|
||||||
SUFFIX_REPLICATION_ACTUAL_OBJECT_SIZE, SUFFIX_REPLICATION_SSEC_CRC, SUFFIX_SOURCE_REPLICATION_LEGALHOLD_TIMESTAMP,
|
SUFFIX_REPLICATION_ACTUAL_OBJECT_SIZE, SUFFIX_REPLICATION_SSEC_CRC, SUFFIX_TAGGING_TIMESTAMP, get_str, insert_header_map,
|
||||||
SUFFIX_SOURCE_REPLICATION_RETENTION_TIMESTAMP, SUFFIX_SOURCE_REPLICATION_TAGGING_TIMESTAMP, SUFFIX_TAGGING_TIMESTAMP,
|
is_internal_key, is_object_encryption_marker, is_replication_stripped_encryption_key, ssec_replication_transport_header,
|
||||||
get_str, insert_header_map, is_internal_key, is_object_encryption_marker, is_replication_stripped_encryption_key,
|
|
||||||
ssec_replication_transport_header,
|
|
||||||
};
|
};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use time::format_description::well_known::Rfc3339;
|
use time::format_description::well_known::Rfc3339;
|
||||||
@@ -121,27 +119,6 @@ fn classify_replication_source_encryption(metadata: &HashMap<String, String>) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_legacy_source_replication_timestamp_key(key: &str) -> bool {
|
|
||||||
fn has_prefix_and_suffix(key: &str, prefix: &str, suffix: &str) -> bool {
|
|
||||||
let key = key.as_bytes();
|
|
||||||
key.len() == prefix.len() + suffix.len()
|
|
||||||
&& key[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes())
|
|
||||||
&& key[prefix.len()..].eq_ignore_ascii_case(suffix.as_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
[
|
|
||||||
SUFFIX_SOURCE_REPLICATION_TAGGING_TIMESTAMP,
|
|
||||||
SUFFIX_SOURCE_REPLICATION_RETENTION_TIMESTAMP,
|
|
||||||
SUFFIX_SOURCE_REPLICATION_LEGALHOLD_TIMESTAMP,
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.any(|suffix| {
|
|
||||||
["x-rustfs-", "x-minio-"]
|
|
||||||
.iter()
|
|
||||||
.any(|prefix| has_prefix_and_suffix(key, prefix, suffix))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn replication_object_is_ssec_encrypted(user_defined: &HashMap<String, String>) -> bool {
|
pub(crate) fn replication_object_is_ssec_encrypted(user_defined: &HashMap<String, String>) -> bool {
|
||||||
rustfs_replication::is_ssec_encrypted(user_defined)
|
rustfs_replication::is_ssec_encrypted(user_defined)
|
||||||
}
|
}
|
||||||
@@ -199,11 +176,6 @@ pub(crate) fn replication_put_object_options(sc: &str, object_info: &ObjectInfo)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_legacy_source_replication_timestamp_key(key) {
|
|
||||||
meta.insert(format!("x-amz-meta-{key}"), value.to_string());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_internal_key(key) || is_standard_header(key) {
|
if is_internal_key(key) || is_standard_header(key) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -287,23 +259,15 @@ pub(crate) fn replication_put_object_options(sc: &str, object_info: &ObjectInfo)
|
|||||||
|
|
||||||
if !tags.is_empty() {
|
if !tags.is_empty() {
|
||||||
put_options.user_tags = tags;
|
put_options.user_tags = tags;
|
||||||
|
put_options.internal.tagging_timestamp =
|
||||||
|
if let Some(timestamp) = get_str(&object_info.user_defined, SUFFIX_TAGGING_TIMESTAMP) {
|
||||||
|
OffsetDateTime::parse(×tamp, &Rfc3339)
|
||||||
|
.map_err(|err| Error::other(format!("Failed to parse tagging timestamp: {err}")))?
|
||||||
|
} else {
|
||||||
|
object_info.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Load the stored tagging timestamp independently of whether any tags
|
|
||||||
// remain: DeleteObjectTagging leaves the object tagless but stamps this
|
|
||||||
// key, and the deletion's LWW timestamp must still reach the replica.
|
|
||||||
// With no stored key, fall back to mod_time only while tags exist
|
|
||||||
// (MinIO parity); a tagless object without the key was never tagged and
|
|
||||||
// keeps the epoch default (no header).
|
|
||||||
put_options.internal.tagging_timestamp = if let Some(timestamp) = get_str(&object_info.user_defined, SUFFIX_TAGGING_TIMESTAMP)
|
|
||||||
{
|
|
||||||
OffsetDateTime::parse(×tamp, &Rfc3339)
|
|
||||||
.map_err(|err| Error::other(format!("Failed to parse tagging timestamp: {err}")))?
|
|
||||||
} else if !put_options.user_tags.is_empty() {
|
|
||||||
object_info.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH)
|
|
||||||
} else {
|
|
||||||
OffsetDateTime::UNIX_EPOCH
|
|
||||||
};
|
|
||||||
|
|
||||||
let metadata = &*object_info.user_defined;
|
let metadata = &*object_info.user_defined;
|
||||||
|
|
||||||
@@ -319,15 +283,13 @@ pub(crate) fn replication_put_object_options(sc: &str, object_info: &ObjectInfo)
|
|||||||
put_options.cache_control = cache_control.to_string();
|
put_options.cache_control = cache_control.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mode) = metadata.lookup(AMZ_OBJECT_LOCK_MODE).filter(|mode| !mode.is_empty()) {
|
if let Some(mode) = metadata.lookup(AMZ_OBJECT_LOCK_MODE) {
|
||||||
put_options.mode = Some(ObjectLockRetentionMode::from(mode.to_uppercase().as_str()));
|
put_options.mode = Some(ObjectLockRetentionMode::from(mode.to_uppercase().as_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(retain_until_date) = metadata.lookup(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE) {
|
if let Some(retain_until_date) = metadata.lookup(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE) {
|
||||||
if !retain_until_date.is_empty() {
|
put_options.retain_until_date = OffsetDateTime::parse(retain_until_date, &Rfc3339)
|
||||||
put_options.retain_until_date = OffsetDateTime::parse(retain_until_date, &Rfc3339)
|
.map_err(|err| Error::other(format!("Failed to parse retain until date: {err}")))?;
|
||||||
.map_err(|err| Error::other(format!("Failed to parse retain until date: {err}")))?;
|
|
||||||
}
|
|
||||||
put_options.internal.retention_timestamp =
|
put_options.internal.retention_timestamp =
|
||||||
if let Some(timestamp) = get_str(&object_info.user_defined, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP) {
|
if let Some(timestamp) = get_str(&object_info.user_defined, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP) {
|
||||||
OffsetDateTime::parse(×tamp, &Rfc3339).unwrap_or(OffsetDateTime::UNIX_EPOCH)
|
OffsetDateTime::parse(×tamp, &Rfc3339).unwrap_or(OffsetDateTime::UNIX_EPOCH)
|
||||||
@@ -732,110 +694,6 @@ mod tests {
|
|||||||
assert!(options.internal.replication_request);
|
assert!(options.internal.replication_request);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// DeleteObjectTagging leaves the object tagless but stamps the
|
|
||||||
/// tagging-timestamp internal key; the deletion's LWW timestamp must
|
|
||||||
/// still be loaded (and therefore sent) so the replica can order the
|
|
||||||
/// deletion against concurrent tag edits.
|
|
||||||
#[test]
|
|
||||||
fn replication_put_options_carry_tagging_timestamp_after_tag_deletion() {
|
|
||||||
let mut metadata = std::collections::HashMap::new();
|
|
||||||
rustfs_utils::http::insert_str(&mut metadata, SUFFIX_TAGGING_TIMESTAMP, "2026-01-02T03:04:05Z".to_string());
|
|
||||||
|
|
||||||
let object_info = ObjectInfo {
|
|
||||||
user_defined: Arc::new(metadata),
|
|
||||||
user_tags: Arc::new(String::new()),
|
|
||||||
mod_time: Some(OffsetDateTime::UNIX_EPOCH),
|
|
||||||
version_id: Some(Uuid::nil()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let (options, _) = replication_put_object_options("", &object_info).expect("build put options");
|
|
||||||
|
|
||||||
assert!(options.user_tags.is_empty());
|
|
||||||
assert_eq!(
|
|
||||||
options.internal.tagging_timestamp,
|
|
||||||
OffsetDateTime::parse("2026-01-02T03:04:05Z", &Rfc3339).expect("valid timestamp"),
|
|
||||||
"the stored tagging timestamp must load independently of remaining tags"
|
|
||||||
);
|
|
||||||
|
|
||||||
// A tagless object without the stored key was never tagged: the epoch
|
|
||||||
// default keeps the header unsent.
|
|
||||||
let untagged = ObjectInfo {
|
|
||||||
user_tags: Arc::new(String::new()),
|
|
||||||
mod_time: Some(OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("timestamp")),
|
|
||||||
version_id: Some(Uuid::nil()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let (options, _) = replication_put_object_options("", &untagged).expect("build put options");
|
|
||||||
assert_eq!(options.internal.tagging_timestamp, OffsetDateTime::UNIX_EPOCH);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn replication_put_options_do_not_promote_legacy_user_timestamp_metadata() {
|
|
||||||
let legacy_keys = [
|
|
||||||
"x-rustfs-source-replication-tagging-timestamp",
|
|
||||||
"x-rustfs-source-replication-retention-timestamp",
|
|
||||||
"x-rustfs-source-replication-legalhold-timestamp",
|
|
||||||
"x-minio-source-replication-tagging-timestamp",
|
|
||||||
"x-minio-source-replication-retention-timestamp",
|
|
||||||
"x-minio-source-replication-legalhold-timestamp",
|
|
||||||
];
|
|
||||||
let object_info = ObjectInfo {
|
|
||||||
user_defined: Arc::new(
|
|
||||||
legacy_keys
|
|
||||||
.iter()
|
|
||||||
.map(|key| (key.to_string(), "2099-01-02T03:04:05Z".to_string()))
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let (options, _) = replication_put_object_options("", &object_info).expect("build put options");
|
|
||||||
|
|
||||||
for legacy_key in legacy_keys {
|
|
||||||
assert!(!options.user_metadata.contains_key(legacy_key));
|
|
||||||
assert_eq!(
|
|
||||||
options
|
|
||||||
.user_metadata
|
|
||||||
.get(&format!("x-amz-meta-{legacy_key}"))
|
|
||||||
.map(String::as_str),
|
|
||||||
Some("2099-01-02T03:04:05Z")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
assert_eq!(options.internal.tagging_timestamp, OffsetDateTime::UNIX_EPOCH);
|
|
||||||
assert_eq!(options.internal.retention_timestamp, OffsetDateTime::UNIX_EPOCH);
|
|
||||||
assert_eq!(options.internal.legalhold_timestamp, OffsetDateTime::UNIX_EPOCH);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn replication_put_options_carry_retention_timestamp_after_clear() {
|
|
||||||
let mut metadata = HashMap::from([
|
|
||||||
(AMZ_OBJECT_LOCK_MODE.to_string(), String::new()),
|
|
||||||
(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(), String::new()),
|
|
||||||
]);
|
|
||||||
rustfs_utils::http::insert_str(&mut metadata, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP, "2026-01-02T03:04:05Z".to_string());
|
|
||||||
let object_info = ObjectInfo {
|
|
||||||
user_defined: Arc::new(metadata),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let (options, _) = replication_put_object_options("", &object_info).expect("retention clear must replicate");
|
|
||||||
|
|
||||||
assert!(options.mode.is_none());
|
|
||||||
assert_eq!(options.retain_until_date, OffsetDateTime::UNIX_EPOCH);
|
|
||||||
assert_eq!(
|
|
||||||
options.internal.retention_timestamp,
|
|
||||||
OffsetDateTime::parse("2026-01-02T03:04:05Z", &Rfc3339).expect("valid timestamp")
|
|
||||||
);
|
|
||||||
let headers = options.header();
|
|
||||||
assert!(!headers.contains_key(AMZ_OBJECT_LOCK_MODE));
|
|
||||||
assert!(!headers.contains_key(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE));
|
|
||||||
assert_eq!(
|
|
||||||
rustfs_utils::http::get_header(&headers, SUFFIX_SOURCE_REPLICATION_RETENTION_TIMESTAMP).as_deref(),
|
|
||||||
Some("2026-01-02T03:04:05Z")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn replication_put_options_strip_encryption_metadata_from_plaintext_objects() {
|
fn replication_put_options_strip_encryption_metadata_from_plaintext_objects() {
|
||||||
use rustfs_utils::http::object_encryption_keys::{INTERNAL_ENCRYPTION_ORIGINAL_SIZE_HEADER, SSEC_ORIGINAL_SIZE_HEADER};
|
use rustfs_utils::http::object_encryption_keys::{INTERNAL_ENCRYPTION_ORIGINAL_SIZE_HEADER, SSEC_ORIGINAL_SIZE_HEADER};
|
||||||
|
|||||||
@@ -40,14 +40,7 @@ impl ARN {
|
|||||||
|
|
||||||
impl Display for ARN {
|
impl Display for ARN {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
// The `minio` partition is deliberate: madmin-go's ParseARN
|
write!(f, "arn:rustfs:{}:{}:{}:{}", self.arn_type, self.region, self.id, self.bucket)
|
||||||
// hard-rejects any other partition, so native mc/madmin tooling can
|
|
||||||
// only decode remote-target ARNs minted in this form (backlog#1675
|
|
||||||
// P1-7). Legacy `arn:rustfs:` ARNs persisted by older releases stay
|
|
||||||
// readable via the FromStr whitelist below; runtime matching between
|
|
||||||
// targets and replication rules is by full-string equality, so mixed
|
|
||||||
// partitions coexist safely.
|
|
||||||
write!(f, "arn:minio:{}:{}:{}:{}", self.arn_type, self.region, self.id, self.bucket)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,12 +48,7 @@ impl FromStr for ARN {
|
|||||||
type Err = std::io::Error;
|
type Err = std::io::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
// Partition whitelist, not just an `arn:` check: `BucketTargetType::
|
if !s.starts_with("arn:rustfs:") {
|
||||||
// from_str(...).unwrap_or_default()` below never fails, so this is
|
|
||||||
// the only structural gate rejecting foreign ARNs. `arn:rustfs:` is
|
|
||||||
// the legacy partition and must stay accepted forever (persisted
|
|
||||||
// bucket-targets.json / replication configs from older releases).
|
|
||||||
if !s.starts_with("arn:minio:") && !s.starts_with("arn:rustfs:") {
|
|
||||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid ARN format"));
|
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid ARN format"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,50 +101,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// RustFS commonly generates ARNs with an empty region:
|
/// RustFS commonly generates ARNs with an empty region:
|
||||||
/// `arn:minio:replication::<deployment_id>:<bucket>`.
|
/// `arn:rustfs:replication::<deployment_id>:<bucket>`.
|
||||||
#[test]
|
#[test]
|
||||||
fn from_str_handles_empty_region_segment() {
|
fn from_str_handles_empty_region_segment() {
|
||||||
let parsed = ARN::from_str("arn:minio:replication::depl-123:bucket-a").expect("valid ARN must parse");
|
let parsed = ARN::from_str("arn:rustfs:replication::depl-123:bucket-a").expect("valid ARN must parse");
|
||||||
|
|
||||||
assert_eq!(parsed.arn_type, BucketTargetType::ReplicationService);
|
assert_eq!(parsed.arn_type, BucketTargetType::ReplicationService);
|
||||||
assert_eq!(parsed.region, "", "region segment is empty in this form");
|
assert_eq!(parsed.region, "", "region segment is empty in this form");
|
||||||
assert_eq!(parsed.id, "depl-123");
|
assert_eq!(parsed.id, "depl-123");
|
||||||
assert_eq!(parsed.bucket, "bucket-a");
|
assert_eq!(parsed.bucket, "bucket-a");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// madmin-go's `ParseARN` hard-rejects anything that does not start with
|
|
||||||
/// `arn:minio:`, so generated ARNs must use the `minio` partition or the
|
|
||||||
/// native mc/madmin tooling cannot decode remote-target listings.
|
|
||||||
#[test]
|
|
||||||
fn display_emits_minio_partition() {
|
|
||||||
let arn = ARN::new(
|
|
||||||
BucketTargetType::ReplicationService,
|
|
||||||
"depl-123".to_string(),
|
|
||||||
String::new(),
|
|
||||||
"bucket-a".to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(arn.to_string(), "arn:minio:replication::depl-123:bucket-a");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Persisted bucket-targets.json files from older RustFS releases carry
|
|
||||||
/// `arn:rustfs:` ARNs; the legacy partition must stay parseable forever.
|
|
||||||
#[test]
|
|
||||||
fn from_str_accepts_legacy_rustfs_partition() {
|
|
||||||
let parsed = ARN::from_str("arn:rustfs:replication:us-east-1:depl-123:bucket-a").expect("legacy ARN must parse");
|
|
||||||
|
|
||||||
assert_eq!(parsed.arn_type, BucketTargetType::ReplicationService);
|
|
||||||
assert_eq!(parsed.region, "us-east-1");
|
|
||||||
assert_eq!(parsed.id, "depl-123");
|
|
||||||
assert_eq!(parsed.bucket, "bucket-a");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The partition whitelist is the only structural gate: `BucketTargetType::
|
|
||||||
/// from_str(...).unwrap_or_default()` never fails, so any 6-segment string
|
|
||||||
/// would otherwise parse as `type=None`.
|
|
||||||
#[test]
|
|
||||||
fn from_str_rejects_unknown_partition() {
|
|
||||||
assert!(ARN::from_str("arn:aws:replication::depl-123:bucket-a").is_err());
|
|
||||||
assert!(ARN::from_str("not-an-arn").is_err());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
use crate::disk::disk_store::{get_drive_walkdir_peek_timeout, get_drive_walkdir_stall_timeout};
|
use crate::disk::disk_store::{get_drive_walkdir_peek_timeout, get_drive_walkdir_stall_timeout};
|
||||||
use crate::disk::error::DiskError;
|
use crate::disk::error::DiskError;
|
||||||
use crate::disk::{self, DiskAPI, DiskStore, WalkDirOptions};
|
use crate::disk::{self, DiskAPI, DiskStore, WalkDirOptions};
|
||||||
use futures::future::join_all;
|
|
||||||
use metrics::counter;
|
use metrics::counter;
|
||||||
use rustfs_filemeta::{MetaCacheEntries, MetaCacheEntry, MetacacheReader, is_io_eof};
|
use rustfs_filemeta::{MetaCacheEntries, MetaCacheEntry, MetacacheReader, is_io_eof};
|
||||||
use std::{
|
use std::{
|
||||||
@@ -656,7 +655,6 @@ async fn list_path_raw_inner(
|
|||||||
errs.push(None);
|
errs.push(None);
|
||||||
}
|
}
|
||||||
let mut pending_entries: Vec<Option<MetaCacheEntry>> = vec![None; readers.len()];
|
let mut pending_entries: Vec<Option<MetaCacheEntry>> = vec![None; readers.len()];
|
||||||
let mut peek_outcomes: Vec<Option<PeekOutcome>> = std::iter::repeat_with(|| None).take(readers.len()).collect();
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let mut current = MetaCacheEntry::default();
|
let mut current = MetaCacheEntry::default();
|
||||||
@@ -678,21 +676,6 @@ async fn list_path_raw_inner(
|
|||||||
let mut has_err = 0;
|
let mut has_err = 0;
|
||||||
let mut agree = 0;
|
let mut agree = 0;
|
||||||
|
|
||||||
// Start every missing head read in the same round so one stalled
|
|
||||||
// disk cannot multiply the wait budget by the erasure-set width.
|
|
||||||
// Outcomes are still consumed below in stable disk-index order.
|
|
||||||
let concurrent_peeks = readers.iter_mut().enumerate().filter_map(|(i, reader)| {
|
|
||||||
if errs[i].is_some() || pending_entries[i].is_some() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cancel = &revjob_rx;
|
|
||||||
Some(async move { (i, peek_with_timeout(cancel, reader, peek_timeout).await) })
|
|
||||||
});
|
|
||||||
for (i, outcome) in join_all(concurrent_peeks).await {
|
|
||||||
peek_outcomes[i] = Some(outcome);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i, r) in readers.iter_mut().enumerate() {
|
for (i, r) in readers.iter_mut().enumerate() {
|
||||||
if errs[i].is_some() {
|
if errs[i].is_some() {
|
||||||
has_err += 1;
|
has_err += 1;
|
||||||
@@ -702,10 +685,7 @@ async fn list_path_raw_inner(
|
|||||||
let entry = if let Some(entry) = pending_entries[i].take() {
|
let entry = if let Some(entry) = pending_entries[i].take() {
|
||||||
entry
|
entry
|
||||||
} else {
|
} else {
|
||||||
let Some(outcome) = peek_outcomes[i].take() else {
|
match peek_with_timeout(&revjob_rx, r, peek_timeout).await {
|
||||||
return Err(DiskError::Unexpected);
|
|
||||||
};
|
|
||||||
match outcome {
|
|
||||||
PeekOutcome::Ready(res) => {
|
PeekOutcome::Ready(res) => {
|
||||||
if let Some(entry) = res {
|
if let Some(entry) = res {
|
||||||
// info!("read entry disk: {}, name: {}", i, entry.name);
|
// info!("read entry disk: {}, name: {}", i, entry.name);
|
||||||
@@ -1315,36 +1295,6 @@ mod tests {
|
|||||||
assert_eq!(err, DiskError::Timeout);
|
assert_eq!(err, DiskError::Timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(start_paused = true)]
|
|
||||||
async fn list_path_raw_bounds_multiple_stalled_readers_by_one_peek_deadline() {
|
|
||||||
let peek_timeout = Duration::from_millis(20);
|
|
||||||
let started = tokio::time::Instant::now();
|
|
||||||
let err = list_path_raw(
|
|
||||||
CancellationToken::new(),
|
|
||||||
ListPathRawOptions {
|
|
||||||
disks: vec![None, None, None, None],
|
|
||||||
min_disks: 1,
|
|
||||||
test_reader_behaviors: vec![
|
|
||||||
TestReaderBehavior::Stall,
|
|
||||||
TestReaderBehavior::Stall,
|
|
||||||
TestReaderBehavior::Stall,
|
|
||||||
TestReaderBehavior::Stall,
|
|
||||||
],
|
|
||||||
peek_timeout: Some(peek_timeout),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect_err("all stalled readers should fail the listing");
|
|
||||||
|
|
||||||
assert_eq!(err, DiskError::Timeout);
|
|
||||||
assert_eq!(
|
|
||||||
started.elapsed(),
|
|
||||||
peek_timeout,
|
|
||||||
"reader deadlines must overlap instead of accumulating once per disk"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn list_path_raw_waits_past_producer_stall_for_slow_progressing_reader() {
|
async fn list_path_raw_waits_past_producer_stall_for_slow_progressing_reader() {
|
||||||
let entry = MetaCacheEntry {
|
let entry = MetaCacheEntry {
|
||||||
|
|||||||
@@ -229,6 +229,17 @@ pub fn http_resp_to_error_response(
|
|||||||
err_resp
|
err_resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn err_transfer_acceleration_bucket(bucket_name: &str) -> ErrorResponse {
|
||||||
|
ErrorResponse {
|
||||||
|
status_code: StatusCode::BAD_REQUEST,
|
||||||
|
code: S3ErrorCode::InvalidArgument,
|
||||||
|
message: "The name of the bucket used for Transfer Acceleration must be DNS-compliant and must not contain periods ‘.’."
|
||||||
|
.to_string(),
|
||||||
|
bucket_name: bucket_name.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn err_entity_too_large(total_size: i64, max_object_size: i64, bucket_name: &str, object_name: &str) -> ErrorResponse {
|
pub fn err_entity_too_large(total_size: i64, max_object_size: i64, bucket_name: &str, object_name: &str) -> ErrorResponse {
|
||||||
let msg = format!(
|
let msg = format!(
|
||||||
"Your proposed upload size ‘{}’ exceeds the maximum allowed object size ‘{}’ for single PUT operation.",
|
"Your proposed upload size ‘{}’ exceeds the maximum allowed object size ‘{}’ for single PUT operation.",
|
||||||
@@ -284,6 +295,16 @@ pub fn err_invalid_argument(message: &str) -> ErrorResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn err_api_not_supported(message: &str) -> ErrorResponse {
|
||||||
|
ErrorResponse {
|
||||||
|
status_code: StatusCode::NOT_IMPLEMENTED,
|
||||||
|
code: S3ErrorCode::Custom("APINotSupported".into()),
|
||||||
|
message: message.to_string(),
|
||||||
|
request_id: "rustfs".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -135,10 +135,6 @@ impl Object {
|
|||||||
Self { ..Default::default() }
|
Self { ..Default::default() }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity reader surface with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn do_get_request(&self, request: &GetRequest) -> Result<GetResponse, std::io::Error> {
|
fn do_get_request(&self, request: &GetRequest) -> Result<GetResponse, std::io::Error> {
|
||||||
let _ = request.did_offset_change;
|
let _ = request.did_offset_change;
|
||||||
let _ = request.offset;
|
let _ = request.offset;
|
||||||
@@ -154,20 +150,12 @@ impl Object {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity Object reader method with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn set_offset(&mut self, bytes_read: i64) -> Result<(), std::io::Error> {
|
fn set_offset(&mut self, bytes_read: i64) -> Result<(), std::io::Error> {
|
||||||
self.curr_offset += bytes_read;
|
self.curr_offset += bytes_read;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity Object reader method with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn read(&mut self, b: &[u8]) -> Result<i64, std::io::Error> {
|
fn read(&mut self, b: &[u8]) -> Result<i64, std::io::Error> {
|
||||||
let mut read_req = GetRequest {
|
let mut read_req = GetRequest {
|
||||||
is_read_op: true,
|
is_read_op: true,
|
||||||
@@ -192,10 +180,6 @@ impl Object {
|
|||||||
Ok(response.size)
|
Ok(response.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity Object reader method with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn stat(&self) -> Result<ObjectInfo, std::io::Error> {
|
fn stat(&self) -> Result<ObjectInfo, std::io::Error> {
|
||||||
if !self.is_started || !self.object_info_set {
|
if !self.is_started || !self.object_info_set {
|
||||||
let _ = self.do_get_request(&GetRequest {
|
let _ = self.do_get_request(&GetRequest {
|
||||||
@@ -208,10 +192,6 @@ impl Object {
|
|||||||
Ok(self.object_info.clone())
|
Ok(self.object_info.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity Object reader method with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn read_at(&mut self, b: &[u8], offset: i64) -> Result<i64, std::io::Error> {
|
fn read_at(&mut self, b: &[u8], offset: i64) -> Result<i64, std::io::Error> {
|
||||||
self.curr_offset = offset;
|
self.curr_offset = offset;
|
||||||
|
|
||||||
@@ -239,10 +219,6 @@ impl Object {
|
|||||||
Ok(response.size)
|
Ok(response.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity Object reader method with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn seek(&mut self, offset: i64, whence: i64) -> Result<i64, std::io::Error> {
|
fn seek(&mut self, offset: i64, whence: i64) -> Result<i64, std::io::Error> {
|
||||||
if !self.is_started || !self.object_info_set {
|
if !self.is_started || !self.object_info_set {
|
||||||
let seek_req = GetRequest {
|
let seek_req = GetRequest {
|
||||||
@@ -277,10 +253,6 @@ impl Object {
|
|||||||
Ok(self.curr_offset)
|
Ok(self.curr_offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity Object reader method with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn close(&mut self) -> Result<(), std::io::Error> {
|
fn close(&mut self) -> Result<(), std::io::Error> {
|
||||||
self.is_closed = true;
|
self.is_closed = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ use crate::client::{
|
|||||||
api_put_object_common::optimal_part_info,
|
api_put_object_common::optimal_part_info,
|
||||||
api_put_object_multipart::UploadPartParams,
|
api_put_object_multipart::UploadPartParams,
|
||||||
api_s3_datatypes::{CompleteMultipartUpload, CompletePart, ObjectPart},
|
api_s3_datatypes::{CompleteMultipartUpload, CompletePart, ObjectPart},
|
||||||
constants::{ISO8601_DATEFORMAT, MAX_MULTIPART_PUT_OBJECT_SIZE, MIN_PART_SIZE},
|
constants::{ISO8601_DATEFORMAT, MAX_MULTIPART_PUT_OBJECT_SIZE, MIN_PART_SIZE, TOTAL_WORKERS},
|
||||||
credentials::SignatureType,
|
credentials::SignatureType,
|
||||||
transition_api::{ReaderImpl, TransitionClient, UploadInfo},
|
transition_api::{ReaderImpl, TransitionClient, UploadInfo},
|
||||||
utils::{is_amz_header, is_minio_header, is_rustfs_header, is_standard_header, is_storageclass_header},
|
utils::{is_amz_header, is_minio_header, is_rustfs_header, is_standard_header, is_storageclass_header},
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ pub fn is_object(reader: &ReaderImpl) -> bool {
|
|||||||
matches!(reader, ReaderImpl::ObjectBody(_))
|
matches!(reader, ReaderImpl::ObjectBody(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_read_at(reader: ReaderImpl) -> bool {
|
||||||
|
matches!(reader, ReaderImpl::ObjectBody(_))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn optimal_part_info(object_size: i64, configured_part_size: u64) -> Result<(i64, i64, i64), std::io::Error> {
|
pub fn optimal_part_info(object_size: i64, configured_part_size: u64) -> Result<(i64, i64, i64), std::io::Error> {
|
||||||
let unknown_size;
|
let unknown_size;
|
||||||
let mut object_size = object_size;
|
let mut object_size = object_size;
|
||||||
|
|||||||
@@ -81,6 +81,18 @@ async fn read_multipart_part(reader: &mut ReaderImpl, want: usize) -> Result<Vec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct UploadedPartRes {
|
||||||
|
pub error: std::io::Error,
|
||||||
|
pub part_num: i64,
|
||||||
|
pub size: i64,
|
||||||
|
pub part: ObjectPart,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UploadPartReq {
|
||||||
|
pub part_num: i64,
|
||||||
|
pub part: ObjectPart,
|
||||||
|
}
|
||||||
|
|
||||||
impl TransitionClient {
|
impl TransitionClient {
|
||||||
pub async fn put_object_multipart_stream(
|
pub async fn put_object_multipart_stream(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ use crate::client::utils::base64_decode;
|
|||||||
|
|
||||||
use super::transition_api;
|
use super::transition_api;
|
||||||
|
|
||||||
|
pub struct ListAllMyBucketsResult {
|
||||||
|
pub owner: Owner,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
pub struct CommonPrefix {
|
pub struct CommonPrefix {
|
||||||
pub prefix: String,
|
pub prefix: String,
|
||||||
@@ -85,10 +89,6 @@ pub struct ListVersionsResult {
|
|||||||
pub next_version_id_marker: String,
|
pub next_version_id_marker: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "fields of a MinIO-parity list result that this port builds but never reads back (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub struct ListBucketResult {
|
pub struct ListBucketResult {
|
||||||
common_prefixes: Vec<CommonPrefix>,
|
common_prefixes: Vec<CommonPrefix>,
|
||||||
contents: Vec<transition_api::ObjectInfo>,
|
contents: Vec<transition_api::ObjectInfo>,
|
||||||
@@ -102,10 +102,6 @@ pub struct ListBucketResult {
|
|||||||
prefix: String,
|
prefix: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "fields of a MinIO-parity list result that this port builds but never reads back (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub struct ListMultipartUploadsResult {
|
pub struct ListMultipartUploadsResult {
|
||||||
bucket: String,
|
bucket: String,
|
||||||
key_marker: String,
|
key_marker: String,
|
||||||
@@ -121,15 +117,16 @@ pub struct ListMultipartUploadsResult {
|
|||||||
common_prefixes: Vec<CommonPrefix>,
|
common_prefixes: Vec<CommonPrefix>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "fields of a MinIO-parity list result that this port builds but never reads back (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub struct Initiator {
|
pub struct Initiator {
|
||||||
id: String,
|
id: String,
|
||||||
display_name: String,
|
display_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct CopyObjectResult {
|
||||||
|
pub etag: String,
|
||||||
|
pub last_modified: OffsetDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ObjectPart {
|
pub struct ObjectPart {
|
||||||
pub etag: String,
|
pub etag: String,
|
||||||
@@ -263,7 +260,6 @@ pub struct CompletePart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CompletePart {
|
impl CompletePart {
|
||||||
#[allow(dead_code, reason = "MinIO-parity accessor with no caller in this port (backlog#1823)")]
|
|
||||||
fn checksum(&self, t: &ChecksumMode) -> String {
|
fn checksum(&self, t: &ChecksumMode) -> String {
|
||||||
match t {
|
match t {
|
||||||
ChecksumMode::ChecksumCRC32C => {
|
ChecksumMode::ChecksumCRC32C => {
|
||||||
@@ -288,6 +284,11 @@ impl CompletePart {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct CopyObjectPartResult {
|
||||||
|
pub etag: String,
|
||||||
|
pub last_modified: OffsetDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, serde::Serialize)]
|
#[derive(Debug, Default, serde::Serialize)]
|
||||||
#[serde(rename = "CompleteMultipartUpload")]
|
#[serde(rename = "CompleteMultipartUpload")]
|
||||||
pub struct CompleteMultipartUpload {
|
pub struct CompleteMultipartUpload {
|
||||||
@@ -356,10 +357,10 @@ impl CompleteMultipartUpload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
pub struct CreateBucketConfiguration {
|
||||||
dead_code,
|
pub location: String,
|
||||||
reason = "live via quick_xml::de::from_str in bucket_cache.rs; serde deserialization is not a construction (backlog#1823)"
|
}
|
||||||
)]
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct DeleteObject {
|
pub struct DeleteObject {
|
||||||
//api has
|
//api has
|
||||||
@@ -367,6 +368,21 @@ pub struct DeleteObject {
|
|||||||
pub version_id: String,
|
pub version_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DeletedObject {
|
||||||
|
//s3s has
|
||||||
|
pub key: String,
|
||||||
|
pub version_id: String,
|
||||||
|
pub deletemarker: bool,
|
||||||
|
pub deletemarker_version_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NonDeletedObject {
|
||||||
|
pub key: String,
|
||||||
|
pub code: String,
|
||||||
|
pub message: String,
|
||||||
|
pub version_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct DeleteMultiObjects {
|
pub struct DeleteMultiObjects {
|
||||||
pub quiet: bool,
|
pub quiet: bool,
|
||||||
@@ -386,7 +402,6 @@ impl DeleteMultiObjects {
|
|||||||
Ok(buf)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "MinIO-parity XML helper with no caller in this port (backlog#1823)")]
|
|
||||||
pub fn unmarshal(buf: &[u8]) -> Result<Self, std::io::Error> {
|
pub fn unmarshal(buf: &[u8]) -> Result<Self, std::io::Error> {
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct WireDeleteObject {
|
struct WireDeleteObject {
|
||||||
@@ -421,3 +436,8 @@ impl DeleteMultiObjects {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DeleteMultiObjectsResult {
|
||||||
|
pub deleted_objects: Vec<DeletedObject>,
|
||||||
|
pub undeleted_objects: Vec<NonDeletedObject>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -365,10 +365,6 @@ mod tests {
|
|||||||
pub struct Checksum {
|
pub struct Checksum {
|
||||||
checksum_type: ChecksumMode,
|
checksum_type: ChecksumMode,
|
||||||
r: Vec<u8>,
|
r: Vec<u8>,
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "checksum bookkeeping field kept beside the value it guards (backlog#1823)"
|
|
||||||
)]
|
|
||||||
computed: bool,
|
computed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,5 +32,8 @@ pub const MAX_MULTIPART_PUT_OBJECT_SIZE: i64 = 1024 * 1024 * 1024 * 1024 * 5;
|
|||||||
pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
|
pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
|
||||||
pub const UNSIGNED_PAYLOAD_TRAILER: &str = "STREAMING-UNSIGNED-PAYLOAD-TRAILER";
|
pub const UNSIGNED_PAYLOAD_TRAILER: &str = "STREAMING-UNSIGNED-PAYLOAD-TRAILER";
|
||||||
|
|
||||||
|
pub const TOTAL_WORKERS: i64 = 4;
|
||||||
|
|
||||||
|
pub const SIGN_V4_ALGORITHM: &str = "AWS4-HMAC-SHA256";
|
||||||
pub const ISO8601_DATEFORMAT: &[FormatItem<'_>] =
|
pub const ISO8601_DATEFORMAT: &[FormatItem<'_>] =
|
||||||
format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond]Z");
|
format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond]Z");
|
||||||
|
|||||||
@@ -67,10 +67,6 @@ impl<P: Provider + Default> Credentials<P> {
|
|||||||
Ok(self.creds.clone())
|
Ok(self.creds.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity credential surface with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn expire(&mut self) {
|
fn expire(&mut self) {
|
||||||
self.force_refresh = true;
|
self.force_refresh = true;
|
||||||
}
|
}
|
||||||
@@ -137,10 +133,6 @@ impl Provider for Static {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct STSError {
|
pub struct STSError {
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity STS error detail that this port never reads back (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub r#type: String,
|
pub r#type: String,
|
||||||
pub code: String,
|
pub code: String,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
@@ -149,10 +141,6 @@ pub struct STSError {
|
|||||||
#[derive(Debug, Clone, thiserror::Error)]
|
#[derive(Debug, Clone, thiserror::Error)]
|
||||||
pub struct ErrorResponse {
|
pub struct ErrorResponse {
|
||||||
pub sts_error: STSError,
|
pub sts_error: STSError,
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity STS error detail that this port never reads back (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub request_id: String,
|
pub request_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,3 +158,22 @@ impl ErrorResponse {
|
|||||||
return self.sts_error.message.clone();
|
return self.sts_error.message.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn xml_decoder<T>(body: &[u8]) -> Result<T, Error>
|
||||||
|
where
|
||||||
|
for<'de> T: Deserialize<'de>,
|
||||||
|
{
|
||||||
|
match std::str::from_utf8(body) {
|
||||||
|
Ok(xml_body) => quick_xml::de::from_str::<T>(xml_body).map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string())),
|
||||||
|
Err(err) => Err(Error::new(ErrorKind::InvalidData, err.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xml_decode_and_body<T>(body_reader: &[u8]) -> Result<(Vec<u8>, T), std::io::Error>
|
||||||
|
where
|
||||||
|
for<'de> T: Deserialize<'de>,
|
||||||
|
{
|
||||||
|
let body = body_reader.to_vec();
|
||||||
|
let parsed = xml_decoder(&body)?;
|
||||||
|
Ok((body, parsed))
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: S3 client compatibility models are kept while ECStore callers move to narrower facades.
|
// #730: S3 client compatibility models are kept while ECStore callers move to narrower facades.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
pub mod admin_handler_utils;
|
pub mod admin_handler_utils;
|
||||||
pub mod api_error_response;
|
pub mod api_error_response;
|
||||||
|
|||||||
@@ -77,6 +77,39 @@ fn part_number_to_rangespec(oi: ObjectInfo, part_number: usize) -> Option<HTTPRa
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_compressed_offsets(oi: ObjectInfo, offset: i64) -> (i64, i64, i64, i64, u64) {
|
||||||
|
let mut skip_length: i64 = 0;
|
||||||
|
let mut cumulative_actual_size: i64 = 0;
|
||||||
|
let mut first_part_idx: i64 = 0;
|
||||||
|
let mut compressed_offset: i64 = 0;
|
||||||
|
let mut part_skip: i64 = 0;
|
||||||
|
let mut decrypt_skip: i64 = 0;
|
||||||
|
let mut seq_num: u64 = 0;
|
||||||
|
for (i, part) in oi.parts.iter().enumerate() {
|
||||||
|
cumulative_actual_size += part.actual_size as i64;
|
||||||
|
if cumulative_actual_size <= offset {
|
||||||
|
compressed_offset += part.size as i64;
|
||||||
|
} else {
|
||||||
|
first_part_idx = i as i64;
|
||||||
|
skip_length = cumulative_actual_size - part.actual_size as i64;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
skip_length = offset - skip_length;
|
||||||
|
|
||||||
|
let parts: &[ObjectPartInfo] = &oi.parts;
|
||||||
|
if skip_length > 0
|
||||||
|
&& parts.len() > first_part_idx as usize
|
||||||
|
&& parts[first_part_idx as usize].index.as_ref().is_some_and(|idx| idx.len() > 0)
|
||||||
|
{
|
||||||
|
let _ = part_skip;
|
||||||
|
let _ = decrypt_skip;
|
||||||
|
let _ = seq_num;
|
||||||
|
}
|
||||||
|
|
||||||
|
(compressed_offset, part_skip, first_part_idx, decrypt_skip, seq_num)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_getobjectreader<'a>(
|
pub fn new_getobjectreader<'a>(
|
||||||
rs: &Option<HTTPRangeSpec>,
|
rs: &Option<HTTPRangeSpec>,
|
||||||
oi: &'a ObjectInfo,
|
oi: &'a ObjectInfo,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ const X_OBS_VERSION_ID: &str = "x-obs-version-id";
|
|||||||
const MAX_REMOTE_VERSION_ID_LEN: usize = 1024;
|
const MAX_REMOTE_VERSION_ID_LEN: usize = 1024;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
#[allow(dead_code, reason = "bucket versioning states kept as a complete vocabulary (backlog#1823)")]
|
|
||||||
pub(crate) enum BucketVersioningState {
|
pub(crate) enum BucketVersioningState {
|
||||||
Unknown,
|
Unknown,
|
||||||
Disabled,
|
Disabled,
|
||||||
@@ -48,7 +47,6 @@ impl RemoteVersion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "MinIO-parity accessor with no caller in this port (backlog#1823)")]
|
|
||||||
pub(crate) fn exact_request_id(&self) -> Result<Option<&str>, Error> {
|
pub(crate) fn exact_request_id(&self) -> Result<Option<&str>, Error> {
|
||||||
match self {
|
match self {
|
||||||
Self::Unknown => Err(Error::new(
|
Self::Unknown => Err(Error::new(
|
||||||
|
|||||||
@@ -101,10 +101,6 @@ where
|
|||||||
|
|
||||||
const C_UNKNOWN: i32 = -1;
|
const C_UNKNOWN: i32 = -1;
|
||||||
const C_OFFLINE: i32 = 0;
|
const C_OFFLINE: i32 = 0;
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "reachable only from the unused transition client methods below (backlog#1823)"
|
|
||||||
)]
|
|
||||||
const C_ONLINE: i32 = 1;
|
const C_ONLINE: i32 = 1;
|
||||||
|
|
||||||
fn invalid_utf8_header_error(scope: &str, header_name: &str) -> std::io::Error {
|
fn invalid_utf8_header_error(scope: &str, header_name: &str) -> std::io::Error {
|
||||||
@@ -324,10 +320,6 @@ impl TransitionClient {
|
|||||||
Ok(client)
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity transition client surface with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn endpoint_url(&self) -> Url {
|
fn endpoint_url(&self) -> Url {
|
||||||
self.endpoint_url.clone()
|
self.endpoint_url.clone()
|
||||||
}
|
}
|
||||||
@@ -356,20 +348,12 @@ impl TransitionClient {
|
|||||||
.to_string())
|
.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity transition client method with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn trace_errors_only_off(&self) {
|
fn trace_errors_only_off(&self) {
|
||||||
if let Ok(mut trace_errors_only) = self.trace_errors_only.lock() {
|
if let Ok(mut trace_errors_only) = self.trace_errors_only.lock() {
|
||||||
*trace_errors_only = false;
|
*trace_errors_only = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity transition client method with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn trace_off(&self) {
|
fn trace_off(&self) {
|
||||||
if let Ok(mut is_trace_enabled) = self.is_trace_enabled.lock() {
|
if let Ok(mut is_trace_enabled) = self.is_trace_enabled.lock() {
|
||||||
*is_trace_enabled = false;
|
*is_trace_enabled = false;
|
||||||
@@ -379,20 +363,12 @@ impl TransitionClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity transition client method with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn set_s3_transfer_accelerate(&self, accelerate_endpoint: &str) {
|
fn set_s3_transfer_accelerate(&self, accelerate_endpoint: &str) {
|
||||||
if let Ok(mut endpoint) = self.s3_accelerate_endpoint.lock() {
|
if let Ok(mut endpoint) = self.s3_accelerate_endpoint.lock() {
|
||||||
*endpoint = accelerate_endpoint.to_string();
|
*endpoint = accelerate_endpoint.to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity transition client method with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn set_s3_enable_dual_stack(&self, enabled: bool) {
|
fn set_s3_enable_dual_stack(&self, enabled: bool) {
|
||||||
if let Ok(mut dual_stack) = self.s3_dual_stack_enabled.lock() {
|
if let Ok(mut dual_stack) = self.s3_dual_stack_enabled.lock() {
|
||||||
*dual_stack = enabled;
|
*dual_stack = enabled;
|
||||||
@@ -422,18 +398,10 @@ impl TransitionClient {
|
|||||||
(hash_algos, hash_sums)
|
(hash_algos, hash_sums)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity transition client method with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn is_online(&self) -> bool {
|
fn is_online(&self) -> bool {
|
||||||
!self.is_offline()
|
!self.is_offline()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity transition client method with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn mark_offline(&self) {
|
fn mark_offline(&self) {
|
||||||
self.health_status
|
self.health_status
|
||||||
.compare_exchange(C_ONLINE, C_OFFLINE, Ordering::SeqCst, Ordering::SeqCst);
|
.compare_exchange(C_ONLINE, C_OFFLINE, Ordering::SeqCst, Ordering::SeqCst);
|
||||||
@@ -443,18 +411,10 @@ impl TransitionClient {
|
|||||||
self.health_status.load(Ordering::SeqCst) == C_OFFLINE
|
self.health_status.load(Ordering::SeqCst) == C_OFFLINE
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity transition client method with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn health_check(hc_duration: Duration) {
|
fn health_check(hc_duration: Duration) {
|
||||||
let _ = hc_duration;
|
let _ = hc_duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "MinIO-parity transition client method with no caller in this port (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn dump_http(&self, req: &Request<s3s::Body>, resp: &Response<Incoming>) -> Result<(), std::io::Error> {
|
fn dump_http(&self, req: &Request<s3s::Body>, resp: &Response<Incoming>) -> Result<(), std::io::Error> {
|
||||||
let mut resp_trace: Vec<u8>;
|
let mut resp_trace: Vec<u8>;
|
||||||
|
|
||||||
@@ -1142,7 +1102,6 @@ impl Default for ObjectInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectInfo {
|
impl ObjectInfo {
|
||||||
#[allow(dead_code, reason = "MinIO-parity accessor with no caller in this port (backlog#1823)")]
|
|
||||||
pub(crate) fn remote_version(
|
pub(crate) fn remote_version(
|
||||||
&self,
|
&self,
|
||||||
capabilities: ProviderVersionCapabilities,
|
capabilities: ProviderVersionCapabilities,
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ lazy_static! {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_standard_query_value(qs_key: &str) -> bool {
|
||||||
|
SUPPORTED_QUERY_VALUES[qs_key]
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_storageclass_header(header_key: &str) -> bool {
|
pub fn is_storageclass_header(header_key: &str) -> bool {
|
||||||
header_key.to_lowercase() == X_AMZ_STORAGE_CLASS.as_str().to_lowercase()
|
header_key.to_lowercase() == X_AMZ_STORAGE_CLASS.as_str().to_lowercase()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: cluster/RPC migration leaves transport capabilities staged for upcoming owners.
|
// #730: cluster/RPC migration leaves transport capabilities staged for upcoming owners.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
mod control_plane;
|
mod control_plane;
|
||||||
pub(crate) mod rpc;
|
pub(crate) mod rpc;
|
||||||
|
|||||||
@@ -256,7 +256,6 @@ impl<S> ReplayScopeChannel<S> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "replay-state probe asserted by this file's tests (backlog#1823)")]
|
|
||||||
fn peer_replay_state(audience: &str) -> PeerReplayState {
|
fn peer_replay_state(audience: &str) -> PeerReplayState {
|
||||||
PEER_REPLAY_STATES
|
PEER_REPLAY_STATES
|
||||||
.lock()
|
.lock()
|
||||||
|
|||||||
@@ -43,10 +43,6 @@ use tokio::io::{AsyncReadExt, AsyncWrite};
|
|||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "live in the cfg(not(test)) half of build_internode_data_transport_from_env (backlog#1823)"
|
|
||||||
)]
|
|
||||||
static INTERNODE_DATA_TRANSPORT: OnceLock<std::result::Result<Arc<dyn InternodeDataTransport>, String>> = OnceLock::new();
|
static INTERNODE_DATA_TRANSPORT: OnceLock<std::result::Result<Arc<dyn InternodeDataTransport>, String>> = OnceLock::new();
|
||||||
|
|
||||||
const READ_FILE_STREAM_PATH: &str = "/rustfs/rpc/read_file_stream";
|
const READ_FILE_STREAM_PATH: &str = "/rustfs/rpc/read_file_stream";
|
||||||
@@ -138,10 +134,6 @@ fn put_file_capability_status_is_legacy(status: u16) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "capability-negotiation seam; constructed only by transport test doubles (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub struct InternodeDataTransportCapabilities {
|
pub struct InternodeDataTransportCapabilities {
|
||||||
/// Backend can open a streaming remote disk reader.
|
/// Backend can open a streaming remote disk reader.
|
||||||
pub streaming_read: bool,
|
pub streaming_read: bool,
|
||||||
@@ -158,10 +150,6 @@ pub struct InternodeDataTransportCapabilities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InternodeDataTransportCapabilities {
|
impl InternodeDataTransportCapabilities {
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "capability-negotiation seam; used by transport test doubles (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub const fn tcp_http() -> Self {
|
pub const fn tcp_http() -> Self {
|
||||||
Self {
|
Self {
|
||||||
streaming_read: true,
|
streaming_read: true,
|
||||||
@@ -246,12 +234,7 @@ pub trait InternodeDataTransport: Send + Sync + std::fmt::Debug {
|
|||||||
async fn probe_ns_scanner(&self, _request: NsScannerCapabilityRequest) -> Result<Uuid> {
|
async fn probe_ns_scanner(&self, _request: NsScannerCapabilityRequest) -> Result<Uuid> {
|
||||||
Err(Error::MethodNotAllowed)
|
Err(Error::MethodNotAllowed)
|
||||||
}
|
}
|
||||||
// Interface facet nobody calls yet: every transport implements both, but no
|
|
||||||
// caller negotiates on them. Kept for the internode transport split
|
|
||||||
// (backlog#1350); deleting them would delete the seam and six impls.
|
|
||||||
#[allow(dead_code, reason = "unused capability-negotiation facet (backlog#1823)")]
|
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
#[allow(dead_code, reason = "unused capability-negotiation facet (backlog#1823)")]
|
|
||||||
fn capabilities(&self) -> InternodeDataTransportCapabilities;
|
fn capabilities(&self) -> InternodeDataTransportCapabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,10 +670,6 @@ fn build_internode_data_transport_result(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "live in the cfg(test) half of build_internode_data_transport_from_env, which bypasses the process static (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub fn build_internode_data_transport(configured_transport: Option<&str>) -> Result<Arc<dyn InternodeDataTransport>> {
|
pub fn build_internode_data_transport(configured_transport: Option<&str>) -> Result<Arc<dyn InternodeDataTransport>> {
|
||||||
build_internode_data_transport_result(configured_transport).map_err(Error::other)
|
build_internode_data_transport_result(configured_transport).map_err(Error::other)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,16 +248,6 @@ fn decode_remote_version_state_capability(expected_member: &str, result: &[u8])
|
|||||||
Ok(server_epoch)
|
Ok(server_epoch)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_cross_pool_fence_capability(expected_member: &str, result: &[u8]) -> Result<(u32, Uuid)> {
|
|
||||||
let version = result
|
|
||||||
.get(..4)
|
|
||||||
.and_then(|value| value.try_into().ok())
|
|
||||||
.map(u32::from_be_bytes)
|
|
||||||
.ok_or_else(|| Error::other("peer returned an invalid cross-pool fence capability version"))?;
|
|
||||||
let epoch = decode_remote_version_state_capability(expected_member, &result[4..])?;
|
|
||||||
Ok((version, epoch))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct PeerLiveEventsBatch {
|
pub struct PeerLiveEventsBatch {
|
||||||
pub events: Vec<u8>,
|
pub events: Vec<u8>,
|
||||||
@@ -1298,16 +1288,6 @@ impl PeerRestClient {
|
|||||||
Ok((self.topology_member.clone(), epoch))
|
Ok((self.topology_member.clone(), epoch))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn probe_cross_pool_fence(&self, topology_fingerprint: String) -> Result<(String, u32, Uuid)> {
|
|
||||||
let mut probe = rustfs_protos::CROSS_POOL_FENCE_CAPABILITY_PROBE_PREFIX.to_vec();
|
|
||||||
probe.extend_from_slice(Uuid::new_v4().as_bytes());
|
|
||||||
let result = self
|
|
||||||
.heal_control(rustfs_protos::HEAL_CONTROL_PROTOCOL_VERSION, topology_fingerprint, probe)
|
|
||||||
.await?;
|
|
||||||
let (supported_version, epoch) = decode_cross_pool_fence_capability(&self.topology_member, &result)?;
|
|
||||||
Ok((self.topology_member.clone(), supported_version, epoch))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn load_bucket_metadata(&self, bucket: &str, scanner_maintenance_change: bool) -> Result<()> {
|
pub async fn load_bucket_metadata(&self, bucket: &str, scanner_maintenance_change: bool) -> Result<()> {
|
||||||
self.finalize_result(
|
self.finalize_result(
|
||||||
async {
|
async {
|
||||||
@@ -2758,24 +2738,6 @@ mod tests {
|
|||||||
assert!(decode_remote_version_state_capability("node-a:9000", &nil).is_err());
|
assert!(decode_remote_version_state_capability("node-a:9000", &nil).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cross_pool_fence_capability_decoder_fails_closed() {
|
|
||||||
let epoch = Uuid::new_v4();
|
|
||||||
let result = rustfs_protos::encode_cross_pool_fence_capability(1, "node-a:9000", epoch.as_bytes())
|
|
||||||
.expect("small capability response should encode");
|
|
||||||
assert_eq!(
|
|
||||||
decode_cross_pool_fence_capability("node-a:9000", &result).expect("valid capability should decode"),
|
|
||||||
(1, epoch)
|
|
||||||
);
|
|
||||||
for malformed in [&[][..], &[0, 0, 0][..], &result[..result.len() - 1]] {
|
|
||||||
assert!(decode_cross_pool_fence_capability("node-a:9000", malformed).is_err());
|
|
||||||
}
|
|
||||||
assert!(decode_cross_pool_fence_capability("node-b:9000", &result).is_err());
|
|
||||||
let nil = rustfs_protos::encode_cross_pool_fence_capability(1, "node-a:9000", Uuid::nil().as_bytes())
|
|
||||||
.expect("small capability response should encode");
|
|
||||||
assert!(decode_cross_pool_fence_capability("node-a:9000", &nil).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TierMutationResponseFixture<'a> {
|
struct TierMutationResponseFixture<'a> {
|
||||||
version: u32,
|
version: u32,
|
||||||
phase: TierMutationRpcPhase,
|
phase: TierMutationRpcPhase,
|
||||||
|
|||||||
@@ -854,6 +854,7 @@ impl PeerS3Client for LocalPeerS3Client {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RemotePeerS3Client {
|
pub struct RemotePeerS3Client {
|
||||||
|
pub node: Option<Node>,
|
||||||
pub pools: Option<Vec<usize>>,
|
pub pools: Option<Vec<usize>>,
|
||||||
addr: String,
|
addr: String,
|
||||||
/// Health tracker for connection monitoring
|
/// Health tracker for connection monitoring
|
||||||
@@ -885,6 +886,7 @@ impl RemotePeerS3Client {
|
|||||||
pub fn new(node: Option<Node>, pools: Option<Vec<usize>>) -> Self {
|
pub fn new(node: Option<Node>, pools: Option<Vec<usize>>) -> Self {
|
||||||
let addr = node.as_ref().map(|v| v.url.to_string()).unwrap_or_default();
|
let addr = node.as_ref().map(|v| v.url.to_string()).unwrap_or_default();
|
||||||
let client = Self {
|
let client = Self {
|
||||||
|
node,
|
||||||
pools,
|
pools,
|
||||||
addr,
|
addr,
|
||||||
health: Arc::new(DiskHealthTracker::new()),
|
health: Arc::new(DiskHealthTracker::new()),
|
||||||
@@ -903,6 +905,10 @@ impl RemotePeerS3Client {
|
|||||||
.map_err(|err| Error::other(format!("can not get client, err: {err}")))
|
.map_err(|err| Error::other(format!("can not get client, err: {err}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_addr(&self) -> String {
|
||||||
|
self.addr.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Start health monitoring for the remote peer
|
/// Start health monitoring for the remote peer
|
||||||
fn start_health_monitoring(&self) {
|
fn start_health_monitoring(&self) {
|
||||||
let health = Arc::clone(&self.health);
|
let health = Arc::clone(&self.health);
|
||||||
@@ -1202,10 +1208,6 @@ impl PeerS3Client for RemotePeerS3Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "local bucket-heal path reached only by this file's tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub async fn heal_bucket_local(bucket: &str, opts: &HealOpts) -> Result<HealResultItem> {
|
pub async fn heal_bucket_local(bucket: &str, opts: &HealOpts) -> Result<HealResultItem> {
|
||||||
let disks = clone_drives().await;
|
let disks = clone_drives().await;
|
||||||
heal_bucket_local_on_disks(bucket, opts, disks).await
|
heal_bucket_local_on_disks(bucket, opts, disks).await
|
||||||
@@ -1402,10 +1404,6 @@ pub(crate) async fn heal_bucket_local_on_disks(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "reached only through heal_bucket_local, which only tests call (backlog#1823)"
|
|
||||||
)]
|
|
||||||
async fn clone_drives() -> Vec<Option<DiskStore>> {
|
async fn clone_drives() -> Vec<Option<DiskStore>> {
|
||||||
runtime_sources::local_disk_entries().await
|
runtime_sources::local_disk_entries().await
|
||||||
}
|
}
|
||||||
@@ -1587,7 +1585,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn test_remote_peer(addr: &str) -> RemotePeerS3Client {
|
fn test_remote_peer(addr: &str) -> RemotePeerS3Client {
|
||||||
|
let node = Node {
|
||||||
|
url: url::Url::parse(addr).expect("test peer URL should parse"),
|
||||||
|
pools: vec![0],
|
||||||
|
is_local: false,
|
||||||
|
grid_host: addr.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
RemotePeerS3Client {
|
RemotePeerS3Client {
|
||||||
|
node: Some(node),
|
||||||
pools: Some(vec![0]),
|
pools: Some(vec![0]),
|
||||||
addr: addr.to_string(),
|
addr: addr.to_string(),
|
||||||
health: Arc::new(DiskHealthTracker::new()),
|
health: Arc::new(DiskHealthTracker::new()),
|
||||||
|
|||||||
@@ -1359,71 +1359,6 @@ fn validate_decoded_file_info(file_info: &FileInfo) -> Result<()> {
|
|||||||
file_info.validate_for_metadata_read().map_err(Into::into)
|
file_info.validate_for_metadata_read().map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RemoteDisk {
|
|
||||||
#[tracing::instrument(level = "trace", skip_all)]
|
|
||||||
pub(crate) async fn rename_data_borrowed(
|
|
||||||
&self,
|
|
||||||
src_volume: &str,
|
|
||||||
src_path: &str,
|
|
||||||
fi: &FileInfo,
|
|
||||||
dst_volume: &str,
|
|
||||||
dst_path: &str,
|
|
||||||
) -> Result<RenameDataResp> {
|
|
||||||
trace!(
|
|
||||||
event = EVENT_REMOTE_DISK_RPC,
|
|
||||||
component = LOG_COMPONENT_ECSTORE,
|
|
||||||
subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
|
|
||||||
endpoint = %self.endpoint,
|
|
||||||
src_volume,
|
|
||||||
src_path,
|
|
||||||
dst_volume,
|
|
||||||
dst_path,
|
|
||||||
op = "rename_data",
|
|
||||||
state = "started",
|
|
||||||
"Remote disk RPC started"
|
|
||||||
);
|
|
||||||
|
|
||||||
self.execute_with_timeout_for_op(
|
|
||||||
"rename_data",
|
|
||||||
|| async {
|
|
||||||
let file_info = compat_json(fi)?;
|
|
||||||
let file_info_bin = encode_file_info_msgpack(fi)?;
|
|
||||||
let mut client = self
|
|
||||||
.get_client()
|
|
||||||
.await
|
|
||||||
.map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
|
|
||||||
let mut request = Request::new(RenameDataRequest {
|
|
||||||
disk: self.endpoint.to_string(),
|
|
||||||
src_volume: src_volume.to_string(),
|
|
||||||
src_path: src_path.to_string(),
|
|
||||||
file_info,
|
|
||||||
dst_volume: dst_volume.to_string(),
|
|
||||||
dst_path: dst_path.to_string(),
|
|
||||||
file_info_bin: file_info_bin.into(),
|
|
||||||
});
|
|
||||||
let canonical_body = rustfs_protos::canonical_rename_data_request_body(request.get_ref());
|
|
||||||
attach_mutation_body_digest(&mut request, canonical_body, "rename_data")?;
|
|
||||||
|
|
||||||
let response = client.rename_data(request).await?.into_inner();
|
|
||||||
|
|
||||||
if !response.success {
|
|
||||||
return Err(response.error.unwrap_or_default().into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let rename_data_resp = decode_msgpack_or_json::<RenameDataResp>(
|
|
||||||
&response.rename_data_resp_bin,
|
|
||||||
&response.rename_data_resp,
|
|
||||||
"RenameDataResp",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(rename_data_resp)
|
|
||||||
},
|
|
||||||
get_max_timeout_duration(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl DiskAPI for RemoteDisk {
|
impl DiskAPI for RemoteDisk {
|
||||||
#[tracing::instrument(level = "trace", skip_all)]
|
#[tracing::instrument(level = "trace", skip_all)]
|
||||||
@@ -2349,8 +2284,58 @@ impl DiskAPI for RemoteDisk {
|
|||||||
dst_volume: &str,
|
dst_volume: &str,
|
||||||
dst_path: &str,
|
dst_path: &str,
|
||||||
) -> Result<RenameDataResp> {
|
) -> Result<RenameDataResp> {
|
||||||
self.rename_data_borrowed(src_volume, src_path, &fi, dst_volume, dst_path)
|
trace!(
|
||||||
.await
|
event = EVENT_REMOTE_DISK_RPC,
|
||||||
|
component = LOG_COMPONENT_ECSTORE,
|
||||||
|
subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
|
||||||
|
endpoint = %self.endpoint,
|
||||||
|
src_volume,
|
||||||
|
src_path,
|
||||||
|
dst_volume,
|
||||||
|
dst_path,
|
||||||
|
op = "rename_data",
|
||||||
|
state = "started",
|
||||||
|
"Remote disk RPC started"
|
||||||
|
);
|
||||||
|
|
||||||
|
self.execute_with_timeout_for_op(
|
||||||
|
"rename_data",
|
||||||
|
|| async {
|
||||||
|
let file_info = compat_json(&fi)?;
|
||||||
|
let file_info_bin = encode_file_info_msgpack(&fi)?;
|
||||||
|
let mut client = self
|
||||||
|
.get_client()
|
||||||
|
.await
|
||||||
|
.map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
|
||||||
|
let mut request = Request::new(RenameDataRequest {
|
||||||
|
disk: self.endpoint.to_string(),
|
||||||
|
src_volume: src_volume.to_string(),
|
||||||
|
src_path: src_path.to_string(),
|
||||||
|
file_info,
|
||||||
|
dst_volume: dst_volume.to_string(),
|
||||||
|
dst_path: dst_path.to_string(),
|
||||||
|
file_info_bin: file_info_bin.into(),
|
||||||
|
});
|
||||||
|
let canonical_body = rustfs_protos::canonical_rename_data_request_body(request.get_ref());
|
||||||
|
attach_mutation_body_digest(&mut request, canonical_body, "rename_data")?;
|
||||||
|
|
||||||
|
let response = client.rename_data(request).await?.into_inner();
|
||||||
|
|
||||||
|
if !response.success {
|
||||||
|
return Err(response.error.unwrap_or_default().into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let rename_data_resp = decode_msgpack_or_json::<RenameDataResp>(
|
||||||
|
&response.rename_data_resp_bin,
|
||||||
|
&response.rename_data_resp,
|
||||||
|
"RenameDataResp",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(rename_data_resp)
|
||||||
|
},
|
||||||
|
get_max_timeout_duration(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "trace", skip_all)]
|
#[tracing::instrument(level = "trace", skip_all)]
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ impl RemoteClient {
|
|||||||
Self { addr: endpoint }
|
Self { addr: endpoint }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_url(url: url::Url) -> Self {
|
||||||
|
Self { addr: url.to_string() }
|
||||||
|
}
|
||||||
|
|
||||||
fn build_ping_request() -> PingRequest {
|
fn build_ping_request() -> PingRequest {
|
||||||
let mut fbb = flatbuffers::FlatBufferBuilder::new();
|
let mut fbb = flatbuffers::FlatBufferBuilder::new();
|
||||||
let payload = fbb.create_vector(b"health-check");
|
let payload = fbb.create_vector(b"health-check");
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ use rustfs_config::{
|
|||||||
SCANNER_SUB_SYS,
|
SCANNER_SUB_SYS,
|
||||||
};
|
};
|
||||||
use rustfs_filemeta::FileInfo;
|
use rustfs_filemeta::FileInfo;
|
||||||
|
use rustfs_utils::path::SLASH_SEPARATOR;
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
@@ -199,6 +200,8 @@ pub const STORAGE_CLASS_SUB_SYS: &str = "storage_class";
|
|||||||
|
|
||||||
pub const COMMA_SEPARATED_LISTS: &[&str] = &[rustfs_config::oidc::OIDC_SCOPES, rustfs_config::oidc::OIDC_OTHER_AUDIENCES];
|
pub const COMMA_SEPARATED_LISTS: &[&str] = &[rustfs_config::oidc::OIDC_SCOPES, rustfs_config::oidc::OIDC_OTHER_AUDIENCES];
|
||||||
|
|
||||||
|
static CONFIG_BUCKET: LazyLock<String> = LazyLock::new(|| format!("{RUSTFS_META_BUCKET}{SLASH_SEPARATOR}{CONFIG_PREFIX}"));
|
||||||
|
|
||||||
type ServerConfigDecryptFn = crate::bucket::migration::LegacyBlobDecryptFn;
|
type ServerConfigDecryptFn = crate::bucket::migration::LegacyBlobDecryptFn;
|
||||||
|
|
||||||
static SERVER_CONFIG_DECRYPT_FN: LazyLock<RwLock<Option<ServerConfigDecryptFn>>> = LazyLock::new(|| RwLock::new(None));
|
static SERVER_CONFIG_DECRYPT_FN: LazyLock<RwLock<Option<ServerConfigDecryptFn>>> = LazyLock::new(|| RwLock::new(None));
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: configuration migration keeps legacy subsystem definitions available behind this module.
|
// #730: configuration migration keeps legacy subsystem definitions available behind this module.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
mod audit;
|
mod audit;
|
||||||
pub mod com;
|
pub mod com;
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ const DEFAULT_RRS_STORAGE_CLASS: &str = "EC:1";
|
|||||||
const ZERO_SET_DRIVE_COUNT_ERROR: &str = "set drive count must be greater than zero";
|
const ZERO_SET_DRIVE_COUNT_ERROR: &str = "set drive count must be greater than zero";
|
||||||
|
|
||||||
pub static DEFAULT_INLINE_BLOCK: usize = 128 * 1024;
|
pub static DEFAULT_INLINE_BLOCK: usize = 128 * 1024;
|
||||||
const DEFAULT_INLINE_OBJECT_BUDGET: usize = 2 * DEFAULT_INLINE_BLOCK;
|
|
||||||
|
|
||||||
pub static DEFAULT_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
pub static DEFAULT_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
||||||
let kvs = vec![
|
let kvs = vec![
|
||||||
@@ -151,8 +150,6 @@ pub struct Config {
|
|||||||
optimize: Option<String>,
|
optimize: Option<String>,
|
||||||
inline_block: usize,
|
inline_block: usize,
|
||||||
initialized: bool,
|
initialized: bool,
|
||||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
|
||||||
inline_block_explicit: bool,
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
standard_parities: Vec<PoolParity>,
|
standard_parities: Vec<PoolParity>,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
@@ -189,10 +186,6 @@ impl Config {
|
|||||||
/// A topology-bound lookup fails closed for unknown drive counts and for
|
/// A topology-bound lookup fails closed for unknown drive counts and for
|
||||||
/// deserialized legacy configurations that have no pool topology. Legacy
|
/// deserialized legacy configurations that have no pool topology. Legacy
|
||||||
/// callers retain scalar compatibility through [`Self::get_parity_for_sc`].
|
/// callers retain scalar compatibility through [`Self::get_parity_for_sc`].
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "per-set parity resolution asserted by this file's tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) fn parity_for_sc(&self, sc: &str, drives_per_set: usize) -> Option<usize> {
|
pub(crate) fn parity_for_sc(&self, sc: &str, drives_per_set: usize) -> Option<usize> {
|
||||||
if !self.initialized {
|
if !self.initialized {
|
||||||
return None;
|
return None;
|
||||||
@@ -240,19 +233,17 @@ impl Config {
|
|||||||
.map(|(pool_index, pool)| (pool_index, pool.drives_per_set))
|
.map(|(pool_index, pool)| (pool_index, pool.drives_per_set))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn should_inline(&self, shard_size: i64, data_shards: usize, versioned: bool) -> bool {
|
pub fn should_inline(&self, shard_size: i64, versioned: bool) -> bool {
|
||||||
if shard_size < 0 || data_shards == 0 {
|
if shard_size < 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let shard_size = shard_size as usize;
|
let shard_size = shard_size as usize;
|
||||||
// Keep the historical two-data-shard object budget while preventing
|
|
||||||
// wider EC layouts from multiplying the maximum inline object size.
|
let mut inline_block = DEFAULT_INLINE_BLOCK;
|
||||||
let inline_block = if self.initialized && self.inline_block_explicit {
|
if self.initialized {
|
||||||
self.inline_block
|
inline_block = self.inline_block;
|
||||||
} else {
|
}
|
||||||
(DEFAULT_INLINE_OBJECT_BUDGET / data_shards).min(DEFAULT_INLINE_BLOCK)
|
|
||||||
};
|
|
||||||
|
|
||||||
if versioned {
|
if versioned {
|
||||||
shard_size <= inline_block / 8
|
shard_size <= inline_block / 8
|
||||||
@@ -401,7 +392,6 @@ fn lookup_config_for_pools_with_env(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let optimize = overrides.optimize;
|
let optimize = overrides.optimize;
|
||||||
let inline_block_explicit = overrides.inline_block.is_some();
|
|
||||||
let inline_block = if let Some(value) = overrides.inline_block {
|
let inline_block = if let Some(value) = overrides.inline_block {
|
||||||
let block = value
|
let block = value
|
||||||
.parse::<bytesize::ByteSize>()
|
.parse::<bytesize::ByteSize>()
|
||||||
@@ -434,7 +424,6 @@ fn lookup_config_for_pools_with_env(
|
|||||||
optimize,
|
optimize,
|
||||||
inline_block,
|
inline_block,
|
||||||
initialized: true,
|
initialized: true,
|
||||||
inline_block_explicit,
|
|
||||||
standard_parities,
|
standard_parities,
|
||||||
rrs_parities,
|
rrs_parities,
|
||||||
})
|
})
|
||||||
@@ -552,26 +541,22 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_inline_scales_default_threshold_by_data_shards() {
|
fn should_inline_preserves_exact_default_shard_boundaries() {
|
||||||
let config = lookup_config_for_pools_with_env(&KVS::new(), &[3, 12], no_env_overrides())
|
let config = Config::default();
|
||||||
.expect("default inline policy should resolve for EC2+1 and EC8+4");
|
|
||||||
|
|
||||||
for (case, shard_size, data_shards, versioned, expected) in [
|
for (case, shard_size, versioned, expected) in [
|
||||||
("EC2+1 unversioned exact", 128 * 1024, 2, false, true),
|
("unversioned below", 128 * 1024 - 1, false, true),
|
||||||
("EC2+1 unversioned above", 128 * 1024 + 1, 2, false, false),
|
("unversioned exact", 128 * 1024, false, true),
|
||||||
("EC2+1 versioned exact", 16 * 1024, 2, true, true),
|
("unversioned above", 128 * 1024 + 1, false, false),
|
||||||
("EC2+1 versioned above", 16 * 1024 + 1, 2, true, false),
|
("versioned below", 16 * 1024 - 1, true, true),
|
||||||
("EC8+4 unversioned exact", 32 * 1024, 8, false, true),
|
("versioned exact", 16 * 1024, true, true),
|
||||||
("EC8+4 unversioned above", 32 * 1024 + 1, 8, false, false),
|
("versioned above", 16 * 1024 + 1, true, false),
|
||||||
("EC8+4 versioned exact", 4 * 1024, 8, true, true),
|
("negative", -1, false, false),
|
||||||
("EC8+4 versioned above", 4 * 1024 + 1, 8, true, false),
|
|
||||||
("negative", -1, 2, false, false),
|
|
||||||
("zero data shards", 0, 0, false, false),
|
|
||||||
] {
|
] {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
config.should_inline(shard_size, data_shards, versioned),
|
config.should_inline(shard_size, versioned),
|
||||||
expected,
|
expected,
|
||||||
"{case}: shard_size={shard_size}, data_shards={data_shards}, versioned={versioned}"
|
"{case}: shard_size={shard_size}, versioned={versioned}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -592,28 +577,13 @@ mod tests {
|
|||||||
let shard_size = erasure.shard_file_size(object_size);
|
let shard_size = erasure.shard_file_size(object_size);
|
||||||
assert_eq!(shard_size, expected_shard_size, "{case}: object_size={object_size}");
|
assert_eq!(shard_size, expected_shard_size, "{case}: object_size={object_size}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
config.should_inline(shard_size, erasure.data_shards, versioned),
|
config.should_inline(shard_size, versioned),
|
||||||
expected,
|
expected,
|
||||||
"{case}: object_size={object_size}, shard_size={shard_size}, versioned={versioned}"
|
"{case}: object_size={object_size}, shard_size={shard_size}, versioned={versioned}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn explicit_inline_block_preserves_fixed_per_shard_rollback() {
|
|
||||||
let overrides = StorageClassEnvOverrides {
|
|
||||||
inline_block: Some("128KiB".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let config = lookup_config_for_pools_with_env(&KVS::new(), &[12], overrides)
|
|
||||||
.expect("explicit inline block should resolve for EC8+4");
|
|
||||||
|
|
||||||
assert!(config.should_inline(128 * 1024, 8, false));
|
|
||||||
assert!(!config.should_inline(128 * 1024 + 1, 8, false));
|
|
||||||
assert!(config.should_inline(16 * 1024, 8, true));
|
|
||||||
assert!(!config.should_inline(16 * 1024 + 1, 8, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn write_capability_contract_only_accepts_implemented_layouts() {
|
fn write_capability_contract_only_accepts_implemented_layouts() {
|
||||||
assert_eq!(SUPPORTED_WRITE_CLASSES, [STANDARD, RRS]);
|
assert_eq!(SUPPORTED_WRITE_CLASSES, [STANDARD, RRS]);
|
||||||
@@ -807,7 +777,6 @@ mod tests {
|
|||||||
let encoded = serde_json::to_string(&cfg).expect("config should serialize");
|
let encoded = serde_json::to_string(&cfg).expect("config should serialize");
|
||||||
assert!(!encoded.contains("standard_parities"));
|
assert!(!encoded.contains("standard_parities"));
|
||||||
assert!(!encoded.contains("rrs_parities"));
|
assert!(!encoded.contains("rrs_parities"));
|
||||||
assert!(!encoded.contains("inline_block_explicit"));
|
|
||||||
|
|
||||||
let decoded: Config = serde_json::from_str(&encoded).expect("legacy scalar config should deserialize");
|
let decoded: Config = serde_json::from_str(&encoded).expect("legacy scalar config should deserialize");
|
||||||
assert_eq!(decoded.get_parity_for_sc(STANDARD), Some(2));
|
assert_eq!(decoded.get_parity_for_sc(STANDARD), Some(2));
|
||||||
@@ -817,25 +786,6 @@ mod tests {
|
|||||||
assert!(validate_parity(0, 0).is_err());
|
assert!(validate_parity(0, 0).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn explicit_inline_block_survives_config_round_trip() {
|
|
||||||
let cfg = lookup_config_for_pools_with_env(
|
|
||||||
&KVS::new(),
|
|
||||||
&[12],
|
|
||||||
StorageClassEnvOverrides {
|
|
||||||
inline_block: Some("128KiB".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("explicit inline block should resolve");
|
|
||||||
assert!(cfg.should_inline(100 * 1024, 8, false));
|
|
||||||
|
|
||||||
let encoded = serde_json::to_string(&cfg).expect("config should serialize");
|
|
||||||
assert!(encoded.contains("\"inline_block_explicit\":true"));
|
|
||||||
let decoded: Config = serde_json::from_str(&encoded).expect("explicit inline config should deserialize");
|
|
||||||
assert!(decoded.should_inline(100 * 1024, 8, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lookup_config_reads_rrs_from_class_rrs_key() {
|
fn lookup_config_reads_rrs_from_class_rrs_key() {
|
||||||
// Regression: kvs.get(RRS) used RRS="REDUCED_REDUNDANCY" instead of
|
// Regression: kvs.get(RRS) used RRS="REDUCED_REDUNDANCY" instead of
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: pool coordination helpers are being migrated behind runtime owners.
|
// #730: pool coordination helpers are being migrated behind runtime owners.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
pub(crate) mod pools;
|
pub(crate) mod pools;
|
||||||
pub(crate) mod sets;
|
pub(crate) mod sets;
|
||||||
|
|||||||
@@ -226,7 +226,6 @@ fn ensure_decommission_start_rebalance_meta_allowed(meta: Option<&RebalanceMeta>
|
|||||||
ensure_decommission_not_rebalancing(meta.is_some_and(is_rebalance_conflicting_with_decommission))
|
ensure_decommission_not_rebalancing(meta.is_some_and(is_rebalance_conflicting_with_decommission))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "leader precondition asserted by this file's tests (backlog#1823)")]
|
|
||||||
fn ensure_local_decommission_pool_leaders(endpoints: &EndpointServerPools, indices: &[usize]) -> Result<()> {
|
fn ensure_local_decommission_pool_leaders(endpoints: &EndpointServerPools, indices: &[usize]) -> Result<()> {
|
||||||
for idx in indices {
|
for idx in indices {
|
||||||
ensure_local_decommission_pool_leader(endpoints, *idx)?;
|
ensure_local_decommission_pool_leader(endpoints, *idx)?;
|
||||||
@@ -1059,19 +1058,11 @@ fn should_cleanup_decommission_source_entry(decommissioned: usize, total_version
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "terminal-state classification asserted by this file's tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
enum DecommissionTerminalState {
|
enum DecommissionTerminalState {
|
||||||
Completed,
|
Completed,
|
||||||
Failed,
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "terminal-state classification asserted by this file's tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn classify_decommission_terminal_state(failed_items_present: bool) -> DecommissionTerminalState {
|
fn classify_decommission_terminal_state(failed_items_present: bool) -> DecommissionTerminalState {
|
||||||
if failed_items_present {
|
if failed_items_present {
|
||||||
DecommissionTerminalState::Failed
|
DecommissionTerminalState::Failed
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: data-movement migration keeps staged cleanup helpers until copy paths converge.
|
// #730: data-movement migration keeps staged cleanup helpers until copy paths converge.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
pub(crate) mod backpressure;
|
pub(crate) mod backpressure;
|
||||||
|
|
||||||
@@ -1018,10 +1019,6 @@ struct SourceCleanupDeleteBarrierState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "installed by set_disk object tests behind `--features test-util` (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) struct SourceCleanupDeleteBarrier {
|
pub(crate) struct SourceCleanupDeleteBarrier {
|
||||||
state: Arc<SourceCleanupDeleteBarrierState>,
|
state: Arc<SourceCleanupDeleteBarrierState>,
|
||||||
}
|
}
|
||||||
@@ -1031,10 +1028,6 @@ static SOURCE_CLEANUP_DELETE_BARRIER: std::sync::OnceLock<std::sync::Mutex<Optio
|
|||||||
std::sync::OnceLock::new();
|
std::sync::OnceLock::new();
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "installed by set_disk object tests behind `--features test-util` (backlog#1823)"
|
|
||||||
)]
|
|
||||||
impl SourceCleanupDeleteBarrier {
|
impl SourceCleanupDeleteBarrier {
|
||||||
pub(crate) fn install(bucket: &str, object: &str) -> Self {
|
pub(crate) fn install(bucket: &str, object: &str) -> Self {
|
||||||
let state = Arc::new(SourceCleanupDeleteBarrierState {
|
let state = Arc::new(SourceCleanupDeleteBarrierState {
|
||||||
@@ -1173,7 +1166,6 @@ async fn find_data_movement_target_info(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "resume adjudication asserted by this file's tests (backlog#1823)")]
|
|
||||||
fn resolve_data_movement_overwrite_resume_result(
|
fn resolve_data_movement_overwrite_resume_result(
|
||||||
err: &Error,
|
err: &Error,
|
||||||
target_result: Result<Option<ObjectInfo>>,
|
target_result: Result<Option<ObjectInfo>>,
|
||||||
|
|||||||
@@ -12,16 +12,6 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
//! Per-disk usage snapshots persisted under the metadata bucket.
|
|
||||||
//!
|
|
||||||
//! **Nothing calls into this module.** It landed complete with tests in #5307
|
|
||||||
//! (2026-07-27) and its aggregation entry point,
|
|
||||||
//! [`crate::data_usage::aggregate_local_snapshots`], has never had a caller in
|
|
||||||
//! the tree's history. The live data-usage path is
|
|
||||||
//! `load_data_usage_from_backend` / `store_data_usage_in_backend`. The items
|
|
||||||
//! below therefore carry individual `dead_code` allows rather than a module
|
|
||||||
//! blanket, so the gap stays greppable until it is either wired up or removed.
|
|
||||||
|
|
||||||
use crate::data_usage::BucketUsageInfo;
|
use crate::data_usage::BucketUsageInfo;
|
||||||
use crate::disk::RUSTFS_META_BUCKET;
|
use crate::disk::RUSTFS_META_BUCKET;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
@@ -36,12 +26,10 @@ pub const DATA_USAGE_DIR: &str = "datausage";
|
|||||||
/// Directory used to store incremental scan state files under the metadata bucket.
|
/// Directory used to store incremental scan state files under the metadata bucket.
|
||||||
pub const DATA_USAGE_STATE_DIR: &str = "datausage/state";
|
pub const DATA_USAGE_STATE_DIR: &str = "datausage/state";
|
||||||
/// Snapshot file format version, allows forward compatibility if the structure evolves.
|
/// Snapshot file format version, allows forward compatibility if the structure evolves.
|
||||||
#[allow(dead_code, reason = "unwired local usage-snapshot feature; see module docs (backlog#1823)")]
|
|
||||||
pub const LOCAL_USAGE_SNAPSHOT_VERSION: u32 = 1;
|
pub const LOCAL_USAGE_SNAPSHOT_VERSION: u32 = 1;
|
||||||
|
|
||||||
/// Additional metadata describing which disk produced the snapshot.
|
/// Additional metadata describing which disk produced the snapshot.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
#[allow(dead_code, reason = "unwired local usage-snapshot feature; see module docs (backlog#1823)")]
|
|
||||||
pub struct LocalUsageSnapshotMeta {
|
pub struct LocalUsageSnapshotMeta {
|
||||||
/// Disk UUID stored as a string for simpler serialization.
|
/// Disk UUID stored as a string for simpler serialization.
|
||||||
pub disk_id: String,
|
pub disk_id: String,
|
||||||
@@ -55,7 +43,6 @@ pub struct LocalUsageSnapshotMeta {
|
|||||||
|
|
||||||
/// Usage snapshot produced by a single disk.
|
/// Usage snapshot produced by a single disk.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
#[allow(dead_code, reason = "unwired local usage-snapshot feature; see module docs (backlog#1823)")]
|
|
||||||
pub struct LocalUsageSnapshot {
|
pub struct LocalUsageSnapshot {
|
||||||
/// Format version recorded in the snapshot.
|
/// Format version recorded in the snapshot.
|
||||||
pub format_version: u32,
|
pub format_version: u32,
|
||||||
@@ -77,7 +64,6 @@ pub struct LocalUsageSnapshot {
|
|||||||
pub objects_total_size: u64,
|
pub objects_total_size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "unwired local usage-snapshot feature; see module docs (backlog#1823)")]
|
|
||||||
impl LocalUsageSnapshot {
|
impl LocalUsageSnapshot {
|
||||||
/// Create an empty snapshot with the default format version filled in.
|
/// Create an empty snapshot with the default format version filled in.
|
||||||
pub fn new(meta: LocalUsageSnapshotMeta) -> Self {
|
pub fn new(meta: LocalUsageSnapshotMeta) -> Self {
|
||||||
@@ -113,13 +99,11 @@ impl LocalUsageSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build the snapshot file name `<disk-id>.json`.
|
/// Build the snapshot file name `<disk-id>.json`.
|
||||||
#[allow(dead_code, reason = "unwired local usage-snapshot feature; see module docs (backlog#1823)")]
|
|
||||||
pub fn snapshot_file_name(disk_id: &str) -> String {
|
pub fn snapshot_file_name(disk_id: &str) -> String {
|
||||||
format!("{disk_id}.json")
|
format!("{disk_id}.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the object path relative to `RUSTFS_META_BUCKET`, e.g. `datausage/<disk-id>.json`.
|
/// Build the object path relative to `RUSTFS_META_BUCKET`, e.g. `datausage/<disk-id>.json`.
|
||||||
#[allow(dead_code, reason = "unwired local usage-snapshot feature; see module docs (backlog#1823)")]
|
|
||||||
pub fn snapshot_object_path(disk_id: &str) -> String {
|
pub fn snapshot_object_path(disk_id: &str) -> String {
|
||||||
format!("{}/{}", DATA_USAGE_DIR, snapshot_file_name(disk_id))
|
format!("{}/{}", DATA_USAGE_DIR, snapshot_file_name(disk_id))
|
||||||
}
|
}
|
||||||
@@ -135,13 +119,11 @@ pub fn data_usage_state_dir(root: &Path) -> PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build the absolute path to the snapshot file for the provided disk ID.
|
/// Build the absolute path to the snapshot file for the provided disk ID.
|
||||||
#[allow(dead_code, reason = "unwired local usage-snapshot feature; see module docs (backlog#1823)")]
|
|
||||||
pub fn snapshot_path(root: &Path, disk_id: &str) -> PathBuf {
|
pub fn snapshot_path(root: &Path, disk_id: &str) -> PathBuf {
|
||||||
data_usage_dir(root).join(snapshot_file_name(disk_id))
|
data_usage_dir(root).join(snapshot_file_name(disk_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a snapshot from disk if it exists.
|
/// Read a snapshot from disk if it exists.
|
||||||
#[allow(dead_code, reason = "unwired local usage-snapshot feature; see module docs (backlog#1823)")]
|
|
||||||
pub async fn read_snapshot(root: &Path, disk_id: &str) -> Result<Option<LocalUsageSnapshot>> {
|
pub async fn read_snapshot(root: &Path, disk_id: &str) -> Result<Option<LocalUsageSnapshot>> {
|
||||||
let path = snapshot_path(root, disk_id);
|
let path = snapshot_path(root, disk_id);
|
||||||
match fs::read(&path).await {
|
match fs::read(&path).await {
|
||||||
@@ -156,7 +138,6 @@ pub async fn read_snapshot(root: &Path, disk_id: &str) -> Result<Option<LocalUsa
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Persist a snapshot to disk, creating directories as needed and overwriting any existing file.
|
/// Persist a snapshot to disk, creating directories as needed and overwriting any existing file.
|
||||||
#[allow(dead_code, reason = "unwired local usage-snapshot feature; see module docs (backlog#1823)")]
|
|
||||||
pub async fn write_snapshot(root: &Path, disk_id: &str, snapshot: &LocalUsageSnapshot) -> Result<()> {
|
pub async fn write_snapshot(root: &Path, disk_id: &str, snapshot: &LocalUsageSnapshot) -> Result<()> {
|
||||||
let dir = data_usage_dir(root);
|
let dir = data_usage_dir(root);
|
||||||
fs::create_dir_all(&dir).await.map_err(Error::other)?;
|
fs::create_dir_all(&dir).await.map_err(Error::other)?;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: scanner/data-usage state is partially migrated and still owns staged cache helpers.
|
// #730: scanner/data-usage state is partially migrated and still owns staged cache helpers.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
pub mod local_snapshot;
|
pub mod local_snapshot;
|
||||||
|
|
||||||
@@ -33,8 +34,8 @@ use crate::{
|
|||||||
pub use local_snapshot::{LocalUsageSnapshot, read_snapshot as read_local_snapshot, snapshot_path};
|
pub use local_snapshot::{LocalUsageSnapshot, read_snapshot as read_local_snapshot, snapshot_path};
|
||||||
use rustfs_data_usage::{
|
use rustfs_data_usage::{
|
||||||
BucketTargetUsageInfo, BucketUsageInfo, CompressionTotalInfo, DATA_USAGE_OBJECT_NAME, DATA_USAGE_OBSERVED_OBJECT_NAME,
|
BucketTargetUsageInfo, BucketUsageInfo, CompressionTotalInfo, DATA_USAGE_OBJECT_NAME, DATA_USAGE_OBSERVED_OBJECT_NAME,
|
||||||
DataUsageCache, DataUsageInfo, DiskUsageStatus, LEGACY_DATA_USAGE_OBJECT_NAME, SizeHistogram, VersionsHistogram,
|
DataUsageCache, DataUsageEntry, DataUsageInfo, DiskUsageStatus, LEGACY_DATA_USAGE_OBJECT_NAME, SizeHistogram, SizeSummary,
|
||||||
observed_data_usage_is_newer,
|
VersionsHistogram, observed_data_usage_is_newer,
|
||||||
};
|
};
|
||||||
use rustfs_io_metrics::record_system_path_failure;
|
use rustfs_io_metrics::record_system_path_failure;
|
||||||
use rustfs_utils::path::SLASH_SEPARATOR;
|
use rustfs_utils::path::SLASH_SEPARATOR;
|
||||||
@@ -54,6 +55,7 @@ use tracing::{debug, error, info, instrument};
|
|||||||
// Data usage storage constants
|
// Data usage storage constants
|
||||||
pub const DATA_USAGE_ROOT: &str = SLASH_SEPARATOR;
|
pub const DATA_USAGE_ROOT: &str = SLASH_SEPARATOR;
|
||||||
const DATA_COMPRESSION_TOTAL_NAME: &str = ".compression.json";
|
const DATA_COMPRESSION_TOTAL_NAME: &str = ".compression.json";
|
||||||
|
const DATA_USAGE_BLOOM_NAME: &str = ".bloomcycle.bin";
|
||||||
pub const DATA_USAGE_CACHE_NAME: &str = ".usage-cache.bin";
|
pub const DATA_USAGE_CACHE_NAME: &str = ".usage-cache.bin";
|
||||||
const DATA_USAGE_CACHE_TTL_SECS: u64 = 30;
|
const DATA_USAGE_CACHE_TTL_SECS: u64 = 30;
|
||||||
const LIVE_BUCKET_USAGE_MAX_ENTRIES: u64 = 1024;
|
const LIVE_BUCKET_USAGE_MAX_ENTRIES: u64 = 1024;
|
||||||
@@ -311,6 +313,11 @@ lazy_static::lazy_static! {
|
|||||||
LEGACY_DATA_USAGE_OBJECT_NAME
|
LEGACY_DATA_USAGE_OBJECT_NAME
|
||||||
);
|
);
|
||||||
static ref LEGACY_DATA_USAGE_OBJ_BACKUP_PATH: String = format!("{}.bkp", LEGACY_DATA_USAGE_OBJ_NAME_PATH.as_str());
|
static ref LEGACY_DATA_USAGE_OBJ_BACKUP_PATH: String = format!("{}.bkp", LEGACY_DATA_USAGE_OBJ_NAME_PATH.as_str());
|
||||||
|
pub static ref DATA_USAGE_BLOOM_NAME_PATH: String = format!("{}{}{}",
|
||||||
|
crate::disk::BUCKET_META_PREFIX,
|
||||||
|
SLASH_SEPARATOR,
|
||||||
|
DATA_USAGE_BLOOM_NAME
|
||||||
|
);
|
||||||
pub static ref DATA_COMPRESSION_TOTAL_NAME_PATH: String = format!("{}{}{}",
|
pub static ref DATA_COMPRESSION_TOTAL_NAME_PATH: String = format!("{}{}{}",
|
||||||
crate::disk::BUCKET_META_PREFIX,
|
crate::disk::BUCKET_META_PREFIX,
|
||||||
SLASH_SEPARATOR,
|
SLASH_SEPARATOR,
|
||||||
@@ -851,10 +858,6 @@ async fn resolve_loaded_snapshot_pair_with_source(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "primary/backup snapshot fallback asserted by this file's tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
async fn resolve_loaded_snapshot(
|
async fn resolve_loaded_snapshot(
|
||||||
primary: Result<Vec<u8>, Error>,
|
primary: Result<Vec<u8>, Error>,
|
||||||
backup: impl Future<Output = Result<Vec<u8>, Error>>,
|
backup: impl Future<Output = Result<Vec<u8>, Error>>,
|
||||||
@@ -1184,10 +1187,6 @@ pub async fn invalidate_admin_data_usage_snapshot_cache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Aggregate usage information from local disk snapshots.
|
/// Aggregate usage information from local disk snapshots.
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "reached only through aggregate_local_snapshots, which has no caller (backlog#1823)"
|
|
||||||
)]
|
|
||||||
fn merge_snapshot(aggregated: &mut DataUsageInfo, mut snapshot: LocalUsageSnapshot, latest_update: &mut Option<SystemTime>) {
|
fn merge_snapshot(aggregated: &mut DataUsageInfo, mut snapshot: LocalUsageSnapshot, latest_update: &mut Option<SystemTime>) {
|
||||||
if let Some(update) = snapshot.last_update
|
if let Some(update) = snapshot.last_update
|
||||||
&& latest_update.is_none_or(|current| update > current)
|
&& latest_update.is_none_or(|current| update > current)
|
||||||
@@ -1221,10 +1220,6 @@ fn merge_snapshot(aggregated: &mut DataUsageInfo, mut snapshot: LocalUsageSnapsh
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "entry point of the local usage-snapshot feature, which has had no caller since it landed in #5307 (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub async fn aggregate_local_snapshots(store: Arc<ECStore>) -> Result<(Vec<DiskUsageStatus>, DataUsageInfo), Error> {
|
pub async fn aggregate_local_snapshots(store: Arc<ECStore>) -> Result<(Vec<DiskUsageStatus>, DataUsageInfo), Error> {
|
||||||
let mut aggregated = DataUsageInfo::default();
|
let mut aggregated = DataUsageInfo::default();
|
||||||
let mut latest_update: Option<SystemTime> = None;
|
let mut latest_update: Option<SystemTime> = None;
|
||||||
@@ -1360,7 +1355,7 @@ impl BucketUsageAccumulator {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let object_size = quota_object_size(object)?;
|
let object_size = object.size.max(0) as u64;
|
||||||
self.current_live_versions = self.current_live_versions.saturating_add(1);
|
self.current_live_versions = self.current_live_versions.saturating_add(1);
|
||||||
self.size_histogram.add(object_size);
|
self.size_histogram.add(object_size);
|
||||||
self.total_size = self.total_size.saturating_add(object_size);
|
self.total_size = self.total_size.saturating_add(object_size);
|
||||||
@@ -1390,31 +1385,6 @@ impl BucketUsageAccumulator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn quota_object_size(object: &ObjectInfo) -> Result<u64, Error> {
|
|
||||||
let logical_size = u64::try_from(object.get_actual_size().map_err(Error::other)?).map_err(|_| Error::PartMissingOrCorrupt)?;
|
|
||||||
let persisted_part_size = if object.parts.is_empty() {
|
|
||||||
u64::try_from(object.size).map_err(|_| Error::PartMissingOrCorrupt)?
|
|
||||||
} else {
|
|
||||||
object.parts.iter().try_fold(0_u64, |total, part| {
|
|
||||||
// Compressed streaming objects persist -1 when the transformed
|
|
||||||
// part size is unknown. The physical part size remains a valid
|
|
||||||
// quota floor; reject only non-negative values that overflow.
|
|
||||||
let actual_size = if part.actual_size < 0 {
|
|
||||||
if object.is_compressed() {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
return Err(Error::PartMissingOrCorrupt);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
u64::try_from(part.actual_size).map_err(|_| Error::PartMissingOrCorrupt)?
|
|
||||||
};
|
|
||||||
let part_size = actual_size.max(u64::try_from(part.size).map_err(|_| Error::PartMissingOrCorrupt)?);
|
|
||||||
total.checked_add(part_size).ok_or(Error::PartMissingOrCorrupt)
|
|
||||||
})?
|
|
||||||
};
|
|
||||||
Ok(logical_size.max(persisted_part_size))
|
|
||||||
}
|
|
||||||
|
|
||||||
type UsageVersionPage = StorageListObjectVersionsInfo<ObjectInfo>;
|
type UsageVersionPage = StorageListObjectVersionsInfo<ObjectInfo>;
|
||||||
|
|
||||||
pub async fn compute_bucket_usage(store: Arc<ECStore>, bucket_name: &str) -> Result<BucketUsageInfo, Error> {
|
pub async fn compute_bucket_usage(store: Arc<ECStore>, bucket_name: &str) -> Result<BucketUsageInfo, Error> {
|
||||||
@@ -1772,6 +1742,11 @@ pub async fn record_bucket_object_write_unknown_previous_memory(bucket: &str, ne
|
|||||||
entry.pending_scanner_position = None;
|
entry.pending_scanner_position = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fast in-memory increment for immediate quota consistency.
|
||||||
|
pub async fn increment_bucket_usage_memory(bucket: &str, size_increment: u64) {
|
||||||
|
record_bucket_object_write_memory(bucket, None, size_increment).await;
|
||||||
|
}
|
||||||
|
|
||||||
/// Fast in-memory update for successful object deletes.
|
/// Fast in-memory update for successful object deletes.
|
||||||
pub async fn record_bucket_object_delete_memory(bucket: &str, deleted_size: u64, removed_current_object: bool) {
|
pub async fn record_bucket_object_delete_memory(bucket: &str, deleted_size: u64, removed_current_object: bool) {
|
||||||
ensure_bucket_usage_cached(bucket).await;
|
ensure_bucket_usage_cached(bucket).await;
|
||||||
@@ -1814,6 +1789,11 @@ pub async fn record_bucket_delete_marker_memory(bucket: &str) {
|
|||||||
entry.pending_scanner_position = None;
|
entry.pending_scanner_position = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fast in-memory decrement for immediate quota consistency
|
||||||
|
pub async fn decrement_bucket_usage_memory(bucket: &str, size_decrement: u64) {
|
||||||
|
record_bucket_object_delete_memory(bucket, size_decrement, size_decrement > 0).await;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get bucket usage from the authoritative cache for this topology.
|
/// Get bucket usage from the authoritative cache for this topology.
|
||||||
async fn get_persisted_bucket_usage(bucket: &str) -> Option<u64> {
|
async fn get_persisted_bucket_usage(bucket: &str) -> Option<u64> {
|
||||||
let store = runtime_sources::object_store_handle()?;
|
let store = runtime_sources::object_store_handle()?;
|
||||||
@@ -2008,6 +1988,91 @@ pub async fn apply_bucket_usage_memory_overlay(data_usage_info: &mut DataUsageIn
|
|||||||
apply_bucket_usage_memory_overlay_if_authoritative(data_usage_info, authoritative).await;
|
apply_bucket_usage_memory_overlay_if_authoritative(data_usage_info, authoritative).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sync memory cache with backend data (called by scanner)
|
||||||
|
pub async fn sync_memory_cache_with_backend() -> Result<(), Error> {
|
||||||
|
if let Some(store) = runtime_sources::object_store_handle() {
|
||||||
|
match load_data_usage_from_backend(store.clone()).await {
|
||||||
|
Ok(data_usage_info) => {
|
||||||
|
replace_bucket_usage_memory_from_info(&data_usage_info).await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to sync memory cache with backend: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a data usage cache entry from size summary
|
||||||
|
pub fn create_cache_entry_from_summary(summary: &SizeSummary) -> DataUsageEntry {
|
||||||
|
let mut entry = DataUsageEntry::default();
|
||||||
|
entry.add_sizes(summary);
|
||||||
|
entry
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert data usage cache to DataUsageInfo
|
||||||
|
pub fn cache_to_data_usage_info(
|
||||||
|
cache: &DataUsageCache,
|
||||||
|
path: &str,
|
||||||
|
buckets: &[crate::storage_api_contracts::bucket::BucketInfo],
|
||||||
|
) -> DataUsageInfo {
|
||||||
|
let e = match cache.find(path) {
|
||||||
|
Some(e) => e,
|
||||||
|
None => return DataUsageInfo::default(),
|
||||||
|
};
|
||||||
|
let flat = cache.flatten(&e);
|
||||||
|
|
||||||
|
let mut buckets_usage = HashMap::new();
|
||||||
|
for bucket in buckets.iter() {
|
||||||
|
let e = match cache.find(&bucket.name) {
|
||||||
|
Some(e) => e,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let flat = cache.flatten(&e);
|
||||||
|
let mut bui = BucketUsageInfo {
|
||||||
|
size: flat.size as u64,
|
||||||
|
versions_count: flat.versions as u64,
|
||||||
|
objects_count: flat.objects as u64,
|
||||||
|
delete_markers_count: flat.delete_markers as u64,
|
||||||
|
object_size_histogram: flat.obj_sizes.to_map(),
|
||||||
|
object_versions_histogram: flat.obj_versions.to_map(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(rs) = &flat.replication_stats {
|
||||||
|
bui.replica_size = rs.replica_size;
|
||||||
|
bui.replica_count = rs.replica_count;
|
||||||
|
|
||||||
|
for (arn, stat) in rs.targets.iter() {
|
||||||
|
bui.replication_info.insert(
|
||||||
|
arn.clone(),
|
||||||
|
BucketTargetUsageInfo {
|
||||||
|
replication_pending_size: stat.pending_size,
|
||||||
|
replicated_size: stat.replicated_size,
|
||||||
|
replication_failed_size: stat.failed_size,
|
||||||
|
replication_pending_count: stat.pending_count,
|
||||||
|
replication_failed_count: stat.failed_count,
|
||||||
|
replicated_count: stat.replicated_count,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buckets_usage.insert(bucket.name.clone(), bui);
|
||||||
|
}
|
||||||
|
|
||||||
|
DataUsageInfo {
|
||||||
|
last_update: cache.info.last_update,
|
||||||
|
objects_total_count: flat.objects as u64,
|
||||||
|
versions_total_count: flat.versions as u64,
|
||||||
|
delete_markers_total_count: flat.delete_markers as u64,
|
||||||
|
objects_total_size: flat.size as u64,
|
||||||
|
buckets_count: e.children.len() as u64,
|
||||||
|
buckets_usage,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper functions for DataUsageCache operations
|
// Helper functions for DataUsageCache operations
|
||||||
pub async fn load_data_usage_cache(store: &crate::set_disk::SetDisks, name: &str) -> crate::error::Result<DataUsageCache> {
|
pub async fn load_data_usage_cache(store: &crate::set_disk::SetDisks, name: &str) -> crate::error::Result<DataUsageCache> {
|
||||||
use crate::disk::{BUCKET_META_PREFIX, RUSTFS_META_BUCKET};
|
use crate::disk::{BUCKET_META_PREFIX, RUSTFS_META_BUCKET};
|
||||||
@@ -3059,102 +3124,6 @@ mod tests {
|
|||||||
assert_eq!(usage.object_versions_histogram.get("BETWEEN_1000_AND_10000"), Some(&1));
|
assert_eq!(usage.object_versions_histogram.get("BETWEEN_1000_AND_10000"), Some(&1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bucket_usage_uses_the_larger_of_logical_and_physical_size() {
|
|
||||||
let mut metadata = HashMap::new();
|
|
||||||
rustfs_utils::http::insert_str(
|
|
||||||
&mut metadata,
|
|
||||||
rustfs_utils::http::SUFFIX_COMPRESSION,
|
|
||||||
"klauspost/compress/s2".to_string(),
|
|
||||||
);
|
|
||||||
rustfs_utils::http::insert_str(&mut metadata, rustfs_utils::http::SUFFIX_ACTUAL_SIZE, "4096".to_string());
|
|
||||||
let object = ObjectInfo {
|
|
||||||
name: "compressed".to_string(),
|
|
||||||
size: 128,
|
|
||||||
user_defined: Arc::new(metadata),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let mut usage = BucketUsageAccumulator::default();
|
|
||||||
usage
|
|
||||||
.record("bucket", &object)
|
|
||||||
.expect("valid compressed metadata should be counted");
|
|
||||||
assert_eq!(usage.finish().size, 4096);
|
|
||||||
|
|
||||||
let mut framed_metadata = HashMap::new();
|
|
||||||
rustfs_utils::http::insert_str(
|
|
||||||
&mut framed_metadata,
|
|
||||||
rustfs_utils::http::SUFFIX_COMPRESSION,
|
|
||||||
"klauspost/compress/s2".to_string(),
|
|
||||||
);
|
|
||||||
rustfs_utils::http::insert_str(&mut framed_metadata, rustfs_utils::http::SUFFIX_ACTUAL_SIZE, "1".to_string());
|
|
||||||
let framed = ObjectInfo {
|
|
||||||
name: "framed".to_string(),
|
|
||||||
size: 17,
|
|
||||||
user_defined: Arc::new(framed_metadata),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
assert_eq!(quota_object_size(&framed).expect("physical framing must remain quota-accounted"), 17);
|
|
||||||
|
|
||||||
let legacy_compressed_part = ObjectInfo {
|
|
||||||
name: "legacy-compressed-part".to_string(),
|
|
||||||
size: 1,
|
|
||||||
user_defined: Arc::new((*framed.user_defined).clone()),
|
|
||||||
parts: Arc::new(vec![rustfs_filemeta::ObjectPartInfo {
|
|
||||||
size: 1,
|
|
||||||
actual_size: -1,
|
|
||||||
..Default::default()
|
|
||||||
}]),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
quota_object_size(&legacy_compressed_part).expect("unknown compressed part size is a valid sentinel"),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
let uncompressed_negative_part = ObjectInfo {
|
|
||||||
name: "uncompressed-negative-part".to_string(),
|
|
||||||
size: 1,
|
|
||||||
parts: Arc::new(vec![rustfs_filemeta::ObjectPartInfo {
|
|
||||||
size: 1,
|
|
||||||
actual_size: -1,
|
|
||||||
..Default::default()
|
|
||||||
}]),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
assert!(matches!(quota_object_size(&uncompressed_negative_part), Err(Error::PartMissingOrCorrupt)));
|
|
||||||
|
|
||||||
let mut corrupt_metadata = (*object.user_defined).clone();
|
|
||||||
rustfs_utils::http::insert_str(&mut corrupt_metadata, rustfs_utils::http::SUFFIX_ACTUAL_SIZE, "-1".to_string());
|
|
||||||
let corrupt = ObjectInfo {
|
|
||||||
user_defined: Arc::new(corrupt_metadata),
|
|
||||||
..object
|
|
||||||
};
|
|
||||||
assert!(matches!(quota_object_size(&corrupt), Err(Error::PartMissingOrCorrupt)));
|
|
||||||
|
|
||||||
let mut poisoned_metadata = HashMap::new();
|
|
||||||
rustfs_utils::http::insert_str(
|
|
||||||
&mut poisoned_metadata,
|
|
||||||
rustfs_utils::http::SUFFIX_COMPRESSION,
|
|
||||||
"klauspost/compress/s2".to_string(),
|
|
||||||
);
|
|
||||||
rustfs_utils::http::insert_str(&mut poisoned_metadata, rustfs_utils::http::SUFFIX_ACTUAL_SIZE, "1".to_string());
|
|
||||||
let poisoned = ObjectInfo {
|
|
||||||
name: "legacy-swift-metadata".to_string(),
|
|
||||||
size: 4096,
|
|
||||||
user_defined: Arc::new(poisoned_metadata),
|
|
||||||
parts: Arc::new(vec![rustfs_filemeta::ObjectPartInfo {
|
|
||||||
size: 4096,
|
|
||||||
actual_size: 4096,
|
|
||||||
..Default::default()
|
|
||||||
}]),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
quota_object_size(&poisoned).expect("persisted part accounting must bound legacy user metadata"),
|
|
||||||
4096
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn live_bucket_usage_refreshes_are_coalesced_only_while_in_flight() {
|
async fn live_bucket_usage_refreshes_are_coalesced_only_while_in_flight() {
|
||||||
|
|||||||
@@ -653,7 +653,6 @@ fn reconcile_servers_with_endpoint_topology(
|
|||||||
(added, report)
|
(added, report)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "exercised by this file's topology tests (backlog#1823)")]
|
|
||||||
fn server_topology_completeness_report(
|
fn server_topology_completeness_report(
|
||||||
servers: &[ServerProperties],
|
servers: &[ServerProperties],
|
||||||
endpoints: &EndpointServerPools,
|
endpoints: &EndpointServerPools,
|
||||||
|
|||||||
@@ -46,49 +46,21 @@ pub(crate) const GET_CODEC_STREAMING_OBJECT_CLASS_MULTIPART: &str = "multipart";
|
|||||||
pub(crate) const GET_STAGE_DECODE: &str = "decode";
|
pub(crate) const GET_STAGE_DECODE: &str = "decode";
|
||||||
pub(crate) const GET_STAGE_EMIT: &str = "emit";
|
pub(crate) const GET_STAGE_EMIT: &str = "emit";
|
||||||
pub(crate) const GET_STAGE_FILL: &str = "fill";
|
pub(crate) const GET_STAGE_FILL: &str = "fill";
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "GET stage vocabulary; value pinned by this file's tests, no writer yet (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) const GET_STAGE_FIRST_BYTE: &str = "first_byte";
|
pub(crate) const GET_STAGE_FIRST_BYTE: &str = "first_byte";
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "GET stage vocabulary; value pinned by this file's tests, no writer yet (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) const GET_STAGE_FIRST_METADATA_RESPONSE: &str = "first_metadata_response";
|
pub(crate) const GET_STAGE_FIRST_METADATA_RESPONSE: &str = "first_metadata_response";
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "GET stage vocabulary; value pinned by this file's tests, no writer yet (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) const GET_STAGE_FIRST_VALID_METADATA_RESPONSE: &str = "first_valid_metadata_response";
|
pub(crate) const GET_STAGE_FIRST_VALID_METADATA_RESPONSE: &str = "first_valid_metadata_response";
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "GET stage vocabulary; value pinned by this file's tests, no writer yet (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) const GET_STAGE_FIRST_SHARD_READ: &str = "first_shard_read";
|
pub(crate) const GET_STAGE_FIRST_SHARD_READ: &str = "first_shard_read";
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "GET stage vocabulary; value pinned by this file's tests, no writer yet (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) const GET_STAGE_FULL_BODY: &str = "full_body";
|
pub(crate) const GET_STAGE_FULL_BODY: &str = "full_body";
|
||||||
pub(crate) const GET_STAGE_INLINE_PREPARE: &str = "inline_prepare";
|
pub(crate) const GET_STAGE_INLINE_PREPARE: &str = "inline_prepare";
|
||||||
pub(crate) const GET_STAGE_LOCK_ACQUIRE: &str = "lock_acquire";
|
pub(crate) const GET_STAGE_LOCK_ACQUIRE: &str = "lock_acquire";
|
||||||
pub(crate) const GET_STAGE_METADATA: &str = "metadata";
|
pub(crate) const GET_STAGE_METADATA: &str = "metadata";
|
||||||
pub(crate) const GET_STAGE_METADATA_CACHE_LOOKUP: &str = "metadata_cache_lookup";
|
pub(crate) const GET_STAGE_METADATA_CACHE_LOOKUP: &str = "metadata_cache_lookup";
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "GET stage vocabulary; value pinned by this file's tests, no writer yet (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) const GET_STAGE_METADATA_FANOUT: &str = "metadata_fanout";
|
pub(crate) const GET_STAGE_METADATA_FANOUT: &str = "metadata_fanout";
|
||||||
pub(crate) const GET_STAGE_METADATA_RESOLVE: &str = "metadata_resolve";
|
pub(crate) const GET_STAGE_METADATA_RESOLVE: &str = "metadata_resolve";
|
||||||
pub(crate) const GET_STAGE_OBJECT_INFO: &str = "object_info";
|
pub(crate) const GET_STAGE_OBJECT_INFO: &str = "object_info";
|
||||||
pub(crate) const GET_STAGE_OUTPUT_LOCK_WAIT: &str = "output_lock_wait";
|
pub(crate) const GET_STAGE_OUTPUT_LOCK_WAIT: &str = "output_lock_wait";
|
||||||
pub(crate) const GET_STAGE_OUTPUT_POLL: &str = "output_poll";
|
pub(crate) const GET_STAGE_OUTPUT_POLL: &str = "output_poll";
|
||||||
pub(crate) const GET_STAGE_PATH_DECISION: &str = "path_decision";
|
pub(crate) const GET_STAGE_PATH_DECISION: &str = "path_decision";
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "GET stage vocabulary; value pinned by this file's tests, no writer yet (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) const GET_STAGE_QUORUM_REACHED: &str = "quorum_reached";
|
pub(crate) const GET_STAGE_QUORUM_REACHED: &str = "quorum_reached";
|
||||||
pub(crate) const GET_STAGE_RANGE: &str = "range";
|
pub(crate) const GET_STAGE_RANGE: &str = "range";
|
||||||
pub(crate) const GET_STAGE_READER_SETUP: &str = "reader_setup";
|
pub(crate) const GET_STAGE_READER_SETUP: &str = "reader_setup";
|
||||||
@@ -112,28 +84,12 @@ pub(crate) const GET_STAGE_READER_STREAM_FIRST_READ: &str = "reader_stream_first
|
|||||||
pub(crate) const GET_STAGE_READER_TASK_BITROT_READER_INIT: &str = "reader_task_bitrot_reader_init";
|
pub(crate) const GET_STAGE_READER_TASK_BITROT_READER_INIT: &str = "reader_task_bitrot_reader_init";
|
||||||
pub(crate) const GET_STAGE_READER_TASK_FILE_OPEN: &str = "reader_task_file_open";
|
pub(crate) const GET_STAGE_READER_TASK_FILE_OPEN: &str = "reader_task_file_open";
|
||||||
pub(crate) const GET_STAGE_READER_TASK_READER_CONSTRUCTION: &str = "reader_task_reader_construction";
|
pub(crate) const GET_STAGE_READER_TASK_READER_CONSTRUCTION: &str = "reader_task_reader_construction";
|
||||||
pub(crate) const GET_STAGE_READ_VERSION_DECODE: &str = "read_version_decode";
|
|
||||||
pub(crate) const GET_STAGE_READ_VERSION_PATH_CHECK: &str = "read_version_path_check";
|
|
||||||
pub(crate) const GET_STAGE_READ_VERSION_PATH_RESOLVE: &str = "read_version_path_resolve";
|
|
||||||
pub(crate) const GET_STAGE_READ_VERSION_XLMETA_READ: &str = "read_version_xlmeta_read";
|
|
||||||
pub(crate) const GET_STAGE_RECONSTRUCT: &str = "reconstruct";
|
pub(crate) const GET_STAGE_RECONSTRUCT: &str = "reconstruct";
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "GET stage vocabulary; value pinned by this file's tests, no writer yet (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) const GET_STAGE_RESPONSE_HANDOFF: &str = "response_handoff";
|
pub(crate) const GET_STAGE_RESPONSE_HANDOFF: &str = "response_handoff";
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "GET stage vocabulary; value pinned by this file's tests, no writer yet (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) const GET_STAGE_SLOWEST_METADATA_RESPONSE: &str = "slowest_metadata_response";
|
pub(crate) const GET_STAGE_SLOWEST_METADATA_RESPONSE: &str = "slowest_metadata_response";
|
||||||
pub(crate) const GET_STAGE_STRIPE_READ: &str = "stripe_read";
|
pub(crate) const GET_STAGE_STRIPE_READ: &str = "stripe_read";
|
||||||
pub(crate) const GET_STAGE_STRIPE_READ_FIRST_SHARD: &str = "stripe_read_first_shard";
|
pub(crate) const GET_STAGE_STRIPE_READ_FIRST_SHARD: &str = "stripe_read_first_shard";
|
||||||
pub(crate) const GET_STAGE_STRIPE_READ_QUORUM: &str = "stripe_read_quorum";
|
pub(crate) const GET_STAGE_STRIPE_READ_QUORUM: &str = "stripe_read_quorum";
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "GET stage vocabulary; value pinned by this file's tests, no writer yet (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) const GET_STAGE_BITROT_VERIFY: &str = "bitrot_verify";
|
pub(crate) const GET_STAGE_BITROT_VERIFY: &str = "bitrot_verify";
|
||||||
|
|
||||||
pub(crate) const GET_READER_BUFFER_OUTPUT: &str = "output";
|
pub(crate) const GET_READER_BUFFER_OUTPUT: &str = "output";
|
||||||
@@ -190,17 +146,6 @@ pub(crate) const GET_METADATA_CACHE_REASON_VERSION_SUSPENDED: &str = "version_su
|
|||||||
pub(crate) const GET_METADATA_CACHE_REASON_VERSIONED: &str = "versioned";
|
pub(crate) const GET_METADATA_CACHE_REASON_VERSIONED: &str = "versioned";
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_CONFLICTING_METADATA: &str = "conflicting_metadata";
|
pub(crate) const GET_METADATA_EARLY_STOP_REASON_CONFLICTING_METADATA: &str = "conflicting_metadata";
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_DELETE_MARKER: &str = "delete_marker";
|
pub(crate) const GET_METADATA_EARLY_STOP_REASON_DELETE_MARKER: &str = "delete_marker";
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_BODY_VERIFY: &str = "data_read_inline_body_verify";
|
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_DELETED: &str = "data_read_inline_deleted";
|
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_GEOMETRY: &str = "data_read_inline_geometry";
|
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_IDENTITY_MISMATCH: &str = "data_read_inline_identity_mismatch";
|
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_MISSING_PAYLOAD: &str = "data_read_inline_missing_payload";
|
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_MISSING_SHARD: &str = "data_read_inline_missing_shard";
|
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_NOT_INLINE: &str = "data_read_inline_not_inline";
|
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_PART_SHAPE: &str = "data_read_inline_part_shape";
|
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_REMOTE: &str = "data_read_inline_remote";
|
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_SIZE: &str = "data_read_inline_size";
|
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_TRANSFORMED: &str = "data_read_inline_transformed";
|
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_ERROR: &str = "error";
|
pub(crate) const GET_METADATA_EARLY_STOP_REASON_ERROR: &str = "error";
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_INSUFFICIENT_QUORUM: &str = "insufficient_quorum";
|
pub(crate) const GET_METADATA_EARLY_STOP_REASON_INSUFFICIENT_QUORUM: &str = "insufficient_quorum";
|
||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_NOT_FOUND: &str = "not_found";
|
pub(crate) const GET_METADATA_EARLY_STOP_REASON_NOT_FOUND: &str = "not_found";
|
||||||
@@ -210,20 +155,8 @@ pub(crate) const GET_METADATA_EARLY_STOP_REASON_VERSION_NOT_FOUND: &str = "versi
|
|||||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_VERSION_MATCH_QUORUM: &str = "version_match_quorum";
|
pub(crate) const GET_METADATA_EARLY_STOP_REASON_VERSION_MATCH_QUORUM: &str = "version_match_quorum";
|
||||||
|
|
||||||
/// Early-stop active state labels
|
/// Early-stop active state labels
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "GET stage vocabulary; value pinned by this file's tests, no writer yet (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) const EARLY_STOP_ACTIVE_HIT: &str = "hit";
|
pub(crate) const EARLY_STOP_ACTIVE_HIT: &str = "hit";
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "GET stage vocabulary; value pinned by this file's tests, no writer yet (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) const EARLY_STOP_ACTIVE_MISS: &str = "miss";
|
pub(crate) const EARLY_STOP_ACTIVE_MISS: &str = "miss";
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "GET stage vocabulary; value pinned by this file's tests, no writer yet (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) const EARLY_STOP_ACTIVE_DISABLED: &str = "disabled";
|
pub(crate) const EARLY_STOP_ACTIVE_DISABLED: &str = "disabled";
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
@@ -509,10 +442,6 @@ mod tests {
|
|||||||
assert_eq!(GET_STAGE_QUORUM_REACHED, "quorum_reached");
|
assert_eq!(GET_STAGE_QUORUM_REACHED, "quorum_reached");
|
||||||
assert_eq!(GET_STAGE_RANGE, "range");
|
assert_eq!(GET_STAGE_RANGE, "range");
|
||||||
assert_eq!(GET_STAGE_READER_SETUP, "reader_setup");
|
assert_eq!(GET_STAGE_READER_SETUP, "reader_setup");
|
||||||
assert_eq!(GET_STAGE_READ_VERSION_DECODE, "read_version_decode");
|
|
||||||
assert_eq!(GET_STAGE_READ_VERSION_PATH_CHECK, "read_version_path_check");
|
|
||||||
assert_eq!(GET_STAGE_READ_VERSION_PATH_RESOLVE, "read_version_path_resolve");
|
|
||||||
assert_eq!(GET_STAGE_READ_VERSION_XLMETA_READ, "read_version_xlmeta_read");
|
|
||||||
assert_eq!(GET_STAGE_RECONSTRUCT, "reconstruct");
|
assert_eq!(GET_STAGE_RECONSTRUCT, "reconstruct");
|
||||||
assert_eq!(GET_STAGE_RESPONSE_HANDOFF, "response_handoff");
|
assert_eq!(GET_STAGE_RESPONSE_HANDOFF, "response_handoff");
|
||||||
assert_eq!(GET_STAGE_SLOWEST_METADATA_RESPONSE, "slowest_metadata_response");
|
assert_eq!(GET_STAGE_SLOWEST_METADATA_RESPONSE, "slowest_metadata_response");
|
||||||
@@ -562,32 +491,6 @@ mod tests {
|
|||||||
assert_eq!(GET_METADATA_CACHE_REASON_VERSIONED, "versioned");
|
assert_eq!(GET_METADATA_CACHE_REASON_VERSIONED, "versioned");
|
||||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_CONFLICTING_METADATA, "conflicting_metadata");
|
assert_eq!(GET_METADATA_EARLY_STOP_REASON_CONFLICTING_METADATA, "conflicting_metadata");
|
||||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_DELETE_MARKER, "delete_marker");
|
assert_eq!(GET_METADATA_EARLY_STOP_REASON_DELETE_MARKER, "delete_marker");
|
||||||
assert_eq!(
|
|
||||||
GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_BODY_VERIFY,
|
|
||||||
"data_read_inline_body_verify"
|
|
||||||
);
|
|
||||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_DELETED, "data_read_inline_deleted");
|
|
||||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_GEOMETRY, "data_read_inline_geometry");
|
|
||||||
assert_eq!(
|
|
||||||
GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_IDENTITY_MISMATCH,
|
|
||||||
"data_read_inline_identity_mismatch"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_MISSING_PAYLOAD,
|
|
||||||
"data_read_inline_missing_payload"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_MISSING_SHARD,
|
|
||||||
"data_read_inline_missing_shard"
|
|
||||||
);
|
|
||||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_NOT_INLINE, "data_read_inline_not_inline");
|
|
||||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_PART_SHAPE, "data_read_inline_part_shape");
|
|
||||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_REMOTE, "data_read_inline_remote");
|
|
||||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_SIZE, "data_read_inline_size");
|
|
||||||
assert_eq!(
|
|
||||||
GET_METADATA_EARLY_STOP_REASON_DATA_READ_INLINE_TRANSFORMED,
|
|
||||||
"data_read_inline_transformed"
|
|
||||||
);
|
|
||||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_ERROR, "error");
|
assert_eq!(GET_METADATA_EARLY_STOP_REASON_ERROR, "error");
|
||||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_INSUFFICIENT_QUORUM, "insufficient_quorum");
|
assert_eq!(GET_METADATA_EARLY_STOP_REASON_INSUFFICIENT_QUORUM, "insufficient_quorum");
|
||||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_NOT_FOUND, "not_found");
|
assert_eq!(GET_METADATA_EARLY_STOP_REASON_NOT_FOUND, "not_found");
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: diagnostics constants are staged for request-path telemetry migration.
|
// #730: diagnostics constants are staged for request-path telemetry migration.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
pub(crate) mod admin_server_info;
|
pub(crate) mod admin_server_info;
|
||||||
pub(crate) mod get;
|
pub(crate) mod get;
|
||||||
|
pub(crate) mod pool;
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! BytesPool metric label constants.
|
||||||
|
//!
|
||||||
|
//! These constants are used when recording pool acquisition and return
|
||||||
|
//! metrics to avoid string allocations and ensure label consistency.
|
||||||
|
|
||||||
|
/// BytesPool tier labels
|
||||||
|
pub const POOL_TIER_SMALL: &str = "small";
|
||||||
|
pub const POOL_TIER_MEDIUM: &str = "medium";
|
||||||
|
pub const POOL_TIER_LARGE: &str = "large";
|
||||||
|
pub const POOL_TIER_XLARGE: &str = "xlarge";
|
||||||
|
|
||||||
|
/// BytesPool outcome labels
|
||||||
|
pub const POOL_OUTCOME_HIT: &str = "hit";
|
||||||
|
pub const POOL_OUTCOME_MISS: &str = "miss";
|
||||||
|
pub const POOL_OUTCOME_RECYCLED: &str = "recycled";
|
||||||
|
pub const POOL_OUTCOME_DROPPED: &str = "dropped";
|
||||||
@@ -241,40 +241,6 @@ pub fn get_drive_list_dir_timeout() -> Duration {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait DiskStoreRenameDataExt {
|
|
||||||
async fn rename_data_borrowed(
|
|
||||||
&self,
|
|
||||||
src_volume: &str,
|
|
||||||
src_path: &str,
|
|
||||||
fi: &FileInfo,
|
|
||||||
dst_volume: &str,
|
|
||||||
dst_path: &str,
|
|
||||||
) -> Result<RenameDataResp>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DiskStoreRenameDataExt for LocalDiskWrapper {
|
|
||||||
async fn rename_data_borrowed(
|
|
||||||
&self,
|
|
||||||
src_volume: &str,
|
|
||||||
src_path: &str,
|
|
||||||
fi: &FileInfo,
|
|
||||||
dst_volume: &str,
|
|
||||||
dst_path: &str,
|
|
||||||
) -> Result<RenameDataResp> {
|
|
||||||
self.track_disk_health_mutation(
|
|
||||||
"rename_data",
|
|
||||||
DiskMetricMutation::Write,
|
|
||||||
|| async {
|
|
||||||
self.disk
|
|
||||||
.rename_data_borrowed(src_volume, src_path, fi, dst_volume, dst_path)
|
|
||||||
.await
|
|
||||||
},
|
|
||||||
get_max_timeout_duration(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_drive_walkdir_timeout() -> Duration {
|
pub fn get_drive_walkdir_timeout() -> Duration {
|
||||||
get_drive_timeout_duration(
|
get_drive_timeout_duration(
|
||||||
rustfs_config::ENV_DRIVE_WALKDIR_TIMEOUT_SECS,
|
rustfs_config::ENV_DRIVE_WALKDIR_TIMEOUT_SECS,
|
||||||
@@ -2019,8 +1985,13 @@ impl DiskAPI for LocalDiskWrapper {
|
|||||||
dst_volume: &str,
|
dst_volume: &str,
|
||||||
dst_path: &str,
|
dst_path: &str,
|
||||||
) -> Result<RenameDataResp> {
|
) -> Result<RenameDataResp> {
|
||||||
self.rename_data_borrowed(src_volume, src_path, &fi, dst_volume, dst_path)
|
self.track_disk_health_mutation(
|
||||||
.await
|
"rename_data",
|
||||||
|
DiskMetricMutation::Write,
|
||||||
|
|| async { self.disk.rename_data(src_volume, src_path, fi, dst_volume, dst_path).await },
|
||||||
|
get_max_timeout_duration(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_dir(&self, origvolume: &str, volume: &str, dir_path: &str, count: i32) -> Result<Vec<String>> {
|
async fn list_dir(&self, origvolume: &str, volume: &str, dir_path: &str, count: i32) -> Result<Vec<String>> {
|
||||||
|
|||||||
@@ -15,11 +15,6 @@
|
|||||||
use crate::config::storageclass::DEFAULT_INLINE_BLOCK;
|
use crate::config::storageclass::DEFAULT_INLINE_BLOCK;
|
||||||
use crate::crash_inject::{self, CrashPoint};
|
use crate::crash_inject::{self, CrashPoint};
|
||||||
use crate::data_usage::local_snapshot::ensure_data_usage_layout;
|
use crate::data_usage::local_snapshot::ensure_data_usage_layout;
|
||||||
use crate::diagnostics::get::{
|
|
||||||
GET_OBJECT_PATH_INTERNAL_META, GET_OBJECT_PATH_LEGACY_DUPLEX, GET_STAGE_READ_VERSION_DECODE,
|
|
||||||
GET_STAGE_READ_VERSION_PATH_CHECK, GET_STAGE_READ_VERSION_PATH_RESOLVE, GET_STAGE_READ_VERSION_XLMETA_READ,
|
|
||||||
get_stage_timer_if_enabled, record_get_stage_duration_if_enabled,
|
|
||||||
};
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use crate::disk::HEALING_MARKER_PATH;
|
use crate::disk::HEALING_MARKER_PATH;
|
||||||
use crate::disk::disk_store::{get_drive_walkdir_stall_timeout, get_object_disk_read_timeout};
|
use crate::disk::disk_store::{get_drive_walkdir_stall_timeout, get_object_disk_read_timeout};
|
||||||
@@ -27,18 +22,17 @@ use crate::disk::{
|
|||||||
BUCKET_META_PREFIX, CHECK_PART_FILE_CORRUPT, CHECK_PART_FILE_NOT_FOUND, CHECK_PART_SUCCESS, CHECK_PART_UNKNOWN,
|
BUCKET_META_PREFIX, CHECK_PART_FILE_CORRUPT, CHECK_PART_FILE_NOT_FOUND, CHECK_PART_SUCCESS, CHECK_PART_UNKNOWN,
|
||||||
CHECK_PART_VOLUME_NOT_FOUND, CheckPartsResp, ConditionalFileUpdate, DataDirDeleteStatus, DeleteOptions, DiskAPI, DiskInfo,
|
CHECK_PART_VOLUME_NOT_FOUND, CheckPartsResp, ConditionalFileUpdate, DataDirDeleteStatus, DeleteOptions, DiskAPI, DiskInfo,
|
||||||
DiskInfoOptions, DiskLocation, DiskMetrics, FileInfoVersions, FileReader, FileWriter, MmapCopyStageMetrics, OldCurrentSize,
|
DiskInfoOptions, DiskLocation, DiskMetrics, FileInfoVersions, FileReader, FileWriter, MmapCopyStageMetrics, OldCurrentSize,
|
||||||
PART_TRANSACTION_NEW_META, PART_TRANSACTION_OLD_META, PART_TRANSACTION_ROLLBACK, PartTransactionAction,
|
PART_TRANSACTION_NEW_META, PART_TRANSACTION_OLD_META, PART_TRANSACTION_ROLLBACK, PartTransactionAction, RUSTFS_META_BUCKET,
|
||||||
QUOTA_MUTATION_FENCE_METADATA_SUFFIX, RUSTFS_META_BUCKET, RUSTFS_META_TMP_BUCKET, RUSTFS_META_TMP_DELETED_BUCKET,
|
RUSTFS_META_TMP_BUCKET, RUSTFS_META_TMP_DELETED_BUCKET, ReadMultipleReq, ReadMultipleResp, ReadOptions, RenameDataResp,
|
||||||
ReadMultipleReq, ReadMultipleResp, ReadOptions, RenameDataResp, STORAGE_FORMAT_FILE, STORAGE_FORMAT_FILE_BACKUP,
|
STORAGE_FORMAT_FILE, STORAGE_FORMAT_FILE_BACKUP, SnapshotLeaseToken, UpdateMetadataOpts, VolumeInfo, WalkDirOptions,
|
||||||
SnapshotLeaseToken, UpdateMetadataOpts, VolumeInfo, WalkDirOptions, conv_part_err_to_int,
|
conv_part_err_to_int,
|
||||||
endpoint::Endpoint,
|
endpoint::Endpoint,
|
||||||
error::{DiskError, Error, FileAccessDeniedWithContext, Result},
|
error::{DiskError, Error, FileAccessDeniedWithContext, Result},
|
||||||
error_conv::{to_access_error, to_file_error, to_unformatted_disk_error, to_volume_error},
|
error_conv::{to_access_error, to_file_error, to_unformatted_disk_error, to_volume_error},
|
||||||
format::FormatV3,
|
format::FormatV3,
|
||||||
fs::{O_APPEND, O_CREATE, O_RDONLY, O_TRUNC, O_WRONLY, access, lstat, lstat_std, remove, remove_all_std, remove_std, rename},
|
fs::{O_APPEND, O_CREATE, O_RDONLY, O_TRUNC, O_WRONLY, access, lstat, lstat_std, remove, remove_all_std, remove_std, rename},
|
||||||
is_quota_mutation_fence_path, os,
|
os,
|
||||||
os::{check_path_length, is_dir_not_empty_error, is_empty_dir, is_root_disk, rename_all, rename_all_ignore_missing_source},
|
os::{check_path_length, is_dir_not_empty_error, is_empty_dir, is_root_disk, rename_all, rename_all_ignore_missing_source},
|
||||||
quota_mutation_fence_path,
|
|
||||||
};
|
};
|
||||||
use crate::erasure::coding::{self, bitrot_verify};
|
use crate::erasure::coding::{self, bitrot_verify};
|
||||||
use crate::runtime::sources as runtime_sources;
|
use crate::runtime::sources as runtime_sources;
|
||||||
@@ -61,7 +55,9 @@ use std::collections::HashMap;
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::io::{Error as IoError, SeekFrom};
|
use std::io::{Error as IoError, SeekFrom};
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
|
#[cfg(target_os = "linux")]
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -2028,17 +2024,14 @@ static RENAME_DATA_REMOVE_DST_BASE_BEFORE_COMMIT: std::sync::Mutex<Option<(Strin
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
type InlinePreparationHook = Box<dyn FnOnce() + Send>;
|
type InlinePreparationHook = Box<dyn FnOnce() + Send>;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
type RenameDataPublicationHookKey = (PathBuf, String, String);
|
|
||||||
#[cfg(test)]
|
|
||||||
static INLINE_PREPARATION_BEFORE_BACKUP: std::sync::LazyLock<std::sync::Mutex<HashMap<String, InlinePreparationHook>>> =
|
static INLINE_PREPARATION_BEFORE_BACKUP: std::sync::LazyLock<std::sync::Mutex<HashMap<String, InlinePreparationHook>>> =
|
||||||
std::sync::LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));
|
std::sync::LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
static INLINE_BEFORE_FILE_SYNC_ADMISSION: std::sync::LazyLock<std::sync::Mutex<HashMap<String, InlinePreparationHook>>> =
|
static INLINE_BEFORE_FILE_SYNC_ADMISSION: std::sync::LazyLock<std::sync::Mutex<HashMap<String, InlinePreparationHook>>> =
|
||||||
std::sync::LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));
|
std::sync::LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
static RENAME_DATA_AFTER_FIRST_PUBLICATION: std::sync::LazyLock<
|
static RENAME_DATA_AFTER_FIRST_PUBLICATION: std::sync::LazyLock<std::sync::Mutex<HashMap<String, InlinePreparationHook>>> =
|
||||||
std::sync::Mutex<HashMap<RenameDataPublicationHookKey, InlinePreparationHook>>,
|
std::sync::LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));
|
||||||
> = std::sync::LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
static OWNED_FILE_WRITE_BEFORE_OPEN: std::sync::LazyLock<std::sync::Mutex<HashMap<PathBuf, InlinePreparationHook>>> =
|
static OWNED_FILE_WRITE_BEFORE_OPEN: std::sync::LazyLock<std::sync::Mutex<HashMap<PathBuf, InlinePreparationHook>>> =
|
||||||
std::sync::LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));
|
std::sync::LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));
|
||||||
@@ -2110,11 +2103,11 @@ fn set_inline_before_file_sync_admission(dst_path: &str, hook: impl FnOnce() + S
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn set_rename_data_after_first_publication(root: &Path, dst_volume: &str, dst_path: &str, hook: impl FnOnce() + Send + 'static) {
|
fn set_rename_data_after_first_publication(dst_path: &str, hook: impl FnOnce() + Send + 'static) {
|
||||||
RENAME_DATA_AFTER_FIRST_PUBLICATION
|
RENAME_DATA_AFTER_FIRST_PUBLICATION
|
||||||
.lock()
|
.lock()
|
||||||
.expect("test publication hook lock should not be poisoned")
|
.expect("test publication hook lock should not be poisoned")
|
||||||
.insert((root.to_path_buf(), dst_volume.to_string(), dst_path.to_string()), Box::new(hook));
|
.insert(dst_path.to_string(), Box::new(hook));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -2266,11 +2259,11 @@ fn run_inline_before_file_sync_admission(dst_path: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn run_rename_data_after_first_publication(root: &Path, dst_volume: &str, dst_path: &str) {
|
fn run_rename_data_after_first_publication(dst_path: &str) {
|
||||||
let hook = RENAME_DATA_AFTER_FIRST_PUBLICATION
|
let hook = RENAME_DATA_AFTER_FIRST_PUBLICATION
|
||||||
.lock()
|
.lock()
|
||||||
.expect("test publication hook lock should not be poisoned")
|
.expect("test publication hook lock should not be poisoned")
|
||||||
.remove(&(root.to_path_buf(), dst_volume.to_string(), dst_path.to_string()));
|
.remove(dst_path);
|
||||||
if let Some(hook) = hook {
|
if let Some(hook) = hook {
|
||||||
hook();
|
hook();
|
||||||
}
|
}
|
||||||
@@ -2368,6 +2361,9 @@ async fn remove_dst_base_before_commit(
|
|||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
fn run_inline_preparation_before_backup(_dst_path: &str) {}
|
fn run_inline_preparation_before_backup(_dst_path: &str) {}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
fn run_rename_data_after_first_publication(_dst_path: &str) {}
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
fn should_fail_after_delete_data_staged(_path: &str) -> bool {
|
fn should_fail_after_delete_data_staged(_path: &str) -> bool {
|
||||||
false
|
false
|
||||||
@@ -4755,25 +4751,6 @@ struct SnapshotLeaseEntry {
|
|||||||
tokens: HashSet<SnapshotLeaseToken>,
|
tokens: HashSet<SnapshotLeaseToken>,
|
||||||
pending_delete: Option<DeleteOptions>,
|
pending_delete: Option<DeleteOptions>,
|
||||||
deleting: bool,
|
deleting: bool,
|
||||||
mutation_fence: Option<Arc<QuotaMutationFenceState>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct QuotaMutationFenceState {
|
|
||||||
revoked: AtomicBool,
|
|
||||||
running: AtomicUsize,
|
|
||||||
notify: Notify,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct QuotaMutationFenceClaim {
|
|
||||||
state: Arc<QuotaMutationFenceState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for QuotaMutationFenceClaim {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.state.running.fetch_sub(1, Ordering::AcqRel);
|
|
||||||
self.state.notify.notify_waiters();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -7338,34 +7315,6 @@ fn normalize_path_components(path: impl AsRef<Path>) -> PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LocalDisk {
|
impl LocalDisk {
|
||||||
async fn claim_quota_mutation_fence(
|
|
||||||
&self,
|
|
||||||
volume: &str,
|
|
||||||
path: &str,
|
|
||||||
token: SnapshotLeaseToken,
|
|
||||||
) -> Result<Arc<QuotaMutationFenceClaim>> {
|
|
||||||
let key = SnapshotLeaseKey {
|
|
||||||
volume: RUSTFS_META_BUCKET.to_string(),
|
|
||||||
path: quota_mutation_fence_path(volume, path),
|
|
||||||
};
|
|
||||||
let state = {
|
|
||||||
let registry = self.snapshot_leases.lock().await;
|
|
||||||
let entry = registry.entries.get(&key).ok_or(DiskError::FileNotFound)?;
|
|
||||||
let state = entry.mutation_fence.as_ref().ok_or(DiskError::FileNotFound)?;
|
|
||||||
if !entry.tokens.contains(&token) || state.revoked.load(Ordering::Acquire) {
|
|
||||||
return Err(DiskError::FileNotFound);
|
|
||||||
}
|
|
||||||
state.running.fetch_add(1, Ordering::AcqRel);
|
|
||||||
Arc::clone(state)
|
|
||||||
};
|
|
||||||
if state.revoked.load(Ordering::Acquire) {
|
|
||||||
state.running.fetch_sub(1, Ordering::AcqRel);
|
|
||||||
state.notify.notify_waiters();
|
|
||||||
return Err(DiskError::FileNotFound);
|
|
||||||
}
|
|
||||||
Ok(Arc::new(QuotaMutationFenceClaim { state }))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn reserve_version_delete(&self, volume: &str, object: &str, data_dir: Uuid, rollback_dir: Uuid) -> Result<bool> {
|
async fn reserve_version_delete(&self, volume: &str, object: &str, data_dir: Uuid, rollback_dir: Uuid) -> Result<bool> {
|
||||||
let path = format!("{object}/{data_dir}");
|
let path = format!("{object}/{data_dir}");
|
||||||
let data_path = self.io_get_object_path(volume, &path)?;
|
let data_path = self.io_get_object_path(volume, &path)?;
|
||||||
@@ -8689,41 +8638,17 @@ impl DiskAPI for LocalDisk {
|
|||||||
&self,
|
&self,
|
||||||
src_volume: &str,
|
src_volume: &str,
|
||||||
src_path: &str,
|
src_path: &str,
|
||||||
fi: FileInfo,
|
mut fi: FileInfo,
|
||||||
dst_volume: &str,
|
dst_volume: &str,
|
||||||
dst_path: &str,
|
dst_path: &str,
|
||||||
) -> Result<RenameDataResp> {
|
) -> Result<RenameDataResp> {
|
||||||
crate::hp_guard!("LocalDisk::rename_data");
|
crate::hp_guard!("LocalDisk::rename_data");
|
||||||
let mut fi = fi;
|
|
||||||
// A non-force DeleteBucket must not remove a directory while a local
|
// A non-force DeleteBucket must not remove a directory while a local
|
||||||
// object commit is publishing into it. The peer's empty scan remains
|
// object commit is publishing into it. The peer's empty scan remains
|
||||||
// optimistic; this lease establishes the local commit/delete order and
|
// optimistic; this lease establishes the local commit/delete order and
|
||||||
// remains owned by any blocking syscall that outlives async cancellation.
|
// remains owned by any blocking syscall that outlives async cancellation.
|
||||||
let destination_object_path = self.io_get_object_path(dst_volume, dst_path)?;
|
let destination_object_path = self.io_get_object_path(dst_volume, dst_path)?;
|
||||||
let quota_fence_token =
|
|
||||||
match rustfs_utils::http::metadata_compat::get_consistent_str(&fi.metadata, QUOTA_MUTATION_FENCE_METADATA_SUFFIX) {
|
|
||||||
Some(value) => {
|
|
||||||
let token = Uuid::parse_str(value).map_err(|_| DiskError::FileCorrupt)?;
|
|
||||||
Some(SnapshotLeaseToken::from_slice(token.as_bytes())?)
|
|
||||||
}
|
|
||||||
None if rustfs_utils::http::metadata_compat::contains_key_str(
|
|
||||||
&fi.metadata,
|
|
||||||
QUOTA_MUTATION_FENCE_METADATA_SUFFIX,
|
|
||||||
) =>
|
|
||||||
{
|
|
||||||
return Err(DiskError::FileCorrupt);
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
rustfs_utils::http::metadata_compat::remove_str(&mut fi.metadata, QUOTA_MUTATION_FENCE_METADATA_SUFFIX);
|
|
||||||
let quota_fence_claim = match quota_fence_token {
|
|
||||||
Some(token) => Some(self.claim_quota_mutation_fence(dst_volume, dst_path, token).await?),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
let mutation_lease = os::acquire_rename_data_mutation_lease(&self.root, dst_volume, &destination_object_path).await;
|
let mutation_lease = os::acquire_rename_data_mutation_lease(&self.root, dst_volume, &destination_object_path).await;
|
||||||
if let Some(claim) = quota_fence_claim {
|
|
||||||
mutation_lease.attach_external_guard(claim);
|
|
||||||
}
|
|
||||||
if fi.is_legacy_indexed_delete_marker() {
|
if fi.is_legacy_indexed_delete_marker() {
|
||||||
fi.erasure.index = 0;
|
fi.erasure.index = 0;
|
||||||
}
|
}
|
||||||
@@ -9016,9 +8941,8 @@ impl DiskAPI for LocalDisk {
|
|||||||
.await?;
|
.await?;
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
|
||||||
if has_data_dir_path.is_some() {
|
if has_data_dir_path.is_some() {
|
||||||
run_rename_data_after_first_publication(&self.root, dst_volume, dst_path);
|
run_rename_data_after_first_publication(dst_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crash-consistency injection: hard power loss after the data dir
|
// Crash-consistency injection: hard power loss after the data dir
|
||||||
@@ -9451,8 +9375,7 @@ impl DiskAPI for LocalDisk {
|
|||||||
let _ = remove_file_if_exists(staged_backup);
|
let _ = remove_file_if_exists(staged_backup);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
run_rename_data_after_first_publication(dst_path);
|
||||||
run_rename_data_after_first_publication(&self.root, dst_volume, dst_path);
|
|
||||||
if sync {
|
if sync {
|
||||||
file_sync_admission = Some(
|
file_sync_admission = Some(
|
||||||
os::acquire_file_sync_admission(self.file_sync_permits.clone())
|
os::acquire_file_sync_admission(self.file_sync_permits.clone())
|
||||||
@@ -9715,26 +9638,11 @@ impl DiskAPI for LocalDisk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn acquire_snapshot_lease(&self, volume: &str, path: &str) -> Result<SnapshotLeaseToken> {
|
async fn acquire_snapshot_lease(&self, volume: &str, path: &str) -> Result<SnapshotLeaseToken> {
|
||||||
|
let file_path = self.io_get_object_path(volume, path)?;
|
||||||
let key = SnapshotLeaseKey {
|
let key = SnapshotLeaseKey {
|
||||||
volume: volume.to_string(),
|
volume: volume.to_string(),
|
||||||
path: path.to_string(),
|
path: path.to_string(),
|
||||||
};
|
};
|
||||||
if volume == RUSTFS_META_BUCKET && is_quota_mutation_fence_path(path) {
|
|
||||||
let mut registry = self.snapshot_leases.lock().await;
|
|
||||||
let entry = registry.entries.entry(key).or_default();
|
|
||||||
let state = entry
|
|
||||||
.mutation_fence
|
|
||||||
.get_or_insert_with(|| Arc::new(QuotaMutationFenceState::default()));
|
|
||||||
if state.revoked.load(Ordering::Acquire) {
|
|
||||||
return Err(DiskError::FileNotFound);
|
|
||||||
}
|
|
||||||
let token = SnapshotLeaseToken::new();
|
|
||||||
entry.tokens.insert(token);
|
|
||||||
return Ok(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
let file_path = self.io_get_object_path(volume, path)?;
|
|
||||||
let _mutation_lease = os::acquire_rename_data_mutation_lease(&self.root, volume, &file_path).await;
|
|
||||||
let token = {
|
let token = {
|
||||||
let mut registry = self.snapshot_leases.lock().await;
|
let mut registry = self.snapshot_leases.lock().await;
|
||||||
if registry.entries.get(&key).is_some_and(|entry| entry.deleting) {
|
if registry.entries.get(&key).is_some_and(|entry| entry.deleting) {
|
||||||
@@ -9762,48 +9670,6 @@ impl DiskAPI for LocalDisk {
|
|||||||
volume: volume.to_string(),
|
volume: volume.to_string(),
|
||||||
path: path.to_string(),
|
path: path.to_string(),
|
||||||
};
|
};
|
||||||
if volume == RUSTFS_META_BUCKET && is_quota_mutation_fence_path(path) {
|
|
||||||
if !token.is_revoke_all() {
|
|
||||||
let mut registry = self.snapshot_leases.lock().await;
|
|
||||||
let Some(entry) = registry.entries.get_mut(&key) else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
entry.tokens.remove(&token);
|
|
||||||
let removable = entry.tokens.is_empty()
|
|
||||||
&& entry
|
|
||||||
.mutation_fence
|
|
||||||
.as_ref()
|
|
||||||
.is_none_or(|state| state.running.load(Ordering::Acquire) == 0);
|
|
||||||
if removable {
|
|
||||||
registry.entries.remove(&key);
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let state = {
|
|
||||||
let mut registry = self.snapshot_leases.lock().await;
|
|
||||||
let Some(entry) = registry.entries.get_mut(&key) else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let Some(state) = entry.mutation_fence.as_ref().cloned() else {
|
|
||||||
registry.entries.remove(&key);
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
state.revoked.store(true, Ordering::Release);
|
|
||||||
entry.tokens.clear();
|
|
||||||
state
|
|
||||||
};
|
|
||||||
loop {
|
|
||||||
let notified = state.notify.notified();
|
|
||||||
tokio::pin!(notified);
|
|
||||||
notified.as_mut().enable();
|
|
||||||
if state.running.load(Ordering::Acquire) == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
notified.await;
|
|
||||||
}
|
|
||||||
self.snapshot_leases.lock().await.entries.remove(&key);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let opts = {
|
let opts = {
|
||||||
let mut registry = self.snapshot_leases.lock().await;
|
let mut registry = self.snapshot_leases.lock().await;
|
||||||
let Some(entry) = registry.entries.get_mut(&key) else {
|
let Some(entry) = registry.entries.get_mut(&key) else {
|
||||||
@@ -9974,12 +9840,6 @@ impl DiskAPI for LocalDisk {
|
|||||||
opts: &ReadOptions,
|
opts: &ReadOptions,
|
||||||
) -> Result<FileInfo> {
|
) -> Result<FileInfo> {
|
||||||
crate::hp_guard!("LocalDisk::read_version");
|
crate::hp_guard!("LocalDisk::read_version");
|
||||||
let stage_metrics_enabled = rustfs_io_metrics::get_stage_metrics_enabled();
|
|
||||||
let metrics_path = if stage_metrics_enabled && crate::bucket::utils::is_meta_bucketname(volume) {
|
|
||||||
GET_OBJECT_PATH_INTERNAL_META
|
|
||||||
} else {
|
|
||||||
GET_OBJECT_PATH_LEGACY_DUPLEX
|
|
||||||
};
|
|
||||||
if !org_volume.is_empty() {
|
if !org_volume.is_empty() {
|
||||||
let org_volume_path = self.io_get_bucket_path(org_volume)?;
|
let org_volume_path = self.io_get_bucket_path(org_volume)?;
|
||||||
if !skip_access_checks(org_volume) {
|
if !skip_access_checks(org_volume) {
|
||||||
@@ -9989,46 +9849,37 @@ impl DiskAPI for LocalDisk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let path_resolve_start = get_stage_timer_if_enabled(stage_metrics_enabled);
|
|
||||||
let file_path = self.io_get_object_path(volume, path)?;
|
let file_path = self.io_get_object_path(volume, path)?;
|
||||||
let volume_dir = self.io_get_bucket_path(volume)?;
|
let volume_dir = self.io_get_bucket_path(volume)?;
|
||||||
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_READ_VERSION_PATH_RESOLVE, path_resolve_start);
|
|
||||||
|
|
||||||
let path_check_start = get_stage_timer_if_enabled(stage_metrics_enabled);
|
|
||||||
check_path_length(file_path.to_string_lossy().as_ref())?;
|
check_path_length(file_path.to_string_lossy().as_ref())?;
|
||||||
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_READ_VERSION_PATH_CHECK, path_check_start);
|
|
||||||
|
|
||||||
let read_data = opts.read_data;
|
let read_data = opts.read_data;
|
||||||
|
|
||||||
let xlmeta_read_start = get_stage_timer_if_enabled(stage_metrics_enabled);
|
let (data, _) = self
|
||||||
let raw_read_result = self.read_raw(volume, volume_dir.clone(), file_path, read_data).await;
|
.read_raw(volume, volume_dir.clone(), file_path, read_data)
|
||||||
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_READ_VERSION_XLMETA_READ, xlmeta_read_start);
|
.await
|
||||||
let (data, _) = raw_read_result.map_err(|e| {
|
.map_err(|e| {
|
||||||
if e == DiskError::FileNotFound && !version_id.is_empty() {
|
if e == DiskError::FileNotFound && !version_id.is_empty() {
|
||||||
DiskError::FileVersionNotFound
|
DiskError::FileVersionNotFound
|
||||||
} else {
|
} else {
|
||||||
e
|
e
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let decode_start = get_stage_timer_if_enabled(stage_metrics_enabled);
|
let mut fi = get_file_info(
|
||||||
let file_info_result: Result<FileInfo> = (|| {
|
&data,
|
||||||
let fi = get_file_info(
|
volume,
|
||||||
&data,
|
path,
|
||||||
volume,
|
version_id,
|
||||||
path,
|
FileInfoOpts {
|
||||||
version_id,
|
data: read_data,
|
||||||
FileInfoOpts {
|
include_free_versions: opts.incl_free_versions,
|
||||||
data: read_data,
|
include_part_checksums: false,
|
||||||
include_free_versions: opts.incl_free_versions,
|
},
|
||||||
include_part_checksums: false,
|
)?;
|
||||||
},
|
|
||||||
)?;
|
fi.validate_for_metadata_read()?;
|
||||||
fi.validate_for_metadata_read()?;
|
|
||||||
Ok(fi)
|
|
||||||
})();
|
|
||||||
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_READ_VERSION_DECODE, decode_start);
|
|
||||||
let mut fi = file_info_result?;
|
|
||||||
if fi.is_canonical_delete_marker() {
|
if fi.is_canonical_delete_marker() {
|
||||||
return Ok(fi);
|
return Ok(fi);
|
||||||
}
|
}
|
||||||
@@ -10582,19 +10433,6 @@ impl DiskAPI for LocalDisk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalDisk {
|
|
||||||
pub(crate) async fn rename_data_borrowed(
|
|
||||||
&self,
|
|
||||||
src_volume: &str,
|
|
||||||
src_path: &str,
|
|
||||||
fi: &FileInfo,
|
|
||||||
dst_volume: &str,
|
|
||||||
dst_path: &str,
|
|
||||||
) -> Result<RenameDataResp> {
|
|
||||||
<Self as DiskAPI>::rename_data(self, src_volume, src_path, fi.clone(), dst_volume, dst_path).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn wait_for_startup_cleanup_signal(
|
async fn wait_for_startup_cleanup_signal(
|
||||||
startup_cleanup_ready: &AtomicU32,
|
startup_cleanup_ready: &AtomicU32,
|
||||||
startup_cleanup_notify: &Notify,
|
startup_cleanup_notify: &Notify,
|
||||||
@@ -10724,108 +10562,6 @@ mod test {
|
|||||||
meta.marshal_msg().expect("test metadata should encode")
|
meta.marshal_msg().expect("test metadata should encode")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial_test::serial]
|
|
||||||
fn read_version_records_local_metadata_stage_breakdown() {
|
|
||||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.expect("test runtime should be created");
|
|
||||||
let recorder = crate::test_metrics::CapturingRecorder::default();
|
|
||||||
let previous_gate = rustfs_io_metrics::get_stage_metrics_enabled();
|
|
||||||
rustfs_io_metrics::set_get_stage_metrics_enabled(true);
|
|
||||||
|
|
||||||
metrics::with_local_recorder(&recorder, || {
|
|
||||||
runtime.block_on(async {
|
|
||||||
let dir = tempfile::tempdir().expect("temp dir should be created");
|
|
||||||
let endpoint =
|
|
||||||
Endpoint::try_from(dir.path().to_str().expect("temp dir should be utf8")).expect("endpoint should parse");
|
|
||||||
let disk = LocalDisk::new(&endpoint, false).await.expect("local disk should be created");
|
|
||||||
let bucket = "bucket";
|
|
||||||
let object = "stage-breakdown";
|
|
||||||
ensure_test_volume(&disk, bucket).await;
|
|
||||||
|
|
||||||
let object_dir = dir.path().join(bucket).join(object);
|
|
||||||
fs::create_dir_all(&object_dir)
|
|
||||||
.await
|
|
||||||
.expect("object directory should be created");
|
|
||||||
fs::write(
|
|
||||||
object_dir.join(STORAGE_FORMAT_FILE),
|
|
||||||
test_meta(test_file_info(object, Uuid::new_v4(), None, Some(Bytes::from_static(b"inline")))),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("object metadata should be written");
|
|
||||||
|
|
||||||
disk.read_version(
|
|
||||||
"",
|
|
||||||
bucket,
|
|
||||||
object,
|
|
||||||
"",
|
|
||||||
&ReadOptions {
|
|
||||||
read_data: true,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("read_version should succeed");
|
|
||||||
|
|
||||||
let meta_object = "stage-breakdown-meta";
|
|
||||||
let meta_object_dir = dir.path().join(RUSTFS_META_BUCKET).join(meta_object);
|
|
||||||
fs::create_dir_all(&meta_object_dir)
|
|
||||||
.await
|
|
||||||
.expect("internal metadata object directory should be created");
|
|
||||||
fs::write(
|
|
||||||
meta_object_dir.join(STORAGE_FORMAT_FILE),
|
|
||||||
test_meta(test_file_info(meta_object, Uuid::new_v4(), None, Some(Bytes::from_static(b"meta")))),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("internal metadata should be written");
|
|
||||||
|
|
||||||
disk.read_version(
|
|
||||||
"",
|
|
||||||
RUSTFS_META_BUCKET,
|
|
||||||
meta_object,
|
|
||||||
"",
|
|
||||||
&ReadOptions {
|
|
||||||
read_data: true,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("internal metadata read_version should succeed");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
rustfs_io_metrics::set_get_stage_metrics_enabled(previous_gate);
|
|
||||||
|
|
||||||
for stage in [
|
|
||||||
GET_STAGE_READ_VERSION_PATH_RESOLVE,
|
|
||||||
GET_STAGE_READ_VERSION_PATH_CHECK,
|
|
||||||
GET_STAGE_READ_VERSION_XLMETA_READ,
|
|
||||||
GET_STAGE_READ_VERSION_DECODE,
|
|
||||||
] {
|
|
||||||
assert_eq!(
|
|
||||||
recorder
|
|
||||||
.histogram_values(
|
|
||||||
"rustfs_io_get_object_stage_duration_seconds",
|
|
||||||
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX), ("stage", stage)]
|
|
||||||
)
|
|
||||||
.len(),
|
|
||||||
1,
|
|
||||||
"{stage} should be recorded once for user-bucket LocalDisk::read_version"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
recorder
|
|
||||||
.histogram_values(
|
|
||||||
"rustfs_io_get_object_stage_duration_seconds",
|
|
||||||
&[("path", GET_OBJECT_PATH_INTERNAL_META), ("stage", stage)]
|
|
||||||
)
|
|
||||||
.len(),
|
|
||||||
1,
|
|
||||||
"{stage} should be recorded once for internal-meta LocalDisk::read_version"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inline_metadata_rollback_dir_avoids_real_data_dir_collision() {
|
fn inline_metadata_rollback_dir_avoids_real_data_dir_collision() {
|
||||||
let target_version = Uuid::parse_str("11111111-2222-3333-4444-555555555555").expect("version id should parse");
|
let target_version = Uuid::parse_str("11111111-2222-3333-4444-555555555555").expect("version id should parse");
|
||||||
@@ -13149,7 +12885,7 @@ mod test {
|
|||||||
let replacement_staging_parent_for_hook = replacement_staging_parent.clone();
|
let replacement_staging_parent_for_hook = replacement_staging_parent.clone();
|
||||||
let staged_metadata_for_hook = staged_metadata.clone();
|
let staged_metadata_for_hook = staged_metadata.clone();
|
||||||
let replacement_staged_metadata_for_hook = replacement_staged_metadata.clone();
|
let replacement_staged_metadata_for_hook = replacement_staged_metadata.clone();
|
||||||
set_rename_data_after_first_publication(&disk.root, bucket, object, move || {
|
set_rename_data_after_first_publication(object, move || {
|
||||||
std::fs::rename(&object_dir_for_hook, &replacement_dir_for_hook)
|
std::fs::rename(&object_dir_for_hook, &replacement_dir_for_hook)
|
||||||
.expect_err("the destination object identity must remain pinned until xl.meta commits");
|
.expect_err("the destination object identity must remain pinned until xl.meta commits");
|
||||||
std::fs::rename(&staging_parent_for_hook, &replacement_staging_parent_for_hook)
|
std::fs::rename(&staging_parent_for_hook, &replacement_staging_parent_for_hook)
|
||||||
@@ -13416,7 +13152,7 @@ mod test {
|
|||||||
let replacement_dir_for_hook = replacement_dir.clone();
|
let replacement_dir_for_hook = replacement_dir.clone();
|
||||||
let staged_metadata_for_hook = staged_metadata.clone();
|
let staged_metadata_for_hook = staged_metadata.clone();
|
||||||
let replacement_staged_metadata_for_hook = replacement_staged_metadata.clone();
|
let replacement_staged_metadata_for_hook = replacement_staged_metadata.clone();
|
||||||
set_rename_data_after_first_publication(&disk.root, bucket, object, move || {
|
set_rename_data_after_first_publication(object, move || {
|
||||||
std::fs::rename(&object_dir_for_hook, &replacement_dir_for_hook)
|
std::fs::rename(&object_dir_for_hook, &replacement_dir_for_hook)
|
||||||
.expect_err("the destination object identity must remain pinned after publishing its rollback backup");
|
.expect_err("the destination object identity must remain pinned after publishing its rollback backup");
|
||||||
std::fs::rename(&staged_metadata_for_hook, &replacement_staged_metadata_for_hook)
|
std::fs::rename(&staged_metadata_for_hook, &replacement_staged_metadata_for_hook)
|
||||||
@@ -13782,7 +13518,7 @@ mod test {
|
|||||||
|
|
||||||
let (entered_tx, entered_rx) = mpsc::channel();
|
let (entered_tx, entered_rx) = mpsc::channel();
|
||||||
let (release_tx, release_rx) = mpsc::channel();
|
let (release_tx, release_rx) = mpsc::channel();
|
||||||
set_rename_data_after_first_publication(&disk.root, bucket, object, move || {
|
set_rename_data_after_first_publication(object, move || {
|
||||||
entered_tx.send(()).expect("signal first publication");
|
entered_tx.send(()).expect("signal first publication");
|
||||||
release_rx.recv().expect("wait while delete_volume is blocked");
|
release_rx.recv().expect("wait while delete_volume is blocked");
|
||||||
});
|
});
|
||||||
@@ -14376,7 +14112,7 @@ mod test {
|
|||||||
|
|
||||||
let (published_tx, published_rx) = mpsc::channel();
|
let (published_tx, published_rx) = mpsc::channel();
|
||||||
let (release_tx, release_rx) = mpsc::channel();
|
let (release_tx, release_rx) = mpsc::channel();
|
||||||
set_rename_data_after_first_publication(&disk.root, bucket, object, move || {
|
set_rename_data_after_first_publication(object, move || {
|
||||||
published_tx.send(()).expect("signal backup publication");
|
published_tx.send(()).expect("signal backup publication");
|
||||||
release_rx.recv().expect("wait for lock-order assertion");
|
release_rx.recv().expect("wait for lock-order assertion");
|
||||||
});
|
});
|
||||||
@@ -19031,48 +18767,6 @@ mod test {
|
|||||||
assert!(matches!(disk.read_all(volume, &first_part).await, Err(DiskError::FileNotFound)));
|
assert!(matches!(disk.read_all(volume, &first_part).await, Err(DiskError::FileNotFound)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn quota_mutation_fence_revoke_waits_for_active_claim_and_rejects_late_claims() {
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
let root_dir = tempdir().expect("temp dir should be created");
|
|
||||||
let endpoint = Endpoint::try_from(root_dir.path().to_string_lossy().as_ref()).expect("endpoint should parse");
|
|
||||||
let disk = Arc::new(LocalDisk::new(&endpoint, false).await.expect("local disk should be created"));
|
|
||||||
let bucket = "quota-fence-volume";
|
|
||||||
let object = "object";
|
|
||||||
let fence_path = quota_mutation_fence_path(bucket, object);
|
|
||||||
let token = disk
|
|
||||||
.acquire_snapshot_lease(RUSTFS_META_BUCKET, &fence_path)
|
|
||||||
.await
|
|
||||||
.expect("quota mutation token should be prepared");
|
|
||||||
let claim = disk
|
|
||||||
.claim_quota_mutation_fence(bucket, object, token)
|
|
||||||
.await
|
|
||||||
.expect("prepared token should be claimable");
|
|
||||||
|
|
||||||
let release_disk = Arc::clone(&disk);
|
|
||||||
let mut release = tokio::spawn(async move {
|
|
||||||
release_disk
|
|
||||||
.release_snapshot_lease(RUSTFS_META_BUCKET, &fence_path, SnapshotLeaseToken::revoke_all())
|
|
||||||
.await
|
|
||||||
});
|
|
||||||
assert!(
|
|
||||||
tokio::time::timeout(Duration::from_millis(50), &mut release).await.is_err(),
|
|
||||||
"revoke must wait until an already claimed mutation has finished"
|
|
||||||
);
|
|
||||||
|
|
||||||
drop(claim);
|
|
||||||
tokio::time::timeout(Duration::from_secs(1), release)
|
|
||||||
.await
|
|
||||||
.expect("revoke should wake after the final claim drops")
|
|
||||||
.expect("revoke task should not panic")
|
|
||||||
.expect("revoke should succeed");
|
|
||||||
assert!(matches!(
|
|
||||||
disk.claim_quota_mutation_fence(bucket, object, token).await,
|
|
||||||
Err(DiskError::FileNotFound)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn delete_version_keeps_later_part_until_snapshot_release() {
|
async fn delete_version_keeps_later_part_until_snapshot_release() {
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ pub fn part_transaction_path(part_path: &str) -> String {
|
|||||||
|
|
||||||
use crate::cluster::rpc::RemoteDisk;
|
use crate::cluster::rpc::RemoteDisk;
|
||||||
use crate::cluster::rpc::build_internode_data_transport_from_env;
|
use crate::cluster::rpc::build_internode_data_transport_from_env;
|
||||||
use crate::disk::disk_store::DiskStoreRenameDataExt;
|
|
||||||
use crate::disk::disk_store::LocalDiskWrapper;
|
use crate::disk::disk_store::LocalDiskWrapper;
|
||||||
use crate::disk::health_state::RuntimeDriveHealthState;
|
use crate::disk::health_state::RuntimeDriveHealthState;
|
||||||
use crate::disk::local::ScanGuard;
|
use crate::disk::local::ScanGuard;
|
||||||
@@ -73,28 +72,6 @@ use time::OffsetDateTime;
|
|||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const QUOTA_MUTATION_FENCE_PREFIX: &str = "tmp/quota-mutation-fences/";
|
|
||||||
pub(crate) const QUOTA_MUTATION_FENCE_METADATA_SUFFIX: &str = "quota-mutation-fence-token";
|
|
||||||
|
|
||||||
pub(crate) fn quota_mutation_fence_path(bucket: &str, object: &str) -> String {
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
|
|
||||||
let mut input = Vec::with_capacity(bucket.len() + object.len() + 1);
|
|
||||||
input.extend_from_slice(bucket.as_bytes());
|
|
||||||
input.push(0);
|
|
||||||
input.extend_from_slice(object.as_bytes());
|
|
||||||
let digest = Sha256::digest(input);
|
|
||||||
format!(
|
|
||||||
"{QUOTA_MUTATION_FENCE_PREFIX}{}",
|
|
||||||
hex_simd::encode_to_string(digest, hex_simd::AsciiCase::Lower)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_quota_mutation_fence_path(path: &str) -> bool {
|
|
||||||
path.strip_prefix(QUOTA_MUTATION_FENCE_PREFIX)
|
|
||||||
.is_some_and(|digest| digest.len() == 64 && digest.bytes().all(|byte| byte.is_ascii_hexdigit()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type DiskStore = Arc<Disk>;
|
pub type DiskStore = Arc<Disk>;
|
||||||
|
|
||||||
pub type FileReader = Box<dyn AsyncRead + Send + Sync + Unpin>;
|
pub type FileReader = Box<dyn AsyncRead + Send + Sync + Unpin>;
|
||||||
@@ -119,20 +96,6 @@ impl SnapshotLeaseToken {
|
|||||||
pub fn as_bytes(&self) -> &[u8; 16] {
|
pub fn as_bytes(&self) -> &[u8; 16] {
|
||||||
self.0.as_bytes()
|
self.0.as_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn as_uuid(self) -> Uuid {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub fn revoke_all() -> Self {
|
|
||||||
Self(Uuid::nil())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub fn is_revoke_all(self) -> bool {
|
|
||||||
self.0.is_nil()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SnapshotLeaseToken {
|
impl Default for SnapshotLeaseToken {
|
||||||
@@ -435,8 +398,10 @@ impl DiskAPI for Disk {
|
|||||||
dst_volume: &str,
|
dst_volume: &str,
|
||||||
dst_path: &str,
|
dst_path: &str,
|
||||||
) -> Result<RenameDataResp> {
|
) -> Result<RenameDataResp> {
|
||||||
self.rename_data_borrowed(src_volume, src_path, &fi, dst_volume, dst_path)
|
match self {
|
||||||
.await
|
Disk::Local(local_disk) => local_disk.rename_data(src_volume, src_path, fi, dst_volume, dst_path).await,
|
||||||
|
Disk::Remote(remote_disk) => remote_disk.rename_data(src_volume, src_path, fi, dst_volume, dst_path).await,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "trace", skip_all)]
|
#[tracing::instrument(level = "trace", skip_all)]
|
||||||
@@ -666,30 +631,6 @@ impl DiskAPI for Disk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Disk {
|
|
||||||
pub(crate) async fn rename_data_borrowed(
|
|
||||||
&self,
|
|
||||||
src_volume: &str,
|
|
||||||
src_path: &str,
|
|
||||||
fi: &FileInfo,
|
|
||||||
dst_volume: &str,
|
|
||||||
dst_path: &str,
|
|
||||||
) -> Result<RenameDataResp> {
|
|
||||||
match self {
|
|
||||||
Disk::Local(local_disk) => {
|
|
||||||
local_disk
|
|
||||||
.rename_data_borrowed(src_volume, src_path, fi, dst_volume, dst_path)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
Disk::Remote(remote_disk) => {
|
|
||||||
remote_disk
|
|
||||||
.rename_data_borrowed(src_volume, src_path, fi, dst_volume, dst_path)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Disk {
|
impl Disk {
|
||||||
pub async fn ns_scanner_server_epoch(&self) -> Result<Option<Uuid>> {
|
pub async fn ns_scanner_server_epoch(&self) -> Result<Option<Uuid>> {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@@ -306,20 +306,12 @@ fn disk_namespace_mutation_lock(path: &Path) -> Arc<NamespaceMutationLock> {
|
|||||||
pub(crate) struct NamespaceMutationLease {
|
pub(crate) struct NamespaceMutationLease {
|
||||||
_namespace_guard: OwnedMutexGuard<()>,
|
_namespace_guard: OwnedMutexGuard<()>,
|
||||||
_volume_guard: Option<OwnedRwLockReadGuard<()>>,
|
_volume_guard: Option<OwnedRwLockReadGuard<()>>,
|
||||||
external_guard: Mutex<Option<Arc<dyn Send + Sync>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NamespaceMutationLease {
|
|
||||||
pub(crate) fn attach_external_guard(&self, guard: Arc<dyn Send + Sync>) {
|
|
||||||
*self.external_guard.lock() = Some(guard);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn acquire_namespace_mutation_lease(path: &Path) -> Arc<NamespaceMutationLease> {
|
async fn acquire_namespace_mutation_lease(path: &Path) -> Arc<NamespaceMutationLease> {
|
||||||
Arc::new(NamespaceMutationLease {
|
Arc::new(NamespaceMutationLease {
|
||||||
_namespace_guard: disk_namespace_mutation_lock(path).lock_owned().await,
|
_namespace_guard: disk_namespace_mutation_lock(path).lock_owned().await,
|
||||||
_volume_guard: None,
|
_volume_guard: None,
|
||||||
external_guard: Mutex::new(None),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +327,6 @@ pub(crate) async fn acquire_rename_data_mutation_lease(
|
|||||||
Arc::new(NamespaceMutationLease {
|
Arc::new(NamespaceMutationLease {
|
||||||
_namespace_guard: namespace_guard,
|
_namespace_guard: namespace_guard,
|
||||||
_volume_guard: Some(volume_guard),
|
_volume_guard: Some(volume_guard),
|
||||||
external_guard: Mutex::new(None),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ pub(crate) const GET_RECONSTRUCT_OUTCOME_SKIP_DATA_COMPLETE: &str = "skip_data_c
|
|||||||
pub(crate) const GET_RECONSTRUCT_OUTCOME_SKIP_EMPTY_PAYLOAD: &str = "skip_empty_payload";
|
pub(crate) const GET_RECONSTRUCT_OUTCOME_SKIP_EMPTY_PAYLOAD: &str = "skip_empty_payload";
|
||||||
|
|
||||||
pub(crate) trait DecodeWorkspace: Send + Sync + 'static {
|
pub(crate) trait DecodeWorkspace: Send + Sync + 'static {
|
||||||
#[allow(dead_code, reason = "workspace width asserted by decode_reader tests (backlog#1823)")]
|
|
||||||
fn shard_len(&self) -> usize;
|
fn shard_len(&self) -> usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,14 +33,11 @@ pub(crate) trait ErasureDecodeEngine: Send + Sync + 'static {
|
|||||||
type Workspace: DecodeWorkspace;
|
type Workspace: DecodeWorkspace;
|
||||||
|
|
||||||
fn data_shards(&self) -> usize;
|
fn data_shards(&self) -> usize;
|
||||||
#[allow(dead_code, reason = "engine trait facet asserted by decode_reader tests (backlog#1823)")]
|
|
||||||
fn parity_shards(&self) -> usize;
|
fn parity_shards(&self) -> usize;
|
||||||
fn block_size(&self) -> usize;
|
fn block_size(&self) -> usize;
|
||||||
fn engine_name(&self) -> &'static str;
|
fn engine_name(&self) -> &'static str;
|
||||||
|
|
||||||
#[allow(dead_code, reason = "engine trait facet asserted by decode_reader tests (backlog#1823)")]
|
|
||||||
fn supports_progressive_decode(&self) -> bool;
|
fn supports_progressive_decode(&self) -> bool;
|
||||||
#[allow(dead_code, reason = "engine trait facet asserted by decode_reader tests (backlog#1823)")]
|
|
||||||
fn supports_aligned_shards(&self) -> bool;
|
fn supports_aligned_shards(&self) -> bool;
|
||||||
|
|
||||||
fn prepare_workspace(&self, shard_len: usize) -> io::Result<Self::Workspace>;
|
fn prepare_workspace(&self, shard_len: usize) -> io::Result<Self::Workspace>;
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ impl RustfsCodecDecodeWorkspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[allow(dead_code, reason = "workspace width asserted by decode_reader tests (backlog#1823)")]
|
|
||||||
pub(crate) fn shard_len(&self) -> usize {
|
pub(crate) fn shard_len(&self) -> usize {
|
||||||
self.shard_len
|
self.shard_len
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,7 +213,6 @@ fn shard_read_launch_rank(cost: ShardReadCost) -> u8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "launch ordering asserted by this file's tests (backlog#1823)")]
|
|
||||||
fn shard_read_launch_order(read_costs: &[ShardReadCost], num_readers: usize, locality_preference_enabled: bool) -> Vec<usize> {
|
fn shard_read_launch_order(read_costs: &[ShardReadCost], num_readers: usize, locality_preference_enabled: bool) -> Vec<usize> {
|
||||||
let mut order: Vec<usize> = (0..num_readers).collect();
|
let mut order: Vec<usize> = (0..num_readers).collect();
|
||||||
if locality_preference_enabled {
|
if locality_preference_enabled {
|
||||||
@@ -409,10 +408,6 @@ where
|
|||||||
R: crate::erasure::coding::ShardSource,
|
R: crate::erasure::coding::ShardSource,
|
||||||
{
|
{
|
||||||
// Readers should handle disk errors before being passed in, ensuring each reader reaches the available number of BitrotReaders
|
// Readers should handle disk errors before being passed in, ensuring each reader reaches the available number of BitrotReaders
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "ParallelReader constructor used only by this file's tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub fn new(readers: Vec<Option<BitrotReader<R>>>, e: Erasure, offset: usize, total_length: usize) -> Self {
|
pub fn new(readers: Vec<Option<BitrotReader<R>>>, e: Erasure, offset: usize, total_length: usize) -> Self {
|
||||||
Self::new_with_metrics_path_read_timeout_and_reconstruction_verification(
|
Self::new_with_metrics_path_read_timeout_and_reconstruction_verification(
|
||||||
readers,
|
readers,
|
||||||
@@ -425,7 +420,6 @@ where
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "constructor used only by this file's tests (backlog#1823)")]
|
|
||||||
pub fn new_with_metrics_path(
|
pub fn new_with_metrics_path(
|
||||||
readers: Vec<Option<BitrotReader<R>>>,
|
readers: Vec<Option<BitrotReader<R>>>,
|
||||||
e: Erasure,
|
e: Erasure,
|
||||||
@@ -444,7 +438,6 @@ where
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "constructor used only by this file's tests (backlog#1823)")]
|
|
||||||
pub fn new_with_metrics_path_and_read_costs(
|
pub fn new_with_metrics_path_and_read_costs(
|
||||||
readers: Vec<Option<BitrotReader<R>>>,
|
readers: Vec<Option<BitrotReader<R>>>,
|
||||||
e: Erasure,
|
e: Erasure,
|
||||||
@@ -521,7 +514,6 @@ where
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "constructor used only by this file's tests (backlog#1823)")]
|
|
||||||
fn new_with_read_timeout(
|
fn new_with_read_timeout(
|
||||||
readers: Vec<Option<BitrotReader<R>>>,
|
readers: Vec<Option<BitrotReader<R>>>,
|
||||||
e: Erasure,
|
e: Erasure,
|
||||||
@@ -1338,6 +1330,10 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn can_decode(&self, shards: &[Option<Vec<u8>>]) -> bool {
|
||||||
|
shards.iter().filter(|s| s.is_some()).count() >= self.data_shards
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@@ -1543,7 +1539,6 @@ impl Erasure {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "read-cost decode path asserted by this file's tests (backlog#1823)")]
|
|
||||||
pub(crate) async fn decode_with_read_costs<W, R>(
|
pub(crate) async fn decode_with_read_costs<W, R>(
|
||||||
&self,
|
&self,
|
||||||
writer: &mut W,
|
writer: &mut W,
|
||||||
@@ -1614,9 +1609,9 @@ impl Erasure {
|
|||||||
*ret_err = Some(err.into());
|
*ret_err = Some(err.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shard-availability check, written out here rather than called on the
|
// Equivalent to `ParallelReader::can_decode`; inlined so this helper does
|
||||||
// reader so this helper does not need to borrow it, leaving the reader
|
// not need to borrow the reader, leaving the reader free for the
|
||||||
// free for the concurrent next-stripe read under prefetch.
|
// concurrent next-stripe read under prefetch.
|
||||||
let available_shards = shards.iter().filter(|shard| shard.is_some()).count();
|
let available_shards = shards.iter().filter(|shard| shard.is_some()).count();
|
||||||
if available_shards < self.data_shards {
|
if available_shards < self.data_shards {
|
||||||
let reason = GetObjectFailureReason::ReadQuorum;
|
let reason = GetObjectFailureReason::ReadQuorum;
|
||||||
|
|||||||
@@ -138,10 +138,6 @@ where
|
|||||||
S: ShardStripeSource + Send + 'static,
|
S: ShardStripeSource + Send + 'static,
|
||||||
E: ErasureDecodeEngine + Clone + Send + Sync + 'static,
|
E: ErasureDecodeEngine + Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "default-metrics-path constructor used only by this file's tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) fn new(source: S, engine: E, total_length: usize) -> io::Result<Self> {
|
pub(crate) fn new(source: S, engine: E, total_length: usize) -> io::Result<Self> {
|
||||||
Self::new_with_metrics_path(source, engine, total_length, GET_OBJECT_PATH_CODEC_STREAMING)
|
Self::new_with_metrics_path(source, engine, total_length, GET_OBJECT_PATH_CODEC_STREAMING)
|
||||||
}
|
}
|
||||||
@@ -683,10 +679,6 @@ pub(crate) struct SyncErasureDecodeReader<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<R> SyncErasureDecodeReader<R> {
|
impl<R> SyncErasureDecodeReader<R> {
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "default-metrics-path constructor used only by this file's tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) fn new(inner: R) -> Self {
|
pub(crate) fn new(inner: R) -> Self {
|
||||||
Self::new_with_metrics_path(inner, GET_OBJECT_PATH_CODEC_STREAMING)
|
Self::new_with_metrics_path(inner, GET_OBJECT_PATH_CODEC_STREAMING)
|
||||||
}
|
}
|
||||||
@@ -813,7 +805,6 @@ where
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "shard emission asserted by this file's tests (backlog#1823)")]
|
|
||||||
fn emit_data_shards(state: &StripeReadState, data_shards: usize, block_size: usize, remaining: usize) -> io::Result<Vec<u8>> {
|
fn emit_data_shards(state: &StripeReadState, data_shards: usize, block_size: usize, remaining: usize) -> io::Result<Vec<u8>> {
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
emit_data_shards_into(state, data_shards, block_size, remaining, &mut output)?;
|
emit_data_shards_into(state, data_shards, block_size, remaining, &mut output)?;
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ where
|
|||||||
if total == 0 { Ok(None) } else { Ok(Some(total)) }
|
if total == 0 { Ok(None) } else { Ok(Some(total)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "byte accounting asserted by this file's tests (backlog#1823)")]
|
|
||||||
fn queued_block_bytes(block: &[Bytes]) -> usize {
|
fn queued_block_bytes(block: &[Bytes]) -> usize {
|
||||||
block.iter().map(Bytes::len).sum()
|
block.iter().map(Bytes::len).sum()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,16 +71,10 @@ impl EncodedBlock {
|
|||||||
|
|
||||||
const MODERN_MAX_TOTAL_SHARDS: usize = <reed_solomon_erasure::galois_8::Field as reed_solomon_erasure::Field>::ORDER;
|
const MODERN_MAX_TOTAL_SHARDS: usize = <reed_solomon_erasure::galois_8::Field as reed_solomon_erasure::Field>::ORDER;
|
||||||
const MODERN_REED_SOLOMON_CACHE_MAX_ENTRIES: usize = 64;
|
const MODERN_REED_SOLOMON_CACHE_MAX_ENTRIES: usize = 64;
|
||||||
const LEGACY_REED_SOLOMON_CACHE_MAX_ENTRIES: usize = 16;
|
|
||||||
// Vec growth may retain twice the requested logical length. Keeping the logical
|
|
||||||
// workspace at half the budget bounds each cached workspace's shard allocation to 1 MiB.
|
|
||||||
const LEGACY_REED_SOLOMON_CACHE_MAX_LOGICAL_SHARD_BYTES_PER_WORKSPACE: usize = 512 * 1024;
|
|
||||||
|
|
||||||
type ModernReedSolomonCache = RwLock<HashMap<(usize, usize), Arc<ReedSolomon>>>;
|
type ModernReedSolomonCache = RwLock<HashMap<(usize, usize), Arc<ReedSolomon>>>;
|
||||||
type LegacyReedSolomonCache = RwLock<HashMap<(usize, usize), Arc<LegacyReedSolomonEncoder>>>;
|
|
||||||
|
|
||||||
static MODERN_REED_SOLOMON_CACHE: OnceLock<ModernReedSolomonCache> = OnceLock::new();
|
static MODERN_REED_SOLOMON_CACHE: OnceLock<ModernReedSolomonCache> = OnceLock::new();
|
||||||
static LEGACY_REED_SOLOMON_CACHE: OnceLock<LegacyReedSolomonCache> = OnceLock::new();
|
|
||||||
|
|
||||||
/// Errors returned when constructing an [`Erasure`] codec.
|
/// Errors returned when constructing an [`Erasure`] codec.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
@@ -147,61 +141,43 @@ pub fn calc_shard_size_legacy(block_size: usize, data_shards: usize) -> usize {
|
|||||||
struct LegacyReedSolomonEncoder {
|
struct LegacyReedSolomonEncoder {
|
||||||
data_shards: usize,
|
data_shards: usize,
|
||||||
parity_shards: usize,
|
parity_shards: usize,
|
||||||
cache_workspaces: bool,
|
encoder_cache: std::sync::RwLock<Option<reed_solomon_simd::ReedSolomonEncoder>>,
|
||||||
encoder_cache: RwLock<Option<reed_solomon_simd::ReedSolomonEncoder>>,
|
decoder_cache: std::sync::RwLock<Option<reed_solomon_simd::ReedSolomonDecoder>>,
|
||||||
decoder_cache: RwLock<Option<reed_solomon_simd::ReedSolomonDecoder>>,
|
}
|
||||||
|
|
||||||
|
impl Clone for LegacyReedSolomonEncoder {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
data_shards: self.data_shards,
|
||||||
|
parity_shards: self.parity_shards,
|
||||||
|
encoder_cache: std::sync::RwLock::new(None),
|
||||||
|
decoder_cache: std::sync::RwLock::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LegacyReedSolomonEncoder {
|
impl LegacyReedSolomonEncoder {
|
||||||
fn new(data_shards: usize, parity_shards: usize) -> io::Result<Self> {
|
fn new(_data_shards: usize, _parity_shards: usize) -> io::Result<Self> {
|
||||||
Self::with_workspace_cache(data_shards, parity_shards, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_workspace_cache(data_shards: usize, parity_shards: usize, cache_workspaces: bool) -> io::Result<Self> {
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
data_shards,
|
data_shards: _data_shards,
|
||||||
parity_shards,
|
parity_shards: _parity_shards,
|
||||||
cache_workspaces,
|
encoder_cache: std::sync::RwLock::new(None),
|
||||||
encoder_cache: RwLock::new(None),
|
decoder_cache: std::sync::RwLock::new(None),
|
||||||
decoder_cache: RwLock::new(None),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn logical_shard_bytes_upper_bound(&self, shard_len: usize) -> Option<usize> {
|
|
||||||
let aligned_shard_len = shard_len.checked_add(63)?.checked_div(64)?.checked_mul(64)?;
|
|
||||||
let high_rate_decoder_work_count = self
|
|
||||||
.parity_shards
|
|
||||||
.checked_next_power_of_two()?
|
|
||||||
.checked_add(self.data_shards)?
|
|
||||||
.checked_next_power_of_two()?;
|
|
||||||
let low_rate_decoder_work_count = self
|
|
||||||
.data_shards
|
|
||||||
.checked_next_power_of_two()?
|
|
||||||
.checked_add(self.parity_shards)?
|
|
||||||
.checked_next_power_of_two()?;
|
|
||||||
aligned_shard_len.checked_mul(high_rate_decoder_work_count.max(low_rate_decoder_work_count))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_cache_workspace(&self, shard_len: usize) -> bool {
|
|
||||||
self.cache_workspaces
|
|
||||||
&& self
|
|
||||||
.logical_shard_bytes_upper_bound(shard_len)
|
|
||||||
.is_some_and(|bytes| bytes <= LEGACY_REED_SOLOMON_CACHE_MAX_LOGICAL_SHARD_BYTES_PER_WORKSPACE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode(&self, shards: SmallVec<[&mut [u8]; 16]>) -> io::Result<()> {
|
fn encode(&self, shards: SmallVec<[&mut [u8]; 16]>) -> io::Result<()> {
|
||||||
let mut shards_vec: Vec<&mut [u8]> = shards.into_vec();
|
let mut shards_vec: Vec<&mut [u8]> = shards.into_vec();
|
||||||
if shards_vec.is_empty() {
|
if shards_vec.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let shard_len = shards_vec[0].len();
|
let shard_len = shards_vec[0].len();
|
||||||
let cached_encoder = self
|
|
||||||
.encoder_cache
|
|
||||||
.write()
|
|
||||||
.map_err(|_| io::Error::other("Failed to acquire encoder cache lock"))?
|
|
||||||
.take();
|
|
||||||
let mut encoder = {
|
let mut encoder = {
|
||||||
match cached_encoder {
|
let mut cache_guard = self
|
||||||
|
.encoder_cache
|
||||||
|
.write()
|
||||||
|
.map_err(|_| io::Error::other("Failed to acquire encoder cache lock"))?;
|
||||||
|
match cache_guard.take() {
|
||||||
Some(mut cached) => {
|
Some(mut cached) => {
|
||||||
if cached.reset(self.data_shards, self.parity_shards, shard_len).is_err() {
|
if cached.reset(self.data_shards, self.parity_shards, shard_len).is_err() {
|
||||||
reed_solomon_simd::ReedSolomonEncoder::new(self.data_shards, self.parity_shards, shard_len)
|
reed_solomon_simd::ReedSolomonEncoder::new(self.data_shards, self.parity_shards, shard_len)
|
||||||
@@ -228,15 +204,10 @@ impl LegacyReedSolomonEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
drop(result);
|
drop(result);
|
||||||
if self.should_cache_workspace(shard_len) {
|
*self
|
||||||
let mut cache = self
|
.encoder_cache
|
||||||
.encoder_cache
|
.write()
|
||||||
.write()
|
.map_err(|_| io::Error::other("Failed to return encoder to cache"))? = Some(encoder);
|
||||||
.map_err(|_| io::Error::other("Failed to return encoder to cache"))?;
|
|
||||||
if cache.is_none() {
|
|
||||||
*cache = Some(encoder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,13 +221,13 @@ impl LegacyReedSolomonEncoder {
|
|||||||
.find_map(|s| s.as_ref().map(|v| v.len()))
|
.find_map(|s| s.as_ref().map(|v| v.len()))
|
||||||
.ok_or_else(|| io::Error::other("No valid shards found for reconstruction"))?;
|
.ok_or_else(|| io::Error::other("No valid shards found for reconstruction"))?;
|
||||||
|
|
||||||
let cached_decoder = self
|
|
||||||
.decoder_cache
|
|
||||||
.write()
|
|
||||||
.map_err(|_| io::Error::other("Failed to acquire decoder cache lock"))?
|
|
||||||
.take();
|
|
||||||
let mut decoder = {
|
let mut decoder = {
|
||||||
match cached_decoder {
|
let mut cache_guard = self
|
||||||
|
.decoder_cache
|
||||||
|
.write()
|
||||||
|
.map_err(|_| io::Error::other("Failed to acquire decoder cache lock"))?;
|
||||||
|
|
||||||
|
match cache_guard.take() {
|
||||||
Some(mut cached_decoder) => {
|
Some(mut cached_decoder) => {
|
||||||
if let Err(e) = cached_decoder.reset(self.data_shards, self.parity_shards, shard_len) {
|
if let Err(e) = cached_decoder.reset(self.data_shards, self.parity_shards, shard_len) {
|
||||||
warn!("Failed to reset SIMD decoder: {:?}, creating new one", e);
|
warn!("Failed to reset SIMD decoder: {:?}, creating new one", e);
|
||||||
@@ -303,15 +274,10 @@ impl LegacyReedSolomonEncoder {
|
|||||||
|
|
||||||
drop(result);
|
drop(result);
|
||||||
|
|
||||||
if self.should_cache_workspace(shard_len) {
|
*self
|
||||||
let mut cache = self
|
.decoder_cache
|
||||||
.decoder_cache
|
.write()
|
||||||
.write()
|
.map_err(|_| io::Error::other("Failed to return decoder to cache"))? = Some(decoder);
|
||||||
.map_err(|_| io::Error::other("Failed to return decoder to cache"))?;
|
|
||||||
if cache.is_none() {
|
|
||||||
*cache = Some(decoder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -469,39 +435,6 @@ fn cached_modern_reed_solomon(data_shards: usize, parity_shards: usize) -> Resul
|
|||||||
Ok(encoder)
|
Ok(encoder)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cached_legacy_reed_solomon(data_shards: usize, parity_shards: usize) -> io::Result<Arc<LegacyReedSolomonEncoder>> {
|
|
||||||
let cache = LEGACY_REED_SOLOMON_CACHE.get_or_init(|| RwLock::new(HashMap::new()));
|
|
||||||
cached_legacy_reed_solomon_in(cache, data_shards, parity_shards)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cached_legacy_reed_solomon_in(
|
|
||||||
cache: &LegacyReedSolomonCache,
|
|
||||||
data_shards: usize,
|
|
||||||
parity_shards: usize,
|
|
||||||
) -> io::Result<Arc<LegacyReedSolomonEncoder>> {
|
|
||||||
let key = (data_shards, parity_shards);
|
|
||||||
if let Some(encoder) = cache
|
|
||||||
.read()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
||||||
.get(&key)
|
|
||||||
.cloned()
|
|
||||||
{
|
|
||||||
return Ok(encoder);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut cache = cache.write().unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
||||||
if let Some(existing) = cache.get(&key) {
|
|
||||||
return Ok(Arc::clone(existing));
|
|
||||||
}
|
|
||||||
if cache.len() < LEGACY_REED_SOLOMON_CACHE_MAX_ENTRIES {
|
|
||||||
let encoder = Arc::new(LegacyReedSolomonEncoder::with_workspace_cache(data_shards, parity_shards, true)?);
|
|
||||||
cache.insert(key, Arc::clone(&encoder));
|
|
||||||
return Ok(encoder);
|
|
||||||
}
|
|
||||||
drop(cache);
|
|
||||||
Ok(Arc::new(LegacyReedSolomonEncoder::new(data_shards, parity_shards)?))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode_parity_shards<F>(shards: &mut [Option<Vec<u8>>], data_shards: usize, parity_shards: usize, encode: F) -> io::Result<()>
|
fn encode_parity_shards<F>(shards: &mut [Option<Vec<u8>>], data_shards: usize, parity_shards: usize, encode: F) -> io::Result<()>
|
||||||
where
|
where
|
||||||
F: FnOnce(SmallVec<[&mut [u8]; 16]>) -> io::Result<()>,
|
F: FnOnce(SmallVec<[&mut [u8]; 16]>) -> io::Result<()>,
|
||||||
@@ -618,7 +551,7 @@ pub struct Erasure {
|
|||||||
pub data_shards: usize,
|
pub data_shards: usize,
|
||||||
pub parity_shards: usize,
|
pub parity_shards: usize,
|
||||||
encoder: Option<ReedSolomonEncoder>,
|
encoder: Option<ReedSolomonEncoder>,
|
||||||
legacy_encoder: Option<Arc<LegacyReedSolomonEncoder>>,
|
legacy_encoder: Option<LegacyReedSolomonEncoder>,
|
||||||
pub block_size: usize,
|
pub block_size: usize,
|
||||||
uses_legacy: bool,
|
uses_legacy: bool,
|
||||||
_id: Uuid,
|
_id: Uuid,
|
||||||
@@ -754,7 +687,7 @@ impl Erasure {
|
|||||||
|
|
||||||
let legacy_encoder = if uses_legacy && parity_shards > 0 {
|
let legacy_encoder = if uses_legacy && parity_shards > 0 {
|
||||||
Some(
|
Some(
|
||||||
cached_legacy_reed_solomon(data_shards, parity_shards)
|
LegacyReedSolomonEncoder::new(data_shards, parity_shards)
|
||||||
.map_err(|source| ErasureConstructionError::LegacyEncoder { source })?,
|
.map_err(|source| ErasureConstructionError::LegacyEncoder { source })?,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -1110,10 +1043,6 @@ impl Erasure {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns error if reading from reader fails or if callback returns error
|
/// Returns error if reading from reader fails or if callback returns error
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "callback encode path exercised only by this file's tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) async fn encode_stream_callback_async<F, Fut, E, R>(
|
pub(crate) async fn encode_stream_callback_async<F, Fut, E, R>(
|
||||||
self: std::sync::Arc<Self>,
|
self: std::sync::Arc<Self>,
|
||||||
reader: &mut R,
|
reader: &mut R,
|
||||||
@@ -1476,7 +1405,7 @@ mod tests {
|
|||||||
assert_eq!(cloned.block_size, legacy.block_size);
|
assert_eq!(cloned.block_size, legacy.block_size);
|
||||||
assert!(cloned.uses_legacy);
|
assert!(cloned.uses_legacy);
|
||||||
|
|
||||||
let data = b"legacy clone should preserve SIMD codec behavior";
|
let data = b"legacy clone should keep independent SIMD caches";
|
||||||
let encoded = cloned.encode_data(data).expect("legacy clone should encode");
|
let encoded = cloned.encode_data(data).expect("legacy clone should encode");
|
||||||
let mut shards = optional_shards(&encoded);
|
let mut shards = optional_shards(&encoded);
|
||||||
shards[0] = None;
|
shards[0] = None;
|
||||||
@@ -1484,93 +1413,6 @@ mod tests {
|
|||||||
assert_eq!(recover_data(&shards, cloned.data_shards, data.len()), data);
|
assert_eq!(recover_data(&shards, cloned.data_shards, data.len()), data);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn legacy_codecs_share_process_cache_across_erasure_instances() {
|
|
||||||
let first = Erasure::new_with_options(6, 3, 64, true)
|
|
||||||
.legacy_encoder
|
|
||||||
.expect("legacy codec should be initialized");
|
|
||||||
let second = Erasure::new_with_options(6, 3, 128, true)
|
|
||||||
.legacy_encoder
|
|
||||||
.expect("same legacy shard layout should be initialized");
|
|
||||||
|
|
||||||
assert!(Arc::ptr_eq(&first, &second));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn legacy_workspace_cache_rejects_oversize_buffers_and_isolates_layouts() {
|
|
||||||
let four_plus_two = Erasure::new_with_options(4, 2, 64, true)
|
|
||||||
.legacy_encoder
|
|
||||||
.expect("legacy codec should be initialized");
|
|
||||||
let four_plus_one = Erasure::new_with_options(4, 1, 64, true)
|
|
||||||
.legacy_encoder
|
|
||||||
.expect("distinct parity layout should be initialized");
|
|
||||||
let three_plus_two = Erasure::new_with_options(3, 2, 64, true)
|
|
||||||
.legacy_encoder
|
|
||||||
.expect("distinct data layout should be initialized");
|
|
||||||
|
|
||||||
assert!(!Arc::ptr_eq(&four_plus_two, &four_plus_one));
|
|
||||||
assert!(!Arc::ptr_eq(&four_plus_two, &three_plus_two));
|
|
||||||
assert_eq!(four_plus_two.logical_shard_bytes_upper_bound(64 * 1024), Some(512 * 1024));
|
|
||||||
assert!(four_plus_two.should_cache_workspace(64 * 1024));
|
|
||||||
assert!(!four_plus_two.should_cache_workspace(64 * 1024 + 1));
|
|
||||||
|
|
||||||
let nine_plus_seven =
|
|
||||||
LegacyReedSolomonEncoder::with_workspace_cache(9, 7, true).expect("9+7 legacy codec should construct");
|
|
||||||
assert_eq!(nine_plus_seven.logical_shard_bytes_upper_bound(16 * 1024), Some(512 * 1024));
|
|
||||||
assert!(nine_plus_seven.should_cache_workspace(16 * 1024));
|
|
||||||
assert!(!nine_plus_seven.should_cache_workspace(16 * 1024 + 1));
|
|
||||||
|
|
||||||
let uncached = LegacyReedSolomonEncoder::new(4, 2).expect("uncached legacy codec should construct");
|
|
||||||
assert!(!uncached.should_cache_workspace(64));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn saturated_legacy_codec_cache_does_not_retain_more_workspaces() {
|
|
||||||
let cache = RwLock::new(HashMap::new());
|
|
||||||
for parity_shards in 1..=LEGACY_REED_SOLOMON_CACHE_MAX_ENTRIES {
|
|
||||||
let cached =
|
|
||||||
cached_legacy_reed_solomon_in(&cache, 32, parity_shards).expect("cacheable legacy codec should construct");
|
|
||||||
assert!(cached.cache_workspaces);
|
|
||||||
}
|
|
||||||
|
|
||||||
let uncached =
|
|
||||||
cached_legacy_reed_solomon_in(&cache, 31, 1).expect("uncached legacy codec should construct after saturation");
|
|
||||||
assert!(!uncached.cache_workspaces);
|
|
||||||
assert_eq!(
|
|
||||||
cache.read().expect("cache lock should remain healthy").len(),
|
|
||||||
LEGACY_REED_SOLOMON_CACHE_MAX_ENTRIES
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn concurrent_legacy_codecs_preserve_byte_exact_results() {
|
|
||||||
let barrier = Arc::new(std::sync::Barrier::new(2));
|
|
||||||
let payloads = [vec![0x35; 257], vec![0xca; 1025]];
|
|
||||||
|
|
||||||
std::thread::scope(|scope| {
|
|
||||||
let handles = payloads.each_ref().map(|payload| {
|
|
||||||
let barrier = Arc::clone(&barrier);
|
|
||||||
scope.spawn(move || {
|
|
||||||
let erasure = Erasure::new_with_options(6, 3, 2048, true);
|
|
||||||
barrier.wait();
|
|
||||||
let encoded = erasure.encode_data(payload).expect("concurrent legacy encode should succeed");
|
|
||||||
barrier.wait();
|
|
||||||
|
|
||||||
let mut shards = optional_shards(&encoded);
|
|
||||||
shards[0] = None;
|
|
||||||
erasure
|
|
||||||
.decode_data(&mut shards)
|
|
||||||
.expect("concurrent legacy decode should reconstruct the missing shard");
|
|
||||||
recover_data(&shards, erasure.data_shards, payload.len())
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
for (handle, payload) in handles.into_iter().zip(payloads.iter()) {
|
|
||||||
assert_eq!(handle.join().expect("concurrent legacy codec worker should not panic"), *payload);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn legacy_verify_reports_invalid_empty_valid_and_corrupt_parity_sets() {
|
fn legacy_verify_reports_invalid_empty_valid_and_corrupt_parity_sets() {
|
||||||
let legacy = LegacyReedSolomonEncoder::new(2, 2).expect("legacy encoder should construct");
|
let legacy = LegacyReedSolomonEncoder::new(2, 2).expect("legacy encoder should construct");
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: erasure codec migration keeps staged streaming decode paths in this module.
|
// #730: erasure codec migration keeps staged streaming decode paths in this module.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
pub(crate) mod codec;
|
pub(crate) mod codec;
|
||||||
pub(crate) mod coding;
|
pub(crate) mod coding;
|
||||||
|
|||||||
@@ -13,12 +13,13 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: error taxonomy still exposes compatibility variants while callers move to contracts.
|
// #730: error taxonomy still exposes compatibility variants while callers move to contracts.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use crate::bucket::error::BucketMetadataError;
|
use crate::bucket::error::BucketMetadataError;
|
||||||
use crate::disk::error::DiskError;
|
use crate::disk::error::DiskError;
|
||||||
use crate::storage_api_contracts::{error::StorageErrorCode, range::HTTPRangeError};
|
use crate::storage_api_contracts::{error::StorageErrorCode, range::HTTPRangeError};
|
||||||
use rustfs_utils::path::decode_dir_object;
|
use rustfs_utils::path::decode_dir_object;
|
||||||
use s3s::S3ErrorCode;
|
use s3s::{S3Error, S3ErrorCode};
|
||||||
|
|
||||||
pub type Error = StorageError;
|
pub type Error = StorageError;
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
pub type Result<T> = core::result::Result<T, Error>;
|
||||||
@@ -901,7 +902,6 @@ pub fn is_err_decommission_running(err: &Error) -> bool {
|
|||||||
matches!(err, &StorageError::DecommissionAlreadyRunning)
|
matches!(err, &StorageError::DecommissionAlreadyRunning)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "predicate asserted by this file's tests (backlog#1823)")]
|
|
||||||
pub fn is_err_rebalance_running(err: &Error) -> bool {
|
pub fn is_err_rebalance_running(err: &Error) -> bool {
|
||||||
matches!(err, &StorageError::RebalanceAlreadyRunning)
|
matches!(err, &StorageError::RebalanceAlreadyRunning)
|
||||||
}
|
}
|
||||||
@@ -910,11 +910,14 @@ pub fn is_err_operation_canceled(err: &Error) -> bool {
|
|||||||
matches!(err, &StorageError::OperationCanceled)
|
matches!(err, &StorageError::OperationCanceled)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "predicate asserted by this file's tests (backlog#1823)")]
|
|
||||||
pub fn is_err_not_initialized(err: &Error) -> bool {
|
pub fn is_err_not_initialized(err: &Error) -> bool {
|
||||||
err.to_string().contains("errServerNotInitialized") || err.to_string().contains("ServerNotInitialized")
|
err.to_string().contains("errServerNotInitialized") || err.to_string().contains("ServerNotInitialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_err_io(err: &Error) -> bool {
|
||||||
|
matches!(err, &StorageError::Io(_))
|
||||||
|
}
|
||||||
|
|
||||||
/// Strict "not found" predicate that only matches genuine object/version/volume
|
/// Strict "not found" predicate that only matches genuine object/version/volume
|
||||||
/// absence errors: `FileNotFound`/`VolumeNotFound`/`FileVersionNotFound`/
|
/// absence errors: `FileNotFound`/`VolumeNotFound`/`FileVersionNotFound`/
|
||||||
/// `ObjectNotFound`/`VersionNotFound`.
|
/// `ObjectNotFound`/`VersionNotFound`.
|
||||||
@@ -1075,6 +1078,21 @@ pub struct GenericError {
|
|||||||
|
|
||||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||||
pub enum ObjectApiError {
|
pub enum ObjectApiError {
|
||||||
|
#[error("Operation timed out")]
|
||||||
|
OperationTimedOut,
|
||||||
|
|
||||||
|
#[error("etag of the object has changed")]
|
||||||
|
InvalidETag,
|
||||||
|
|
||||||
|
#[error("BackendDown")]
|
||||||
|
BackendDown(String),
|
||||||
|
|
||||||
|
#[error("Unsupported headers in Metadata")]
|
||||||
|
UnsupportedMetadata,
|
||||||
|
|
||||||
|
#[error("Method not allowed: {}/{}", .0.bucket, .0.object)]
|
||||||
|
MethodNotAllowed(GenericError),
|
||||||
|
|
||||||
#[error("The operation is not valid for the current state of the object {}/{}({})", .0.bucket, .0.object, .0.version_id)]
|
#[error("The operation is not valid for the current state of the object {}/{}({})", .0.bucket, .0.object, .0.version_id)]
|
||||||
InvalidObjectState(GenericError),
|
InvalidObjectState(GenericError),
|
||||||
}
|
}
|
||||||
@@ -1091,6 +1109,96 @@ pub struct ErrorResponse {
|
|||||||
pub host_id: String,
|
pub host_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn error_resp_to_object_err(err: ErrorResponse, params: Vec<&str>) -> std::io::Error {
|
||||||
|
let mut bucket = "";
|
||||||
|
let mut object = "";
|
||||||
|
let mut version_id = "";
|
||||||
|
if !params.is_empty() {
|
||||||
|
bucket = params[0];
|
||||||
|
}
|
||||||
|
if params.len() >= 2 {
|
||||||
|
object = params[1];
|
||||||
|
}
|
||||||
|
if params.len() >= 3 {
|
||||||
|
version_id = params[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_network_or_host_down(&err.to_string(), false) {
|
||||||
|
return std::io::Error::other(ObjectApiError::BackendDown(format!("{err}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let err_ = std::io::Error::other(err.to_string());
|
||||||
|
let r_err = err;
|
||||||
|
let err;
|
||||||
|
let bucket = bucket.to_string();
|
||||||
|
let object = object.to_string();
|
||||||
|
let version_id = version_id.to_string();
|
||||||
|
|
||||||
|
match r_err.code {
|
||||||
|
S3ErrorCode::BucketNotEmpty => {
|
||||||
|
err = std::io::Error::other(StorageError::BucketNotEmpty("".to_string()).to_string());
|
||||||
|
}
|
||||||
|
S3ErrorCode::InvalidBucketName => {
|
||||||
|
err = std::io::Error::other(StorageError::BucketNameInvalid(bucket));
|
||||||
|
}
|
||||||
|
S3ErrorCode::InvalidPart => {
|
||||||
|
err = std::io::Error::other(StorageError::InvalidPart(0, bucket, object /* , version_id */));
|
||||||
|
}
|
||||||
|
S3ErrorCode::NoSuchBucket => {
|
||||||
|
err = std::io::Error::other(StorageError::BucketNotFound(bucket));
|
||||||
|
}
|
||||||
|
S3ErrorCode::NoSuchKey => {
|
||||||
|
if !object.is_empty() {
|
||||||
|
err = std::io::Error::other(StorageError::ObjectNotFound(bucket, object));
|
||||||
|
} else {
|
||||||
|
err = std::io::Error::other(StorageError::BucketNotFound(bucket));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
S3ErrorCode::NoSuchVersion => {
|
||||||
|
if !object.is_empty() {
|
||||||
|
err = std::io::Error::other(StorageError::ObjectNotFound(bucket, object)); //, version_id);
|
||||||
|
} else {
|
||||||
|
err = std::io::Error::other(StorageError::BucketNotFound(bucket));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
S3ErrorCode::AccessDenied => {
|
||||||
|
err = std::io::Error::other(StorageError::PrefixAccessDenied(bucket, object));
|
||||||
|
}
|
||||||
|
S3ErrorCode::NoSuchUpload => {
|
||||||
|
err = std::io::Error::other(StorageError::InvalidUploadID(bucket, object, version_id));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
err = err_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn storage_to_object_err(err: Error, params: Vec<&str>) -> S3Error {
|
||||||
|
let storage_err = &err;
|
||||||
|
let mut bucket: String = "".to_string();
|
||||||
|
let mut object: String = "".to_string();
|
||||||
|
if !params.is_empty() {
|
||||||
|
bucket = params[0].to_string();
|
||||||
|
}
|
||||||
|
if params.len() >= 2 {
|
||||||
|
object = decode_dir_object(params[1]);
|
||||||
|
}
|
||||||
|
match storage_err {
|
||||||
|
StorageError::MethodNotAllowed => S3Error::with_message(
|
||||||
|
S3ErrorCode::MethodNotAllowed,
|
||||||
|
ObjectApiError::MethodNotAllowed(GenericError {
|
||||||
|
bucket,
|
||||||
|
object,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
_ => s3s::S3Error::with_message(S3ErrorCode::Custom("err".into()), err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: event target types are retained for notification owner migration.
|
// #730: event target types are retained for notification owner migration.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
pub mod name;
|
pub mod name;
|
||||||
|
pub mod targetid;
|
||||||
pub mod targetlist;
|
pub mod targetlist;
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
pub struct TargetID {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TargetID {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
format!("{}:{}", self.id, self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,20 +12,18 @@
|
|||||||
// 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 crate::event::targetid::TargetID;
|
||||||
use std::sync::atomic::AtomicI64;
|
use std::sync::atomic::AtomicI64;
|
||||||
|
|
||||||
/// Placeholder notification target list held by `EventNotifier`.
|
|
||||||
///
|
|
||||||
/// The working notification stack lives in `rustfs-notify` / `rustfs-targets`;
|
|
||||||
/// this type never grew past its counter. `total_events` is read by the
|
|
||||||
/// notifier's log line but nothing increments it, so that field reports zero.
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "held only by the dead ecstore EventNotifier; see services/event_notification.rs (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub struct TargetList {
|
pub struct TargetList {
|
||||||
|
pub current_send_calls: AtomicI64,
|
||||||
pub total_events: AtomicI64,
|
pub total_events: AtomicI64,
|
||||||
|
pub events_skipped: AtomicI64,
|
||||||
|
pub events_errors_total: AtomicI64,
|
||||||
|
//pub targets: HashMap<TargetID, Target>,
|
||||||
|
//pub queue: AsyncEvent,
|
||||||
|
//pub targetStats: HashMap<TargetID, TargetStat>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TargetList {
|
impl TargetList {
|
||||||
@@ -33,3 +31,14 @@ impl TargetList {
|
|||||||
TargetList::default()
|
TargetList::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TargetStat {
|
||||||
|
current_send_calls: i64,
|
||||||
|
total_events: i64,
|
||||||
|
failed_events: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TargetIDResult {
|
||||||
|
id: TargetID,
|
||||||
|
err: std::io::Error,
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,13 +31,6 @@ pub const ENV_DISK_COMPRESSION_MIME_TYPES: &str = "RUSTFS_COMPRESSION_MIME_TYPES
|
|||||||
// Environment variable for additional extensions to exclude from compression (comma-separated, e.g. ".foo,.bar")
|
// Environment variable for additional extensions to exclude from compression (comma-separated, e.g. ".foo,.bar")
|
||||||
pub const ENV_ADDED_EXCLUDE_COMPRESS_EXTENSIONS: &str = "RUSTFS_ADDED_EXCLUDE_COMPRESS_EXTENSIONS";
|
pub const ENV_ADDED_EXCLUDE_COMPRESS_EXTENSIONS: &str = "RUSTFS_ADDED_EXCLUDE_COMPRESS_EXTENSIONS";
|
||||||
|
|
||||||
// Environment variable to additionally enable disk compression for multipart uploads.
|
|
||||||
// Default off: nodes from before the resumable decompressor fix fail transient reads of
|
|
||||||
// compressed objects, so multipart compression stays dark until the operator confirms the
|
|
||||||
// fleet has converged on a fixed build.
|
|
||||||
// RUSTFS_COMPAT_TODO(multipart-compression-default-off-window): staged rollout switch for restored multipart compression, flipping the default to enabled on retirement. Remove after the minimum supported direct-upgrade release ships the resumable DecompressReader.
|
|
||||||
pub const ENV_DISK_COMPRESSION_MULTIPART_ENABLED: &str = "RUSTFS_COMPRESSION_MULTIPART_ENABLED";
|
|
||||||
|
|
||||||
pub const DEFAULT_DISK_COMPRESS_EXTENSIONS: &str = ".txt,.log,.csv,.json,.tar,.xml,.bin";
|
pub const DEFAULT_DISK_COMPRESS_EXTENSIONS: &str = ".txt,.log,.csv,.json,.tar,.xml,.bin";
|
||||||
pub const DEFAULT_DISK_COMPRESS_MIME_TYPES: &str = "text/*,application/json,application/xml,binary/octet-stream";
|
pub const DEFAULT_DISK_COMPRESS_MIME_TYPES: &str = "text/*,application/json,application/xml,binary/octet-stream";
|
||||||
|
|
||||||
@@ -178,21 +171,6 @@ pub fn is_disk_compression_enabled() -> bool {
|
|||||||
DISK_COMPRESSION_CONFIG.get_or_init(parse_disk_compression_config).enabled
|
DISK_COMPRESSION_CONFIG.get_or_init(parse_disk_compression_config).enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parsed once at first use, mirroring DISK_COMPRESSION_CONFIG.
|
|
||||||
static MULTIPART_DISK_COMPRESSION_ENABLED: OnceLock<bool> = OnceLock::new();
|
|
||||||
|
|
||||||
/// Whether multipart uploads may advertise disk compression. Requires the
|
|
||||||
/// regular disk-compression gates to pass as well; this is the staged-rollout
|
|
||||||
/// switch that keeps multipart compression dark during rolling upgrades from
|
|
||||||
/// builds whose decompressor was not yet resumable.
|
|
||||||
pub fn is_multipart_disk_compression_enabled() -> bool {
|
|
||||||
*MULTIPART_DISK_COMPRESSION_ENABLED.get_or_init(|| {
|
|
||||||
env::var(ENV_DISK_COMPRESSION_MULTIPART_ENABLED)
|
|
||||||
.map(|s| matches!(s.to_ascii_lowercase().as_str(), "true" | "on" | "1"))
|
|
||||||
.unwrap_or(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_disk_compressible_with_config(headers: &http::HeaderMap, object_name: &str, config: &DiskCompressionConfig) -> bool {
|
fn is_disk_compressible_with_config(headers: &http::HeaderMap, object_name: &str, config: &DiskCompressionConfig) -> bool {
|
||||||
// Check if disk compression is enabled (read once at first use, then fixed for process lifetime)
|
// Check if disk compression is enabled (read once at first use, then fixed for process lifetime)
|
||||||
if !config.enabled {
|
if !config.enabled {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: I/O backend selection keeps test-only and staged rio helpers scoped here.
|
// #730: I/O backend selection keeps test-only and staged rio helpers scoped here.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
pub(crate) mod bitrot;
|
pub(crate) mod bitrot;
|
||||||
pub(crate) mod compress;
|
pub(crate) mod compress;
|
||||||
|
|||||||
@@ -25,20 +25,9 @@ use tokio::io::AsyncRead;
|
|||||||
|
|
||||||
#[cfg(feature = "rio-v2")]
|
#[cfg(feature = "rio-v2")]
|
||||||
const MINIO_S2_COMPRESSION_SCHEME: &str = "klauspost/compress/s2";
|
const MINIO_S2_COMPRESSION_SCHEME: &str = "klauspost/compress/s2";
|
||||||
// The S2 padding multiple rio-v2 pads compressed streams to before
|
|
||||||
// encryption. Only the padding test asserts it today, so the lib target sees
|
|
||||||
// it as unused (backlog#1823).
|
|
||||||
#[cfg(feature = "rio-v2")]
|
#[cfg(feature = "rio-v2")]
|
||||||
#[allow(dead_code, reason = "on-disk contract asserted by the rio-v2 padding test (backlog#1823)")]
|
|
||||||
const ENCRYPTED_S2_PADDING_MULTIPLE: usize = 256;
|
const ENCRYPTED_S2_PADDING_MULTIPLE: usize = 256;
|
||||||
|
|
||||||
/// Which rio implementation this build compiled in. Only the feature-seam
|
|
||||||
/// guard test in lib.rs reads it, so the lib target sees it as unused
|
|
||||||
/// (backlog#1823).
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "asserted by the rio backend feature-seam test in lib.rs (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub const fn backend_name() -> &'static str {
|
pub const fn backend_name() -> &'static str {
|
||||||
#[cfg(feature = "rio-v2")]
|
#[cfg(feature = "rio-v2")]
|
||||||
{
|
{
|
||||||
@@ -64,6 +53,17 @@ pub fn compression_metadata_value(algorithm: CompressionAlgorithm) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn compression_scheme_to_algorithm(scheme: &str) -> std::io::Result<CompressionAlgorithm> {
|
||||||
|
#[cfg(feature = "rio-v2")]
|
||||||
|
if scheme.eq_ignore_ascii_case(MINIO_S2_COMPRESSION_SCHEME) {
|
||||||
|
// rio_v2 currently routes all compressed-object handling through the S2
|
||||||
|
// reader implementation, so the enum is only a placeholder token here.
|
||||||
|
return Ok(CompressionAlgorithm::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
CompressionAlgorithm::from_str(scheme)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ReadCompressionBackend {
|
pub enum ReadCompressionBackend {
|
||||||
Legacy,
|
Legacy,
|
||||||
@@ -82,11 +82,6 @@ pub fn compression_scheme_to_read_plan(scheme: &str) -> std::io::Result<(Compres
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ReadEncryptionBackend {
|
pub enum ReadEncryptionBackend {
|
||||||
Legacy,
|
Legacy,
|
||||||
// Never constructed today — every read still selects Legacy — but the
|
|
||||||
// decrypt paths below carry live match arms for it. This is the rio-v2
|
|
||||||
// read seam (backlog#1638 / #1835), not dead code: deleting the variant
|
|
||||||
// would delete those arms with it.
|
|
||||||
#[allow(dead_code, reason = "rio-v2 read seam; match arms below are live (backlog#1823)")]
|
|
||||||
V2,
|
V2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,12 +209,15 @@ impl AsMut<Vec<Endpoints>> for PoolEndpointList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PoolEndpointList {
|
impl PoolEndpointList {
|
||||||
/// Creates a list of endpoints per pool, resolves their relevant hostnames
|
/// creates a list of endpoints per pool, resolves their relevant
|
||||||
/// and discovers whether those are local or remote.
|
/// hostnames and discovers those are local or remote.
|
||||||
///
|
async fn create_pool_endpoints(server_addr: &str, disks_layout: &DisksLayout) -> Result<Self> {
|
||||||
/// The policy and host overrides let tests inject an explicit startup
|
Self::create_pool_endpoints_with(server_addr, disks_layout, None, None).await
|
||||||
/// topology convergence policy and local endpoint host instead of
|
}
|
||||||
/// resolving them from the environment; production passes `None` for both.
|
|
||||||
|
/// Same as [`create_pool_endpoints`] but lets tests inject an explicit
|
||||||
|
/// startup topology convergence policy and local endpoint host instead of
|
||||||
|
/// resolving them from the environment.
|
||||||
async fn create_pool_endpoints_with(
|
async fn create_pool_endpoints_with(
|
||||||
server_addr: &str,
|
server_addr: &str,
|
||||||
disks_layout: &DisksLayout,
|
disks_layout: &DisksLayout,
|
||||||
@@ -591,10 +594,6 @@ impl PoolEndpointList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DNS_RETRY_BASE_DELAY: Duration = Duration::from_millis(500);
|
const DNS_RETRY_BASE_DELAY: Duration = Duration::from_millis(500);
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "retry-cap bound asserted by this file's dns_retry_delay tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
const DNS_RETRY_MAX_DELAY: Duration = Duration::from_secs(8);
|
const DNS_RETRY_MAX_DELAY: Duration = Duration::from_secs(8);
|
||||||
const DNS_RETRY_JITTER_PERCENT: u64 = 20;
|
const DNS_RETRY_JITTER_PERCENT: u64 = 20;
|
||||||
/// Minimum spacing between "still retrying" warnings so a long orchestrated
|
/// Minimum spacing between "still retrying" warnings so a long orchestrated
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: set-layout contracts are staged while ECStore ownership boundaries shrink.
|
// #730: set-layout contracts are staged while ECStore ownership boundaries shrink.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
//! Static ECStore layout boundaries.
|
//! Static ECStore layout boundaries.
|
||||||
//!
|
//!
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use std::io::{Error, Result};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
#[allow(dead_code, reason = "ESET-001 layout model; exercised by this file's tests (backlog#1823)")]
|
|
||||||
pub(crate) struct StaticSetLayoutSnapshot {
|
pub(crate) struct StaticSetLayoutSnapshot {
|
||||||
pub(crate) deployment_id: Uuid,
|
pub(crate) deployment_id: Uuid,
|
||||||
pub(crate) set_count: usize,
|
pub(crate) set_count: usize,
|
||||||
@@ -13,7 +12,6 @@ pub(crate) struct StaticSetLayoutSnapshot {
|
|||||||
pub(crate) distribution_algo: DistributionAlgoVersion,
|
pub(crate) distribution_algo: DistributionAlgoVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "ESET-001 layout model; exercised by this file's tests (backlog#1823)")]
|
|
||||||
impl StaticSetLayoutSnapshot {
|
impl StaticSetLayoutSnapshot {
|
||||||
pub(crate) fn from_format(format: &FormatV3) -> Self {
|
pub(crate) fn from_format(format: &FormatV3) -> Self {
|
||||||
let disk_ids = format.erasure.sets.clone();
|
let disk_ids = format.erasure.sets.clone();
|
||||||
@@ -41,20 +39,17 @@ impl StaticSetLayoutSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
#[allow(dead_code, reason = "ESET-001 layout model; exercised by this file's tests (backlog#1823)")]
|
|
||||||
pub(crate) struct SetDiskPosition {
|
pub(crate) struct SetDiskPosition {
|
||||||
pub(crate) set_index: usize,
|
pub(crate) set_index: usize,
|
||||||
pub(crate) disk_index: usize,
|
pub(crate) disk_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
#[allow(dead_code, reason = "ESET-001 layout model; exercised by this file's tests (backlog#1823)")]
|
|
||||||
pub(crate) struct RuntimeSetLayoutPlan {
|
pub(crate) struct RuntimeSetLayoutPlan {
|
||||||
pub(crate) sets: Vec<Vec<RuntimeSetDrivePlan>>,
|
pub(crate) sets: Vec<Vec<RuntimeSetDrivePlan>>,
|
||||||
lock_hosts_by_set: Vec<Vec<String>>,
|
lock_hosts_by_set: Vec<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "ESET-001 layout model; exercised by this file's tests (backlog#1823)")]
|
|
||||||
impl RuntimeSetLayoutPlan {
|
impl RuntimeSetLayoutPlan {
|
||||||
pub(crate) fn from_endpoint_hosts<S>(set_count: usize, drives_per_set: usize, endpoint_hosts: &[S]) -> Result<Self>
|
pub(crate) fn from_endpoint_hosts<S>(set_count: usize, drives_per_set: usize, endpoint_hosts: &[S]) -> Result<Self>
|
||||||
where
|
where
|
||||||
@@ -113,7 +108,6 @@ impl RuntimeSetLayoutPlan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
#[allow(dead_code, reason = "ESET-001 layout model; exercised by this file's tests (backlog#1823)")]
|
|
||||||
pub(crate) struct RuntimeSetDrivePlan {
|
pub(crate) struct RuntimeSetDrivePlan {
|
||||||
pub(crate) set_index: usize,
|
pub(crate) set_index: usize,
|
||||||
pub(crate) disk_index: usize,
|
pub(crate) disk_index: usize,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: object API readers keep staged compatibility paths during facade migration.
|
// #730: object API readers keep staged compatibility paths during facade migration.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use crate::bucket::metadata_sys::get_versioning_config;
|
use crate::bucket::metadata_sys::get_versioning_config;
|
||||||
use crate::bucket::replication::{
|
use crate::bucket::replication::{
|
||||||
@@ -22,7 +23,7 @@ use crate::bucket::replication::{
|
|||||||
use crate::bucket::versioning::VersioningApi as _;
|
use crate::bucket::versioning::VersioningApi as _;
|
||||||
use crate::config::storageclass;
|
use crate::config::storageclass;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::io_support::rio::{HardLimitReader, HashReader};
|
use crate::io_support::rio::{HashReader, LimitReader};
|
||||||
use crate::storage_api_contracts::{
|
use crate::storage_api_contracts::{
|
||||||
lifecycle::{ExpirationOptions, TransitionedObject},
|
lifecycle::{ExpirationOptions, TransitionedObject},
|
||||||
range::HTTPRangeSpec,
|
range::HTTPRangeSpec,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::io_support::rio::Index;
|
use crate::io_support::rio::Index;
|
||||||
use std::mem::MaybeUninit;
|
|
||||||
|
|
||||||
#[cfg(feature = "rio-v2")]
|
#[cfg(feature = "rio-v2")]
|
||||||
const DARE_PAYLOAD_SIZE: i64 = 64 * 1024;
|
const DARE_PAYLOAD_SIZE: i64 = 64 * 1024;
|
||||||
@@ -449,16 +448,10 @@ impl GetObjectReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum ReadTransform {
|
enum ReadTransform {
|
||||||
// Written but never read by production code: the enclosing struct already
|
Plain {
|
||||||
// carries the same pair as `storage_offset`/`storage_length`. They survive
|
visible_offset: usize,
|
||||||
// as the read plan's test-visible record — four tests assert them by
|
visible_length: i64,
|
||||||
// literal pattern (`Plain { visible_offset: 6, visible_length: 4 }`), which
|
},
|
||||||
// rustc does not count as a read.
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "asserted by literal pattern in this file's read-plan tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
Plain { visible_offset: usize, visible_length: i64 },
|
|
||||||
Compressed {
|
Compressed {
|
||||||
algorithm: CompressionAlgorithm,
|
algorithm: CompressionAlgorithm,
|
||||||
backend: crate::io_support::rio::ReadCompressionBackend,
|
backend: crate::io_support::rio::ReadCompressionBackend,
|
||||||
@@ -479,15 +472,7 @@ enum ReadTransform {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How an object's stored bytes must be fetched and transformed to serve a
|
struct ReadPlan {
|
||||||
/// request.
|
|
||||||
///
|
|
||||||
/// Public so callers that fetch the stored bytes from somewhere other than the
|
|
||||||
/// local erasure set — the remote-tier read path — can position their own fetch
|
|
||||||
/// with [`ReadPlan::storage_offset`] / [`ReadPlan::storage_length`] and then
|
|
||||||
/// hand the resulting stream to [`ReadPlan::into_object_reader`], instead of
|
|
||||||
/// reimplementing the transform decisions (rustfs/rustfs#6025).
|
|
||||||
pub struct ReadPlan {
|
|
||||||
storage_offset: usize,
|
storage_offset: usize,
|
||||||
storage_length: i64,
|
storage_length: i64,
|
||||||
object_size: i64,
|
object_size: i64,
|
||||||
@@ -495,43 +480,6 @@ pub struct ReadPlan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ReadPlan {
|
impl ReadPlan {
|
||||||
/// Byte offset into the object's **stored** bytes where the fetch must
|
|
||||||
/// start. Encrypted and compressed objects address their storage in a
|
|
||||||
/// different coordinate system than the plaintext range the caller asked
|
|
||||||
/// for, which is exactly the distinction this plan resolves.
|
|
||||||
pub fn storage_offset(&self) -> usize {
|
|
||||||
self.storage_offset
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Number of **stored** bytes the fetch must deliver, in the same
|
|
||||||
/// coordinate system as [`Self::storage_offset`].
|
|
||||||
pub fn storage_length(&self) -> i64 {
|
|
||||||
self.storage_length
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build the plan for a request without consuming a stream, so a caller
|
|
||||||
/// that has to issue its own positioned fetch can read the offsets first.
|
|
||||||
pub async fn build_for_request(
|
|
||||||
rs: Option<HTTPRangeSpec>,
|
|
||||||
oi: &ObjectInfo,
|
|
||||||
opts: &ObjectOptions,
|
|
||||||
h: &HeaderMap<HeaderValue>,
|
|
||||||
resolver: Option<&dyn ObjectEncryptionResolver>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
Self::build_with_resolver(rs, oi, opts, h, resolver).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrap `reader` — the stored bytes this plan asked for, already positioned
|
|
||||||
/// at [`Self::storage_offset`] — in the transforms that turn them into the
|
|
||||||
/// bytes the caller requested.
|
|
||||||
pub fn into_object_reader(
|
|
||||||
self,
|
|
||||||
reader: Box<dyn AsyncRead + Unpin + Send + Sync>,
|
|
||||||
oi: &ObjectInfo,
|
|
||||||
) -> Result<GetObjectReader> {
|
|
||||||
self.into_reader(reader, oi).map(|(reader, _, _)| reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
async fn build(rs: Option<HTTPRangeSpec>, oi: &ObjectInfo, opts: &ObjectOptions, h: &HeaderMap<HeaderValue>) -> Result<Self> {
|
async fn build(rs: Option<HTTPRangeSpec>, oi: &ObjectInfo, opts: &ObjectOptions, h: &HeaderMap<HeaderValue>) -> Result<Self> {
|
||||||
Self::build_with_resolver(rs, oi, opts, h, Some(&tests::TEST_RESOLVER)).await
|
Self::build_with_resolver(rs, oi, opts, h, Some(&tests::TEST_RESOLVER)).await
|
||||||
@@ -545,17 +493,8 @@ impl ReadPlan {
|
|||||||
resolver: Option<&dyn ObjectEncryptionResolver>,
|
resolver: Option<&dyn ObjectEncryptionResolver>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let mut rs = rs;
|
let mut rs = rs;
|
||||||
// A part number addresses the object's PLAINTEXT bytes. A restore read
|
|
||||||
// serves the stored representation instead (see
|
|
||||||
// [`restore_request_active`]), where that synthesized range would be
|
|
||||||
// reinterpreted as a storage range and truncate an encrypted or
|
|
||||||
// compressed payload by exactly its encoding overhead — the copy-back
|
|
||||||
// then fails its length check partway through
|
|
||||||
// (rustfs/rustfs#6025). An explicit caller range is already in storage
|
|
||||||
// coordinates on that path and is still honored.
|
|
||||||
if let Some(part_number) = opts.part_number
|
if let Some(part_number) = opts.part_number
|
||||||
&& rs.is_none()
|
&& rs.is_none()
|
||||||
&& !restore_request_active(opts)
|
|
||||||
{
|
{
|
||||||
rs = http_range_spec_from_object_info(oi, part_number);
|
rs = http_range_spec_from_object_info(oi, part_number);
|
||||||
}
|
}
|
||||||
@@ -808,7 +747,7 @@ impl ReadPlan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Box::new(HardLimitReader::new(dec_reader, decompressed_length))
|
Box::new(LimitReader::new(dec_reader, total_plaintext_size))
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut object_info = oi.clone();
|
let mut object_info = oi.clone();
|
||||||
@@ -900,7 +839,7 @@ impl ReadPlan {
|
|||||||
)?;
|
)?;
|
||||||
Box::new(ranged_reader)
|
Box::new(ranged_reader)
|
||||||
} else {
|
} else {
|
||||||
Box::new(HardLimitReader::new(decompressed_reader, total_plaintext_size_i64))
|
Box::new(LimitReader::new(decompressed_reader, total_plaintext_size))
|
||||||
}
|
}
|
||||||
} else if plaintext_offset > 0 || plaintext_length != total_plaintext_size_i64 {
|
} else if plaintext_offset > 0 || plaintext_length != total_plaintext_size_i64 {
|
||||||
Box::new(RangedDecompressReader::new(
|
Box::new(RangedDecompressReader::new(
|
||||||
@@ -910,7 +849,7 @@ impl ReadPlan {
|
|||||||
total_plaintext_size,
|
total_plaintext_size,
|
||||||
)?)
|
)?)
|
||||||
} else {
|
} else {
|
||||||
Box::new(HardLimitReader::new(decrypted_reader, total_plaintext_size_i64))
|
Box::new(LimitReader::new(decrypted_reader, total_plaintext_size))
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut object_info = oi.clone();
|
let mut object_info = oi.clone();
|
||||||
@@ -983,7 +922,7 @@ struct SkipReader<R> {
|
|||||||
inner: R,
|
inner: R,
|
||||||
bytes_to_skip: usize,
|
bytes_to_skip: usize,
|
||||||
bytes_skipped: usize,
|
bytes_skipped: usize,
|
||||||
scratch: Box<[MaybeUninit<u8>]>,
|
scratch: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: AsyncRead + Unpin + Send + Sync> SkipReader<R> {
|
impl<R: AsyncRead + Unpin + Send + Sync> SkipReader<R> {
|
||||||
@@ -992,7 +931,7 @@ impl<R: AsyncRead + Unpin + Send + Sync> SkipReader<R> {
|
|||||||
inner,
|
inner,
|
||||||
bytes_to_skip,
|
bytes_to_skip,
|
||||||
bytes_skipped: 0,
|
bytes_skipped: 0,
|
||||||
scratch: Box::<[u8]>::new_uninit_slice(8192),
|
scratch: vec![0u8; 8192],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1004,7 +943,7 @@ impl<R: AsyncRead + Unpin + Send + Sync> AsyncRead for SkipReader<R> {
|
|||||||
while this.bytes_skipped < this.bytes_to_skip {
|
while this.bytes_skipped < this.bytes_to_skip {
|
||||||
let remaining = this.bytes_to_skip - this.bytes_skipped;
|
let remaining = this.bytes_to_skip - this.bytes_skipped;
|
||||||
let scratch_len = remaining.min(this.scratch.len());
|
let scratch_len = remaining.min(this.scratch.len());
|
||||||
let mut scratch_buf = ReadBuf::uninit(&mut this.scratch[..scratch_len]);
|
let mut scratch_buf = ReadBuf::new(&mut this.scratch[..scratch_len]);
|
||||||
match Pin::new(&mut this.inner).poll_read(cx, &mut scratch_buf) {
|
match Pin::new(&mut this.inner).poll_read(cx, &mut scratch_buf) {
|
||||||
Poll::Pending => return Poll::Pending,
|
Poll::Pending => return Poll::Pending,
|
||||||
Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
|
Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
|
||||||
@@ -1035,7 +974,7 @@ pub struct RangedDecompressReader<R: AsyncRead + Unpin + Send + Sync + 'static>
|
|||||||
target_length: usize,
|
target_length: usize,
|
||||||
current_offset: usize,
|
current_offset: usize,
|
||||||
bytes_returned: usize,
|
bytes_returned: usize,
|
||||||
scratch: Box<[MaybeUninit<u8>]>,
|
scratch: Vec<u8>,
|
||||||
drain_on_done: bool,
|
drain_on_done: bool,
|
||||||
drain_task: Option<tokio::task::JoinHandle<()>>,
|
drain_task: Option<tokio::task::JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
@@ -1073,7 +1012,7 @@ impl<R: AsyncRead + Unpin + Send + Sync + 'static> RangedDecompressReader<R> {
|
|||||||
target_length: actual_length,
|
target_length: actual_length,
|
||||||
current_offset: 0,
|
current_offset: 0,
|
||||||
bytes_returned: 0,
|
bytes_returned: 0,
|
||||||
scratch: Box::<[u8]>::new_uninit_slice(8192),
|
scratch: vec![0u8; 8192],
|
||||||
drain_on_done,
|
drain_on_done,
|
||||||
drain_task: None,
|
drain_task: None,
|
||||||
})
|
})
|
||||||
@@ -1123,7 +1062,7 @@ impl<R: AsyncRead + Unpin + Send + Sync + 'static> AsyncRead for RangedDecompres
|
|||||||
}
|
}
|
||||||
|
|
||||||
let scratch_len = std::cmp::min(this.scratch.len(), std::cmp::max(buf_capacity, 1));
|
let scratch_len = std::cmp::min(this.scratch.len(), std::cmp::max(buf_capacity, 1));
|
||||||
let mut temp_read_buf = ReadBuf::uninit(&mut this.scratch[..scratch_len]);
|
let mut temp_read_buf = ReadBuf::new(&mut this.scratch[..scratch_len]);
|
||||||
|
|
||||||
let Some(inner) = this.inner.as_mut() else {
|
let Some(inner) = this.inner.as_mut() else {
|
||||||
return Poll::Ready(Ok(()));
|
return Poll::Ready(Ok(()));
|
||||||
@@ -1175,8 +1114,7 @@ impl<R: AsyncRead + Unpin + Send + Sync + 'static> AsyncRead for RangedDecompres
|
|||||||
);
|
);
|
||||||
|
|
||||||
if bytes_to_return > 0 {
|
if bytes_to_return > 0 {
|
||||||
let data_slice =
|
let data_slice = &this.scratch[data_start_in_buffer..data_start_in_buffer + bytes_to_return];
|
||||||
&temp_read_buf.filled()[data_start_in_buffer..data_start_in_buffer + bytes_to_return];
|
|
||||||
buf.put_slice(data_slice);
|
buf.put_slice(data_slice);
|
||||||
this.bytes_returned += bytes_to_return;
|
this.bytes_returned += bytes_to_return;
|
||||||
|
|
||||||
@@ -1195,7 +1133,7 @@ impl<R: AsyncRead + Unpin + Send + Sync + 'static> AsyncRead for RangedDecompres
|
|||||||
std::cmp::min(n, std::cmp::min(buf.remaining(), this.target_length - this.bytes_returned));
|
std::cmp::min(n, std::cmp::min(buf.remaining(), this.target_length - this.bytes_returned));
|
||||||
|
|
||||||
if bytes_to_return > 0 {
|
if bytes_to_return > 0 {
|
||||||
buf.put_slice(&temp_read_buf.filled()[..bytes_to_return]);
|
buf.put_slice(&this.scratch[..bytes_to_return]);
|
||||||
this.bytes_returned += bytes_to_return;
|
this.bytes_returned += bytes_to_return;
|
||||||
|
|
||||||
tracing::trace!("Returned {} bytes at offset {}", bytes_to_return, old_offset);
|
tracing::trace!("Returned {} bytes at offset {}", bytes_to_return, old_offset);
|
||||||
@@ -1265,7 +1203,20 @@ impl<R: AsyncRead + Unpin + Send + 'static> AsyncRead for StreamConsumer<R> {
|
|||||||
|
|
||||||
impl<R: AsyncRead + Unpin + Send + 'static> Drop for StreamConsumer<R> {
|
impl<R: AsyncRead + Unpin + Send + 'static> Drop for StreamConsumer<R> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.ensure_consumer_started();
|
if self.consumer_task.is_none() && self.inner.is_some() {
|
||||||
|
let mut inner = self.inner.take().unwrap();
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let mut buf = [0u8; 8192];
|
||||||
|
loop {
|
||||||
|
match inner.read(&mut buf).await {
|
||||||
|
Ok(0) => break, // EOF
|
||||||
|
Ok(_) => continue, // Keep consuming
|
||||||
|
Err(_) => break, // Error, stop consuming
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.consumer_task = Some(task);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1312,43 +1263,6 @@ mod tests {
|
|||||||
use temp_env::async_with_vars;
|
use temp_env::async_with_vars;
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct PendingPartialReader {
|
|
||||||
data: &'static [u8],
|
|
||||||
position: usize,
|
|
||||||
pending: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PendingPartialReader {
|
|
||||||
fn new(data: &'static [u8]) -> Self {
|
|
||||||
Self {
|
|
||||||
data,
|
|
||||||
position: 0,
|
|
||||||
pending: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncRead for PendingPartialReader {
|
|
||||||
fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<std::io::Result<()>> {
|
|
||||||
if self.pending {
|
|
||||||
self.pending = false;
|
|
||||||
cx.waker().wake_by_ref();
|
|
||||||
return Poll::Pending;
|
|
||||||
}
|
|
||||||
if self.position == self.data.len() {
|
|
||||||
return Poll::Ready(Ok(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let length = buf.remaining().min(3).min(self.data.len() - self.position);
|
|
||||||
let end = self.position + length;
|
|
||||||
buf.put_slice(&self.data[self.position..end]);
|
|
||||||
self.position = end;
|
|
||||||
self.pending = true;
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TEST_DIRECT_KEY_HEADER: &str = "x-rustfs-test-direct-key";
|
const TEST_DIRECT_KEY_HEADER: &str = "x-rustfs-test-direct-key";
|
||||||
const TEST_OBJECT_KEY_HEADER: &str = "x-rustfs-test-object-key";
|
const TEST_OBJECT_KEY_HEADER: &str = "x-rustfs-test-object-key";
|
||||||
const TEST_NONCE_HEADER: &str = "x-rustfs-test-nonce";
|
const TEST_NONCE_HEADER: &str = "x-rustfs-test-nonce";
|
||||||
@@ -1486,36 +1400,6 @@ mod tests {
|
|||||||
assert_eq!(result, b"World");
|
assert_eq!(result, b"World");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn uninitialized_scratch_preserves_partial_pending_and_eof_reads() {
|
|
||||||
let mut skipped = SkipReader::new(PendingPartialReader::new(b"0123456789abcdef"), 5);
|
|
||||||
let mut skipped_output = Vec::new();
|
|
||||||
skipped
|
|
||||||
.read_to_end(&mut skipped_output)
|
|
||||||
.await
|
|
||||||
.expect("skip reader should survive partial pending reads through EOF");
|
|
||||||
assert_eq!(skipped_output, b"56789abcdef");
|
|
||||||
|
|
||||||
let mut ranged = RangedDecompressReader::new(PendingPartialReader::new(b"0123456789abcdef"), 5, 7, 16)
|
|
||||||
.expect("valid range should construct");
|
|
||||||
let mut ranged_output = Vec::new();
|
|
||||||
ranged
|
|
||||||
.read_to_end(&mut ranged_output)
|
|
||||||
.await
|
|
||||||
.expect("range reader should survive partial pending reads through EOF");
|
|
||||||
assert_eq!(ranged_output, b"56789ab");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn uninitialized_skip_scratch_reports_early_eof() {
|
|
||||||
let mut reader = SkipReader::new(PendingPartialReader::new(b"short"), 6);
|
|
||||||
let error = reader
|
|
||||||
.read_to_end(&mut Vec::new())
|
|
||||||
.await
|
|
||||||
.expect_err("EOF before the skip boundary must remain visible");
|
|
||||||
assert_eq!(error.kind(), std::io::ErrorKind::UnexpectedEof);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_ranged_decompress_reader_from_start() {
|
async fn test_ranged_decompress_reader_from_start() {
|
||||||
let original_data = b"Hello, World! This is a test.";
|
let original_data = b"Hello, World! This is a test.";
|
||||||
@@ -1781,423 +1665,6 @@ mod tests {
|
|||||||
assert_eq!(actual, b"fghijkl");
|
assert_eq!(actual, b"fghijkl");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compresses one multipart part exactly like the write path does
|
|
||||||
/// (`WritePlan::with_compression` wraps each part in its own
|
|
||||||
/// `compression_reader`), returning the on-disk bytes and the storage-format
|
|
||||||
/// compression index.
|
|
||||||
async fn compressed_part_fixture(data: &[u8]) -> (Vec<u8>, Option<Bytes>) {
|
|
||||||
use crate::io_support::rio::TryGetIndex as _;
|
|
||||||
let mut compressor =
|
|
||||||
crate::io_support::rio::compression_reader(Cursor::new(data.to_vec()), CompressionAlgorithm::default(), false);
|
|
||||||
let mut compressed = Vec::new();
|
|
||||||
compressor.read_to_end(&mut compressed).await.expect("compress part stream");
|
|
||||||
let index = compressor
|
|
||||||
.try_get_index()
|
|
||||||
.map(crate::io_support::rio::compression_index_storage_bytes);
|
|
||||||
(compressed, index)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CompressedMultipartFixture {
|
|
||||||
object_info: ObjectInfo,
|
|
||||||
stored: Vec<u8>,
|
|
||||||
plaintext: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds the on-disk representation of a compressed multipart object: each
|
|
||||||
/// part is an independent compressed stream and the storage layer serves
|
|
||||||
/// their concatenation.
|
|
||||||
async fn compressed_multipart_fixture(part_sizes: &[usize]) -> CompressedMultipartFixture {
|
|
||||||
let pattern = b"compressed multipart read path fixture data ";
|
|
||||||
let mut plaintext = Vec::new();
|
|
||||||
let mut stored = Vec::new();
|
|
||||||
let mut parts = Vec::with_capacity(part_sizes.len());
|
|
||||||
|
|
||||||
for (i, part_size) in part_sizes.iter().enumerate() {
|
|
||||||
let mut part_plaintext = Vec::with_capacity(*part_size);
|
|
||||||
while part_plaintext.len() < *part_size {
|
|
||||||
part_plaintext.extend_from_slice(pattern);
|
|
||||||
part_plaintext.push(i as u8);
|
|
||||||
}
|
|
||||||
part_plaintext.truncate(*part_size);
|
|
||||||
|
|
||||||
let (compressed, index) = compressed_part_fixture(&part_plaintext).await;
|
|
||||||
parts.push(ObjectPartInfo {
|
|
||||||
number: i + 1,
|
|
||||||
size: compressed.len(),
|
|
||||||
actual_size: *part_size as i64,
|
|
||||||
index,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
stored.extend_from_slice(&compressed);
|
|
||||||
plaintext.extend_from_slice(&part_plaintext);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut user_defined = HashMap::new();
|
|
||||||
rustfs_utils::http::insert_str(
|
|
||||||
&mut user_defined,
|
|
||||||
rustfs_utils::http::SUFFIX_COMPRESSION,
|
|
||||||
crate::io_support::rio::compression_metadata_value(CompressionAlgorithm::default()),
|
|
||||||
);
|
|
||||||
rustfs_utils::http::insert_str(&mut user_defined, rustfs_utils::http::SUFFIX_ACTUAL_SIZE, plaintext.len().to_string());
|
|
||||||
|
|
||||||
let object_info = ObjectInfo {
|
|
||||||
bucket: "test-bucket".to_string(),
|
|
||||||
name: "compressed-multipart".to_string(),
|
|
||||||
size: stored.len() as i64,
|
|
||||||
etag: Some(format!("6bcf86bed8807b8e78f0fc6e0a53079d-{}", part_sizes.len())),
|
|
||||||
parts: Arc::new(parts),
|
|
||||||
user_defined: Arc::new(user_defined),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
CompressedMultipartFixture {
|
|
||||||
object_info,
|
|
||||||
stored,
|
|
||||||
plaintext,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Plans the read once to learn the storage window, then serves exactly that
|
|
||||||
/// window — mirroring how `set_disk` feeds the erasure read into the
|
|
||||||
/// returned reader.
|
|
||||||
async fn read_compressed_multipart(
|
|
||||||
fixture: &CompressedMultipartFixture,
|
|
||||||
rs: Option<HTTPRangeSpec>,
|
|
||||||
opts: &ObjectOptions,
|
|
||||||
) -> Vec<u8> {
|
|
||||||
let headers = HeaderMap::new();
|
|
||||||
let (_, offset, length) =
|
|
||||||
GetObjectReader::new(Box::new(Cursor::new(Vec::new())), rs.clone(), &fixture.object_info, opts, &headers)
|
|
||||||
.await
|
|
||||||
.expect("plan compressed multipart read");
|
|
||||||
|
|
||||||
let end = offset + usize::try_from(length).expect("storage window length must be non-negative");
|
|
||||||
assert!(
|
|
||||||
end <= fixture.stored.len(),
|
|
||||||
"planned storage window {offset}..{end} exceeds stored stream of {} bytes",
|
|
||||||
fixture.stored.len()
|
|
||||||
);
|
|
||||||
let window = fixture.stored[offset..end].to_vec();
|
|
||||||
|
|
||||||
let (mut reader, replay_offset, replay_length) =
|
|
||||||
GetObjectReader::new(Box::new(Cursor::new(window)), rs, &fixture.object_info, opts, &headers)
|
|
||||||
.await
|
|
||||||
.expect("build compressed multipart reader");
|
|
||||||
assert_eq!((replay_offset, replay_length), (offset, length), "read plan must be deterministic");
|
|
||||||
|
|
||||||
reader.read_all().await.expect("read compressed multipart stream")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Byte pattern with a 2 KiB period: it compresses extremely well while
|
|
||||||
/// looking nothing like ASCII fixtures. Mirrors the e2e generator that
|
|
||||||
/// exposed a truncated full GET on high-ratio multipart payloads.
|
|
||||||
fn high_ratio_binary_payload(size: usize, seed: u8) -> Vec<u8> {
|
|
||||||
(0..size)
|
|
||||||
.map(|i| ((i as u64).wrapping_mul(2_654_435_761).wrapping_add(seed as u64) >> 3) as u8)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn compressed_multipart_full_get_handles_high_ratio_binary_payload() {
|
|
||||||
let part_sizes = [5 * 1024 * 1024_usize, 1024 * 1024];
|
|
||||||
let mut plaintext = Vec::new();
|
|
||||||
let mut stored = Vec::new();
|
|
||||||
let mut parts = Vec::with_capacity(part_sizes.len());
|
|
||||||
|
|
||||||
for (i, part_size) in part_sizes.iter().enumerate() {
|
|
||||||
let part_plaintext = high_ratio_binary_payload(*part_size, if i == 0 { 7 } else { 61 });
|
|
||||||
let (compressed, index) = compressed_part_fixture(&part_plaintext).await;
|
|
||||||
parts.push(ObjectPartInfo {
|
|
||||||
number: i + 1,
|
|
||||||
size: compressed.len(),
|
|
||||||
actual_size: *part_size as i64,
|
|
||||||
index,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
stored.extend_from_slice(&compressed);
|
|
||||||
plaintext.extend_from_slice(&part_plaintext);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut user_defined = HashMap::new();
|
|
||||||
rustfs_utils::http::insert_str(
|
|
||||||
&mut user_defined,
|
|
||||||
rustfs_utils::http::SUFFIX_COMPRESSION,
|
|
||||||
crate::io_support::rio::compression_metadata_value(CompressionAlgorithm::default()),
|
|
||||||
);
|
|
||||||
rustfs_utils::http::insert_str(&mut user_defined, rustfs_utils::http::SUFFIX_ACTUAL_SIZE, plaintext.len().to_string());
|
|
||||||
let fixture = CompressedMultipartFixture {
|
|
||||||
object_info: ObjectInfo {
|
|
||||||
bucket: "test-bucket".to_string(),
|
|
||||||
name: "high-ratio-multipart".to_string(),
|
|
||||||
size: stored.len() as i64,
|
|
||||||
etag: Some("6bcf86bed8807b8e78f0fc6e0a53079d-2".to_string()),
|
|
||||||
parts: Arc::new(parts),
|
|
||||||
user_defined: Arc::new(user_defined),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
stored,
|
|
||||||
plaintext,
|
|
||||||
};
|
|
||||||
|
|
||||||
let read = read_compressed_multipart(&fixture, None, &ObjectOptions::default()).await;
|
|
||||||
|
|
||||||
assert_eq!(read.len(), fixture.plaintext.len(), "full GET must return the logical size");
|
|
||||||
assert_eq!(read, fixture.plaintext, "high-ratio multipart payload must survive the roundtrip");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Full GET over a compressed multipart object must decode across part
|
|
||||||
/// boundaries: every part is an independent compressed stream (this is also
|
|
||||||
/// the on-disk shape written by builds before rustfs/rustfs#5169 disabled
|
|
||||||
/// multipart compression, so this pins legacy-object readability).
|
|
||||||
#[tokio::test]
|
|
||||||
async fn compressed_multipart_full_get_decodes_across_part_boundaries() {
|
|
||||||
let fixture = compressed_multipart_fixture(&[3 * 1024 * 1024, 2 * 1024 * 1024, 512 * 1024]).await;
|
|
||||||
|
|
||||||
let read = read_compressed_multipart(&fixture, None, &ObjectOptions::default()).await;
|
|
||||||
|
|
||||||
assert_eq!(read.len(), fixture.plaintext.len(), "full GET must return the logical size");
|
|
||||||
assert_eq!(read, fixture.plaintext, "full GET must reassemble all parts");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn compressed_multipart_range_get_crosses_part_boundary() {
|
|
||||||
let fixture = compressed_multipart_fixture(&[3 * 1024 * 1024, 2 * 1024 * 1024]).await;
|
|
||||||
let boundary = 3 * 1024 * 1024_i64;
|
|
||||||
let rs = HTTPRangeSpec {
|
|
||||||
is_suffix_length: false,
|
|
||||||
start: boundary - 100_000,
|
|
||||||
end: boundary + 100_000 - 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let read = read_compressed_multipart(&fixture, Some(rs), &ObjectOptions::default()).await;
|
|
||||||
|
|
||||||
let expected = &fixture.plaintext[(boundary - 100_000) as usize..(boundary + 100_000) as usize];
|
|
||||||
assert_eq!(read, expected, "boundary-crossing range must splice both parts");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn compressed_multipart_range_get_seeks_into_later_part() {
|
|
||||||
let fixture = compressed_multipart_fixture(&[3 * 1024 * 1024, 4 * 1024 * 1024]).await;
|
|
||||||
// Deep inside part 2 so the plan skips part 1 entirely and (when the
|
|
||||||
// part carries an index) seeks within part 2.
|
|
||||||
let start = 3 * 1024 * 1024_i64 + 2 * 1024 * 1024_i64 + 137;
|
|
||||||
let rs = HTTPRangeSpec {
|
|
||||||
is_suffix_length: false,
|
|
||||||
start,
|
|
||||||
end: start + 64 * 1024 - 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let read = read_compressed_multipart(&fixture, Some(rs), &ObjectOptions::default()).await;
|
|
||||||
|
|
||||||
let expected = &fixture.plaintext[start as usize..(start + 64 * 1024) as usize];
|
|
||||||
assert_eq!(read, expected, "range inside a later part must decode from that part");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parts written without a compression index (small parts skip the index in
|
|
||||||
/// the rio-v2 backend) must still be rangeable: the plan starts at the part
|
|
||||||
/// boundary and skips decompressed bytes.
|
|
||||||
#[tokio::test]
|
|
||||||
async fn compressed_multipart_range_get_works_without_part_indexes() {
|
|
||||||
let mut fixture = compressed_multipart_fixture(&[1024 * 1024, 1024 * 1024]).await;
|
|
||||||
let parts = fixture
|
|
||||||
.object_info
|
|
||||||
.parts
|
|
||||||
.iter()
|
|
||||||
.map(|part| ObjectPartInfo {
|
|
||||||
index: None,
|
|
||||||
..part.clone()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
fixture.object_info.parts = Arc::new(parts);
|
|
||||||
|
|
||||||
let start = 1024 * 1024_i64 + 4096;
|
|
||||||
let rs = HTTPRangeSpec {
|
|
||||||
is_suffix_length: false,
|
|
||||||
start,
|
|
||||||
end: start + 32 * 1024 - 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let read = read_compressed_multipart(&fixture, Some(rs), &ObjectOptions::default()).await;
|
|
||||||
|
|
||||||
let expected = &fixture.plaintext[start as usize..(start + 32 * 1024) as usize];
|
|
||||||
assert_eq!(read, expected, "index-less parts must fall back to part-boundary skip");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn compressed_multipart_part_number_get_returns_single_part() {
|
|
||||||
let part_sizes = [3 * 1024 * 1024, 2 * 1024 * 1024, 512 * 1024];
|
|
||||||
let fixture = compressed_multipart_fixture(&part_sizes).await;
|
|
||||||
|
|
||||||
let mut logical_offset = 0_usize;
|
|
||||||
for (i, part_size) in part_sizes.iter().enumerate() {
|
|
||||||
let opts = ObjectOptions {
|
|
||||||
part_number: Some(i + 1),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let read = read_compressed_multipart(&fixture, None, &opts).await;
|
|
||||||
|
|
||||||
let expected = &fixture.plaintext[logical_offset..logical_offset + part_size];
|
|
||||||
assert_eq!(read.len(), *part_size, "partNumber={} GET must return the part's logical size", i + 1);
|
|
||||||
assert_eq!(read, expected, "partNumber={} GET must return the original part bytes", i + 1);
|
|
||||||
logical_offset += part_size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn compressed_multipart_suffix_range_reads_tail() {
|
|
||||||
let fixture = compressed_multipart_fixture(&[3 * 1024 * 1024, 1024 * 1024]).await;
|
|
||||||
let suffix_len = 128 * 1024_i64;
|
|
||||||
let rs = HTTPRangeSpec {
|
|
||||||
is_suffix_length: true,
|
|
||||||
start: suffix_len,
|
|
||||||
end: -1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let read = read_compressed_multipart(&fixture, Some(rs), &ObjectOptions::default()).await;
|
|
||||||
|
|
||||||
let expected = &fixture.plaintext[fixture.plaintext.len() - suffix_len as usize..];
|
|
||||||
assert_eq!(read, expected, "suffix range must return the tail of the last part");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds an SSE-C + disk-compression multipart object exactly like the
|
|
||||||
/// write path: each part is compressed into its own stream and then
|
|
||||||
/// encrypted with the per-part key schedule. The fixture is
|
|
||||||
/// legacy-encryption-specific (`rustfs_rio::EncryptReader`), matching the
|
|
||||||
/// pre-existing `build_legacy_ssec_multipart_fixture` shape, while the
|
|
||||||
/// compression layer follows the active backend feature.
|
|
||||||
async fn compressed_encrypted_multipart_fixture(key_bytes: [u8; 32], part_sizes: &[usize]) -> CompressedMultipartFixture {
|
|
||||||
let pattern = b"compressed encrypted multipart fixture data ";
|
|
||||||
let mut plaintext = Vec::new();
|
|
||||||
let mut stored = Vec::new();
|
|
||||||
let mut parts = Vec::with_capacity(part_sizes.len());
|
|
||||||
|
|
||||||
for (i, part_size) in part_sizes.iter().enumerate() {
|
|
||||||
let part_number = i + 1;
|
|
||||||
let mut part_plaintext = Vec::with_capacity(*part_size);
|
|
||||||
while part_plaintext.len() < *part_size {
|
|
||||||
part_plaintext.extend_from_slice(pattern);
|
|
||||||
part_plaintext.push(part_number as u8);
|
|
||||||
}
|
|
||||||
part_plaintext.truncate(*part_size);
|
|
||||||
|
|
||||||
let (compressed, index) = compressed_part_fixture(&part_plaintext).await;
|
|
||||||
let mut part_cipher = Vec::new();
|
|
||||||
rustfs_rio::EncryptReader::new_multipart(Cursor::new(compressed), key_bytes, LEGACY_FIXTURE_BASE_NONCE, part_number)
|
|
||||||
.read_to_end(&mut part_cipher)
|
|
||||||
.await
|
|
||||||
.expect("encrypt compressed fixture part");
|
|
||||||
|
|
||||||
parts.push(ObjectPartInfo {
|
|
||||||
number: part_number,
|
|
||||||
size: part_cipher.len(),
|
|
||||||
actual_size: *part_size as i64,
|
|
||||||
index,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
stored.extend_from_slice(&part_cipher);
|
|
||||||
plaintext.extend_from_slice(&part_plaintext);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut user_defined = legacy_ssec_multipart_metadata(key_bytes, plaintext.len());
|
|
||||||
rustfs_utils::http::insert_str(
|
|
||||||
&mut user_defined,
|
|
||||||
rustfs_utils::http::SUFFIX_COMPRESSION,
|
|
||||||
crate::io_support::rio::compression_metadata_value(CompressionAlgorithm::default()),
|
|
||||||
);
|
|
||||||
rustfs_utils::http::insert_str(&mut user_defined, rustfs_utils::http::SUFFIX_ACTUAL_SIZE, plaintext.len().to_string());
|
|
||||||
|
|
||||||
let object_info = ObjectInfo {
|
|
||||||
bucket: "test-bucket".to_string(),
|
|
||||||
name: "compressed-encrypted-multipart".to_string(),
|
|
||||||
size: stored.len() as i64,
|
|
||||||
etag: Some(format!("6bcf86bed8807b8e78f0fc6e0a53079d-{}", part_sizes.len())),
|
|
||||||
parts: Arc::new(parts),
|
|
||||||
user_defined: Arc::new(user_defined),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
CompressedMultipartFixture {
|
|
||||||
object_info,
|
|
||||||
stored,
|
|
||||||
plaintext,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_compressed_encrypted_multipart(
|
|
||||||
fixture: &CompressedMultipartFixture,
|
|
||||||
key_bytes: [u8; 32],
|
|
||||||
rs: Option<HTTPRangeSpec>,
|
|
||||||
opts: &ObjectOptions,
|
|
||||||
) -> Vec<u8> {
|
|
||||||
let headers = ssec_headers_from_key(key_bytes);
|
|
||||||
let (_, offset, length) =
|
|
||||||
GetObjectReader::new(Box::new(Cursor::new(Vec::new())), rs.clone(), &fixture.object_info, opts, &headers)
|
|
||||||
.await
|
|
||||||
.expect("plan compressed encrypted multipart read");
|
|
||||||
|
|
||||||
let end = offset + usize::try_from(length).expect("storage window length must be non-negative");
|
|
||||||
assert!(
|
|
||||||
end <= fixture.stored.len(),
|
|
||||||
"planned storage window {offset}..{end} exceeds stored stream of {} bytes",
|
|
||||||
fixture.stored.len()
|
|
||||||
);
|
|
||||||
let window = fixture.stored[offset..end].to_vec();
|
|
||||||
|
|
||||||
let (mut reader, replay_offset, replay_length) =
|
|
||||||
GetObjectReader::new(Box::new(Cursor::new(window)), rs, &fixture.object_info, opts, &headers)
|
|
||||||
.await
|
|
||||||
.expect("build compressed encrypted multipart reader");
|
|
||||||
assert_eq!((replay_offset, replay_length), (offset, length), "read plan must be deterministic");
|
|
||||||
|
|
||||||
reader.read_all().await.expect("read compressed encrypted multipart stream")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn compressed_encrypted_multipart_full_get_roundtrip() {
|
|
||||||
let key_bytes = [0x6Eu8; 32];
|
|
||||||
let fixture = compressed_encrypted_multipart_fixture(key_bytes, &[3 * 1024 * 1024, 1024 * 1024]).await;
|
|
||||||
|
|
||||||
let read = read_compressed_encrypted_multipart(&fixture, key_bytes, None, &ObjectOptions::default()).await;
|
|
||||||
|
|
||||||
assert_eq!(read.len(), fixture.plaintext.len(), "full GET must return the logical size");
|
|
||||||
assert_eq!(read, fixture.plaintext, "SSE-C + compression full GET must reassemble all parts");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn compressed_encrypted_multipart_range_crosses_part_boundary() {
|
|
||||||
let key_bytes = [0x6Eu8; 32];
|
|
||||||
let fixture = compressed_encrypted_multipart_fixture(key_bytes, &[3 * 1024 * 1024, 1024 * 1024]).await;
|
|
||||||
let boundary = 3 * 1024 * 1024_i64;
|
|
||||||
let rs = HTTPRangeSpec {
|
|
||||||
is_suffix_length: false,
|
|
||||||
start: boundary - 65_536,
|
|
||||||
end: boundary + 65_536 - 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let read = read_compressed_encrypted_multipart(&fixture, key_bytes, Some(rs), &ObjectOptions::default()).await;
|
|
||||||
|
|
||||||
let expected = &fixture.plaintext[(boundary - 65_536) as usize..(boundary + 65_536) as usize];
|
|
||||||
assert_eq!(read, expected, "SSE-C + compression boundary-crossing range must splice both parts");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn compressed_encrypted_multipart_part_number_get_returns_single_part() {
|
|
||||||
let key_bytes = [0x6Eu8; 32];
|
|
||||||
let part_sizes = [3 * 1024 * 1024, 1024 * 1024];
|
|
||||||
let fixture = compressed_encrypted_multipart_fixture(key_bytes, &part_sizes).await;
|
|
||||||
|
|
||||||
let opts = ObjectOptions {
|
|
||||||
part_number: Some(2),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let read = read_compressed_encrypted_multipart(&fixture, key_bytes, None, &opts).await;
|
|
||||||
|
|
||||||
let expected = &fixture.plaintext[part_sizes[0]..];
|
|
||||||
assert_eq!(read.len(), part_sizes[1], "partNumber=2 GET must return the part's logical size");
|
|
||||||
assert_eq!(read, expected, "partNumber=2 GET must return the original part bytes");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_get_object_reader_rejects_ssec_read_without_headers() {
|
async fn test_get_object_reader_rejects_ssec_read_without_headers() {
|
||||||
let object_info = ObjectInfo {
|
let object_info = ObjectInfo {
|
||||||
|
|||||||
@@ -172,7 +172,6 @@ impl ObjectLockConfigSnapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "snapshot-scope predicate asserted by this file's tests (backlog#1823)")]
|
|
||||||
pub(crate) fn is_for_store_bucket(
|
pub(crate) fn is_for_store_bucket(
|
||||||
&self,
|
&self,
|
||||||
store_id: Uuid,
|
store_id: Uuid,
|
||||||
@@ -277,12 +276,6 @@ pub struct ObjectOptions {
|
|||||||
/// fence avoids recursively acquiring the read lock behind a queued writer.
|
/// fence avoids recursively acquiring the read lock behind a queued writer.
|
||||||
pub bucket_lifecycle_lock_fence: Option<NamespaceLockFence>,
|
pub bucket_lifecycle_lock_fence: Option<NamespaceLockFence>,
|
||||||
pub replication_request: bool,
|
pub replication_request: bool,
|
||||||
/// Source-cluster LWW timestamps carried by an authorized replication
|
|
||||||
/// request; None when the source never modified the category. Only the
|
|
||||||
/// replication-authorized options builders may set these.
|
|
||||||
pub replication_tagging_timestamp: Option<OffsetDateTime>,
|
|
||||||
pub replication_retention_timestamp: Option<OffsetDateTime>,
|
|
||||||
pub replication_legalhold_timestamp: Option<OffsetDateTime>,
|
|
||||||
/// Authorized SSE-C replication passthrough: the body is already
|
/// Authorized SSE-C replication passthrough: the body is already
|
||||||
/// ciphertext, so the write path must not encrypt or compress it and
|
/// ciphertext, so the write path must not encrypt or compress it and
|
||||||
/// stores the restored encryption metadata verbatim. Only the
|
/// stores the restored encryption metadata verbatim. Only the
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ use std::{
|
|||||||
use tokio::sync::{OnceCell, RwLock};
|
use tokio::sync::{OnceCell, RwLock};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub const DISK_ASSUME_UNKNOWN_SIZE: u64 = 1 << 30;
|
pub const DISK_ASSUME_UNKNOWN_SIZE: u64 = 1 << 30;
|
||||||
pub const DISK_MIN_INODES: u64 = 1000;
|
pub const DISK_MIN_INODES: u64 = 1000;
|
||||||
@@ -108,6 +109,18 @@ pub fn set_global_rustfs_port(value: u16) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the global deployment id
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `id` - The Uuid to set as the global deployment id
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * None
|
||||||
|
///
|
||||||
|
pub fn set_global_deployment_id(id: Uuid) {
|
||||||
|
current_ctx().set_deployment_id(id);
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the global deployment id
|
/// Get the global deployment id
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
@@ -275,6 +288,19 @@ pub fn get_global_region() -> Option<s3s::region::Region> {
|
|||||||
current_ctx().region()
|
current_ctx().region()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initialize the global background services cancellation token
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `cancel_token` - The CancellationToken instance to set globally
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` if successful
|
||||||
|
/// * `Err(CancellationToken)` if setting fails
|
||||||
|
///
|
||||||
|
pub fn init_background_services_cancel_token(cancel_token: CancellationToken) -> Result<(), CancellationToken> {
|
||||||
|
current_ctx().init_background_cancel_token(cancel_token)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the global background services cancellation token
|
/// Get the global background services cancellation token
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
@@ -284,6 +310,18 @@ pub fn get_background_services_cancel_token() -> Option<CancellationToken> {
|
|||||||
current_ctx().background_cancel_token()
|
current_ctx().background_cancel_token()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create and initialize the global background services cancellation token
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `CancellationToken` - The newly created global cancellation token
|
||||||
|
///
|
||||||
|
pub fn create_background_services_cancel_token() -> CancellationToken {
|
||||||
|
let cancel_token = CancellationToken::new();
|
||||||
|
init_background_services_cancel_token(cancel_token.clone())
|
||||||
|
.expect("background services cancel token should be initialized once during startup");
|
||||||
|
cancel_token
|
||||||
|
}
|
||||||
|
|
||||||
/// Shutdown all background services gracefully
|
/// Shutdown all background services gracefully
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
|
|||||||
@@ -402,10 +402,6 @@ impl InstanceContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "driven by the tier-delete-journal recovery test behind `--features test-util` (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) fn wake_tier_delete_journal_recovery(&self) {
|
pub(crate) fn wake_tier_delete_journal_recovery(&self) {
|
||||||
self.tier_delete_journal_recovery_wakeup.notify_one();
|
self.tier_delete_journal_recovery_wakeup.notify_one();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: runtime source migration keeps fallback handles until all owners inject state.
|
// #730: runtime source migration keeps fallback handles until all owners inject state.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
pub(crate) mod global;
|
pub(crate) mod global;
|
||||||
pub(crate) mod instance;
|
pub(crate) mod instance;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ use crate::{
|
|||||||
set_object_layer, update_erasure_type,
|
set_object_layer, update_erasure_type,
|
||||||
},
|
},
|
||||||
services::batch_processor::{GlobalBatchProcessors, get_global_processors},
|
services::batch_processor::{GlobalBatchProcessors, get_global_processors},
|
||||||
|
services::event_notification::EventNotifier,
|
||||||
services::notification_sys::{NotificationSys, get_global_notification_sys},
|
services::notification_sys::{NotificationSys, get_global_notification_sys},
|
||||||
services::tier::tier::TierConfigMgr,
|
services::tier::tier::TierConfigMgr,
|
||||||
store::ECStore,
|
store::ECStore,
|
||||||
@@ -142,10 +143,6 @@ pub async fn setup_is_erasure_sd() -> bool {
|
|||||||
is_erasure_sd().await
|
is_erasure_sd().await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "setup-type override used only by tests across this crate (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) async fn current_setup_type() -> SetupType {
|
pub(crate) async fn current_setup_type() -> SetupType {
|
||||||
if setup_is_dist_erasure().await {
|
if setup_is_dist_erasure().await {
|
||||||
SetupType::DistErasure
|
SetupType::DistErasure
|
||||||
@@ -158,10 +155,6 @@ pub(crate) async fn current_setup_type() -> SetupType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "setup-type override used only by tests across this crate (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) async fn set_setup_type(setup_type: SetupType) {
|
pub(crate) async fn set_setup_type(setup_type: SetupType) {
|
||||||
update_erasure_type(setup_type).await;
|
update_erasure_type(setup_type).await;
|
||||||
}
|
}
|
||||||
@@ -239,6 +232,14 @@ pub(crate) fn ensure_test_rpc_secret() {
|
|||||||
let _ = rustfs_credentials::set_global_rpc_secret(TEST_RPC_SECRET.to_owned());
|
let _ = rustfs_credentials::set_global_rpc_secret(TEST_RPC_SECRET.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn storage_class_parity(storage_class: Option<&str>) -> Option<usize> {
|
||||||
|
get_global_storage_class_snapshot().get_parity_for_sc(storage_class.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn storage_class_should_inline(shard_size: i64, versioned: bool) -> bool {
|
||||||
|
get_global_storage_class_snapshot().should_inline(shard_size, versioned)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn deployment_upload_id(upload_id: &str) -> String {
|
pub(crate) fn deployment_upload_id(upload_id: &str) -> String {
|
||||||
base64_simd::URL_SAFE_NO_PAD
|
base64_simd::URL_SAFE_NO_PAD
|
||||||
.encode_to_string(format!("{}.{}", get_global_deployment_id().unwrap_or_default(), upload_id).as_bytes())
|
.encode_to_string(format!("{}.{}", get_global_deployment_id().unwrap_or_default(), upload_id).as_bytes())
|
||||||
@@ -331,6 +332,21 @@ pub(crate) fn storage_class_config_snapshot() -> Arc<storageclass::Config> {
|
|||||||
get_global_storage_class_snapshot()
|
get_global_storage_class_snapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scalar STANDARD / RRS parity for backend-info reporting.
|
||||||
|
///
|
||||||
|
/// Retained for the rebalance/backend-info path. `get_parity_for_sc` returns
|
||||||
|
/// `None` when the runtime config is uninitialized or (post per-pool support)
|
||||||
|
/// when pools disagree, so STANDARD falls back to the caller's default and RRS
|
||||||
|
/// stays `None` — matching the pre-per-pool scalar reporting.
|
||||||
|
pub(crate) fn backend_storage_class_parities(default_standard_parity: usize) -> (Option<usize>, Option<usize>) {
|
||||||
|
let sc = get_global_storage_class_snapshot();
|
||||||
|
let standard = sc
|
||||||
|
.get_parity_for_sc(storageclass::CLASS_STANDARD)
|
||||||
|
.or(Some(default_standard_parity));
|
||||||
|
let reduced_redundancy = sc.get_parity_for_sc(storageclass::RRS);
|
||||||
|
(standard, reduced_redundancy)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_storage_class_config(config: storageclass::Config) {
|
pub(crate) fn set_storage_class_config(config: storageclass::Config) {
|
||||||
set_global_storage_class(config);
|
set_global_storage_class(config);
|
||||||
}
|
}
|
||||||
@@ -398,6 +414,10 @@ pub fn transition_state_handle() -> Arc<TransitionState> {
|
|||||||
crate::runtime::global::current_ctx().transition_state()
|
crate::runtime::global::current_ctx().transition_state()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn event_notifier_handle() -> Arc<RwLock<EventNotifier>> {
|
||||||
|
crate::runtime::global::current_ctx().event_notifier()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn local_disk_by_path(path: &str) -> Option<DiskStore> {
|
pub(crate) async fn local_disk_by_path(path: &str) -> Option<DiskStore> {
|
||||||
local_disk_map_handle().read().await.get(path).cloned().flatten()
|
local_disk_map_handle().read().await.get(path).cloned().flatten()
|
||||||
}
|
}
|
||||||
@@ -491,6 +511,30 @@ pub(crate) async fn local_disk_set_drive(
|
|||||||
instance_ctx.local_disk_set_drives().read().await[pool_idx][set_idx][disk_idx].clone()
|
instance_ctx.local_disk_set_drives().read().await[pool_idx][set_idx][disk_idx].clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn local_disk_for_endpoint(endpoint: &Endpoint) -> Option<DiskStore> {
|
||||||
|
let set_drives = local_disk_set_drives_handle();
|
||||||
|
let global_set_drives = set_drives.read().await;
|
||||||
|
if global_set_drives.is_empty() {
|
||||||
|
return local_disk_map_handle()
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get(&endpoint.to_string())
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pool_idx = usize::try_from(endpoint.pool_idx).ok()?;
|
||||||
|
let set_idx = usize::try_from(endpoint.set_idx).ok()?;
|
||||||
|
let disk_idx = usize::try_from(endpoint.disk_idx).ok()?;
|
||||||
|
|
||||||
|
global_set_drives
|
||||||
|
.get(pool_idx)
|
||||||
|
.and_then(|sets| sets.get(set_idx))
|
||||||
|
.and_then(|disks| disks.get(disk_idx))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(None)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn local_disk_paths() -> Vec<String> {
|
pub(crate) async fn local_disk_paths() -> Vec<String> {
|
||||||
local_disk_map_handle().read().await.keys().cloned().collect()
|
local_disk_map_handle().read().await.keys().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,6 @@ use std::sync::{Arc, Mutex};
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "default operation label for the test-only AsyncBatchProcessor::new (backlog#1823)"
|
|
||||||
)]
|
|
||||||
const BATCH_PROCESSOR_OPERATION_CUSTOM: &str = "custom";
|
const BATCH_PROCESSOR_OPERATION_CUSTOM: &str = "custom";
|
||||||
const BATCH_PROCESSOR_OPERATION_READ: &str = "read";
|
const BATCH_PROCESSOR_OPERATION_READ: &str = "read";
|
||||||
const BATCH_PROCESSOR_OPERATION_WRITE: &str = "write";
|
const BATCH_PROCESSOR_OPERATION_WRITE: &str = "write";
|
||||||
@@ -215,7 +211,6 @@ pub struct AsyncBatchProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AsyncBatchProcessor {
|
impl AsyncBatchProcessor {
|
||||||
#[allow(dead_code, reason = "constructor used only by this file's tests (backlog#1823)")]
|
|
||||||
pub fn new(max_concurrent: usize) -> Self {
|
pub fn new(max_concurrent: usize) -> Self {
|
||||||
Self::new_with_operation(max_concurrent, BATCH_PROCESSOR_OPERATION_CUSTOM)
|
Self::new_with_operation(max_concurrent, BATCH_PROCESSOR_OPERATION_CUSTOM)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,26 +26,11 @@ use std::sync::atomic::Ordering;
|
|||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
/// Dead ecstore-side notification skeleton.
|
|
||||||
///
|
|
||||||
/// The working notification stack is `rustfs-notify`, whose own `EventNotifier`
|
|
||||||
/// is the one bucket configuration actually drives. Nothing calls the methods
|
|
||||||
/// below; `init_bucket_targets` even logs that it is a no-op in this build.
|
|
||||||
/// Removing it means also retiring the `InstanceContext` slot that holds it
|
|
||||||
/// (backlog#939 Phase 5), so it is left explicit here rather than half-removed.
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "ecstore-side notification skeleton superseded by rustfs-notify; see module note (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub struct EventNotifier {
|
pub struct EventNotifier {
|
||||||
target_list: TargetList,
|
target_list: TargetList,
|
||||||
//bucket_rules_map: HashMap<String , HashMap<EventName, Rules>>,
|
//bucket_rules_map: HashMap<String , HashMap<EventName, Rules>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "ecstore-side notification skeleton superseded by rustfs-notify; see module note (backlog#1823)"
|
|
||||||
)]
|
|
||||||
impl EventNotifier {
|
impl EventNotifier {
|
||||||
pub fn new() -> Arc<RwLock<Self>> {
|
pub fn new() -> Arc<RwLock<Self>> {
|
||||||
Arc::new(RwLock::new(Self {
|
Arc::new(RwLock::new(Self {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// #730: background service owners still contain staged notification/rebalance/tier paths.
|
// #730: background service owners still contain staged notification/rebalance/tier paths.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
pub(crate) mod batch_processor;
|
pub(crate) mod batch_processor;
|
||||||
pub(crate) mod event_notification;
|
pub(crate) mod event_notification;
|
||||||
|
|||||||
@@ -44,14 +44,12 @@ const CONSECUTIVE_FAILURE_THRESHOLD: u32 = 3;
|
|||||||
const LOG_COMPONENT_ECSTORE: &str = "ecstore";
|
const LOG_COMPONENT_ECSTORE: &str = "ecstore";
|
||||||
const LOG_SUBSYSTEM_NOTIFICATION: &str = "notification";
|
const LOG_SUBSYSTEM_NOTIFICATION: &str = "notification";
|
||||||
const EVENT_NOTIFICATION_PEER_PROPAGATION: &str = "notification_peer_propagation";
|
const EVENT_NOTIFICATION_PEER_PROPAGATION: &str = "notification_peer_propagation";
|
||||||
const EVENT_NOTIFICATION_CAPABILITY_PROBE: &str = "notification_capability_probe";
|
|
||||||
const SCANNER_ACTIVITY_PROBE_TIMEOUT: Duration = Duration::from_secs(5);
|
const SCANNER_ACTIVITY_PROBE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
const TIER_CONFIG_RELOAD_RETRY_BASE: Duration = Duration::from_millis(100);
|
const TIER_CONFIG_RELOAD_RETRY_BASE: Duration = Duration::from_millis(100);
|
||||||
const TIER_CONFIG_RELOAD_RETRY_CAP: Duration = Duration::from_secs(5);
|
const TIER_CONFIG_RELOAD_RETRY_CAP: Duration = Duration::from_secs(5);
|
||||||
const REMOTE_VERSION_STATE_PROBE_INTERVAL: Duration = Duration::from_secs(10);
|
const REMOTE_VERSION_STATE_PROBE_INTERVAL: Duration = Duration::from_secs(10);
|
||||||
const REMOTE_VERSION_STATE_PROBE_TIMEOUT: Duration = Duration::from_secs(5);
|
const REMOTE_VERSION_STATE_PROBE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
const REMOTE_VERSION_STATE_PROOF_TTL: Duration = Duration::from_secs(30);
|
const REMOTE_VERSION_STATE_PROOF_TTL: Duration = Duration::from_secs(30);
|
||||||
const CROSS_POOL_FENCE_SUPPORTED_VERSION: u32 = 1;
|
|
||||||
|
|
||||||
/// Cached result from the last successful admin call to a peer.
|
/// Cached result from the last successful admin call to a peer.
|
||||||
struct PeerAdminCache {
|
struct PeerAdminCache {
|
||||||
@@ -97,15 +95,15 @@ lazy_static! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct FleetCapabilityProof {
|
struct RemoteVersionStateFleetProof {
|
||||||
topology_fingerprint: String,
|
topology_fingerprint: String,
|
||||||
peer_epochs: Arc<BTreeMap<String, Uuid>>,
|
peer_epochs: Arc<BTreeMap<String, Uuid>>,
|
||||||
expires_at: Instant,
|
expires_at: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FleetCapabilityProof {
|
impl RemoteVersionStateFleetProof {
|
||||||
fn token(&self) -> FleetCapabilityProofToken {
|
fn token(&self) -> RemoteVersionStateFleetProofToken {
|
||||||
FleetCapabilityProofToken {
|
RemoteVersionStateFleetProofToken {
|
||||||
topology_fingerprint: self.topology_fingerprint.clone(),
|
topology_fingerprint: self.topology_fingerprint.clone(),
|
||||||
peer_epochs: self.peer_epochs.clone(),
|
peer_epochs: self.peer_epochs.clone(),
|
||||||
}
|
}
|
||||||
@@ -113,41 +111,37 @@ impl FleetCapabilityProof {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
struct FleetCapabilityProofToken {
|
pub(crate) struct RemoteVersionStateFleetProofToken {
|
||||||
topology_fingerprint: String,
|
topology_fingerprint: String,
|
||||||
peer_epochs: Arc<BTreeMap<String, Uuid>>,
|
peer_epochs: Arc<BTreeMap<String, Uuid>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct FleetCapabilityProofState {
|
struct RemoteVersionStateFleetProofState {
|
||||||
proof: Option<FleetCapabilityProof>,
|
proof: Option<RemoteVersionStateFleetProof>,
|
||||||
topology_conflict: bool,
|
topology_conflict: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
static REMOTE_VERSION_STATE_FLEET_PROOF: OnceLock<std::sync::RwLock<RemoteVersionStateFleetProofState>> = OnceLock::new();
|
||||||
pub(crate) struct RemoteVersionStateFleetProofToken(FleetCapabilityProofToken);
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
|
||||||
pub struct CrossPoolFenceFleetProofToken(FleetCapabilityProofToken);
|
|
||||||
|
|
||||||
static REMOTE_VERSION_STATE_FLEET_PROOF: OnceLock<std::sync::RwLock<FleetCapabilityProofState>> = OnceLock::new();
|
|
||||||
static CROSS_POOL_FENCE_FLEET_PROOF: OnceLock<std::sync::RwLock<FleetCapabilityProofState>> = OnceLock::new();
|
|
||||||
static REMOTE_VERSION_STATE_PROBE_TOPOLOGY: OnceLock<String> = OnceLock::new();
|
static REMOTE_VERSION_STATE_PROBE_TOPOLOGY: OnceLock<String> = OnceLock::new();
|
||||||
|
|
||||||
fn cross_pool_fence_fleet_proof_slot() -> &'static std::sync::RwLock<FleetCapabilityProofState> {
|
fn remote_version_state_fleet_proof_slot() -> &'static std::sync::RwLock<RemoteVersionStateFleetProofState> {
|
||||||
CROSS_POOL_FENCE_FLEET_PROOF.get_or_init(|| std::sync::RwLock::new(FleetCapabilityProofState::default()))
|
REMOTE_VERSION_STATE_FLEET_PROOF.get_or_init(|| std::sync::RwLock::new(RemoteVersionStateFleetProofState::default()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remote_version_state_fleet_proof_slot() -> &'static std::sync::RwLock<FleetCapabilityProofState> {
|
fn replace_remote_version_state_fleet_proof(proof: Option<RemoteVersionStateFleetProof>) {
|
||||||
REMOTE_VERSION_STATE_FLEET_PROOF.get_or_init(|| std::sync::RwLock::new(FleetCapabilityProofState::default()))
|
replace_remote_version_state_fleet_proof_in(remote_version_state_fleet_proof_slot(), proof);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_fleet_capability_proof(slot: &std::sync::RwLock<FleetCapabilityProofState>, proof: Option<FleetCapabilityProof>) {
|
fn replace_remote_version_state_fleet_proof_in(
|
||||||
|
slot: &std::sync::RwLock<RemoteVersionStateFleetProofState>,
|
||||||
|
proof: Option<RemoteVersionStateFleetProof>,
|
||||||
|
) {
|
||||||
slot.write().unwrap_or_else(std::sync::PoisonError::into_inner).proof = proof;
|
slot.write().unwrap_or_else(std::sync::PoisonError::into_inner).proof = proof;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn publish_fleet_capability_probe_result(
|
fn publish_remote_version_state_probe_result(
|
||||||
slot: &std::sync::RwLock<FleetCapabilityProofState>,
|
slot: &std::sync::RwLock<RemoteVersionStateFleetProofState>,
|
||||||
topology_fingerprint: &str,
|
topology_fingerprint: &str,
|
||||||
result: Result<BTreeMap<String, Uuid>>,
|
result: Result<BTreeMap<String, Uuid>>,
|
||||||
observed_at: Instant,
|
observed_at: Instant,
|
||||||
@@ -161,7 +155,7 @@ fn publish_fleet_capability_probe_result(
|
|||||||
.filter(|proof| proof.topology_fingerprint == topology_fingerprint && proof.peer_epochs.as_ref() == &peer_epochs)
|
.filter(|proof| proof.topology_fingerprint == topology_fingerprint && proof.peer_epochs.as_ref() == &peer_epochs)
|
||||||
.map(|proof| Arc::clone(&proof.peer_epochs))
|
.map(|proof| Arc::clone(&proof.peer_epochs))
|
||||||
.unwrap_or_else(|| Arc::new(peer_epochs));
|
.unwrap_or_else(|| Arc::new(peer_epochs));
|
||||||
state.proof = Some(FleetCapabilityProof {
|
state.proof = Some(RemoteVersionStateFleetProof {
|
||||||
topology_fingerprint: topology_fingerprint.to_string(),
|
topology_fingerprint: topology_fingerprint.to_string(),
|
||||||
peer_epochs,
|
peer_epochs,
|
||||||
expires_at: observed_at + REMOTE_VERSION_STATE_PROOF_TTL,
|
expires_at: observed_at + REMOTE_VERSION_STATE_PROOF_TTL,
|
||||||
@@ -169,7 +163,7 @@ fn publish_fleet_capability_probe_result(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
replace_fleet_capability_proof(slot, None);
|
replace_remote_version_state_fleet_proof_in(slot, None);
|
||||||
Some(err)
|
Some(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,60 +174,27 @@ pub(crate) fn acquire_remote_version_state_fleet_proof() -> Option<RemoteVersion
|
|||||||
let state = remote_version_state_fleet_proof_slot()
|
let state = remote_version_state_fleet_proof_slot()
|
||||||
.read()
|
.read()
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
acquire_fleet_capability_proof_from(&state, expected_topology, Instant::now()).map(RemoteVersionStateFleetProofToken)
|
acquire_remote_version_state_fleet_proof_from(&state, expected_topology, Instant::now())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn acquire_fleet_capability_proof_from(
|
fn acquire_remote_version_state_fleet_proof_from(
|
||||||
state: &FleetCapabilityProofState,
|
state: &RemoteVersionStateFleetProofState,
|
||||||
expected_topology: &str,
|
expected_topology: &str,
|
||||||
now: Instant,
|
now: Instant,
|
||||||
) -> Option<FleetCapabilityProofToken> {
|
) -> Option<RemoteVersionStateFleetProofToken> {
|
||||||
if state.topology_conflict || !fleet_capability_proof_valid_at(state.proof.as_ref(), expected_topology, now) {
|
if state.topology_conflict || !remote_version_state_fleet_proof_valid_at(state.proof.as_ref(), expected_topology, now) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
state.proof.as_ref().map(FleetCapabilityProof::token)
|
state.proof.as_ref().map(RemoteVersionStateFleetProof::token)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn remote_version_state_fleet_proof_matches(proof: &RemoteVersionStateFleetProofToken) -> bool {
|
pub(crate) fn remote_version_state_fleet_proof_matches(proof: &RemoteVersionStateFleetProofToken) -> bool {
|
||||||
fleet_capability_proof_matches(remote_version_state_fleet_proof_slot(), &proof.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn acquire_cross_pool_fence_fleet_proof() -> Option<CrossPoolFenceFleetProofToken> {
|
|
||||||
let expected_topology = REMOTE_VERSION_STATE_PROBE_TOPOLOGY.get()?;
|
|
||||||
let state = cross_pool_fence_fleet_proof_slot()
|
|
||||||
.read()
|
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
||||||
acquire_fleet_capability_proof_from(&state, expected_topology, Instant::now()).map(CrossPoolFenceFleetProofToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cross_pool_fence_fleet_proof_matches(proof: &CrossPoolFenceFleetProofToken) -> bool {
|
|
||||||
fleet_capability_proof_matches(cross_pool_fence_fleet_proof_slot(), &proof.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-util"))]
|
|
||||||
pub fn rotate_cross_pool_fence_fleet_proof_for_test() -> bool {
|
|
||||||
let mut state = cross_pool_fence_fleet_proof_slot()
|
|
||||||
.write()
|
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
||||||
let Some(current) = state.proof.as_ref() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
state.proof = Some(FleetCapabilityProof {
|
|
||||||
topology_fingerprint: current.topology_fingerprint.clone(),
|
|
||||||
peer_epochs: Arc::new(current.peer_epochs.as_ref().clone()),
|
|
||||||
expires_at: current.expires_at,
|
|
||||||
});
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fleet_capability_proof_matches(
|
|
||||||
slot: &std::sync::RwLock<FleetCapabilityProofState>,
|
|
||||||
proof: &FleetCapabilityProofToken,
|
|
||||||
) -> bool {
|
|
||||||
let Some(expected_topology) = REMOTE_VERSION_STATE_PROBE_TOPOLOGY.get() else {
|
let Some(expected_topology) = REMOTE_VERSION_STATE_PROBE_TOPOLOGY.get() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let state = slot.read().unwrap_or_else(std::sync::PoisonError::into_inner);
|
let state = remote_version_state_fleet_proof_slot()
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
if state.topology_conflict {
|
if state.topology_conflict {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -245,42 +206,14 @@ fn fleet_capability_proof_matches(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fleet_capability_proof_valid_at(proof: Option<&FleetCapabilityProof>, expected_topology: &str, now: Instant) -> bool {
|
fn remote_version_state_fleet_proof_valid_at(
|
||||||
|
proof: Option<&RemoteVersionStateFleetProof>,
|
||||||
|
expected_topology: &str,
|
||||||
|
now: Instant,
|
||||||
|
) -> bool {
|
||||||
proof.is_some_and(|proof| proof.topology_fingerprint == expected_topology && now < proof.expires_at)
|
proof.is_some_and(|proof| proof.topology_fingerprint == expected_topology && now < proof.expires_at)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) struct RemoteVersionStateFleetProofGuard;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
impl Drop for RemoteVersionStateFleetProofGuard {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
replace_fleet_capability_proof(remote_version_state_fleet_proof_slot(), None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn install_remote_version_state_fleet_proof_for_test(topology_fingerprint: &str) -> RemoteVersionStateFleetProofGuard {
|
|
||||||
match REMOTE_VERSION_STATE_PROBE_TOPOLOGY.set(topology_fingerprint.to_string()) {
|
|
||||||
Ok(()) => {}
|
|
||||||
Err(_)
|
|
||||||
if REMOTE_VERSION_STATE_PROBE_TOPOLOGY
|
|
||||||
.get()
|
|
||||||
.is_some_and(|current| current == topology_fingerprint) => {}
|
|
||||||
Err(_) => panic!("remote version state test topology is already bound to another fingerprint"),
|
|
||||||
}
|
|
||||||
let peer_epochs = BTreeMap::new();
|
|
||||||
if let Some(err) = publish_fleet_capability_probe_result(
|
|
||||||
remote_version_state_fleet_proof_slot(),
|
|
||||||
topology_fingerprint,
|
|
||||||
Ok(peer_epochs),
|
|
||||||
Instant::now(),
|
|
||||||
) {
|
|
||||||
panic!("test proof installation must not fail: {err}");
|
|
||||||
}
|
|
||||||
RemoteVersionStateFleetProofGuard
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_remote_version_state_peer(peer_epochs: &mut BTreeMap<String, Uuid>, peer: String, epoch: Uuid) -> Result<()> {
|
fn insert_remote_version_state_peer(peer_epochs: &mut BTreeMap<String, Uuid>, peer: String, epoch: Uuid) -> Result<()> {
|
||||||
if epoch.is_nil() || peer_epochs.values().any(|existing| *existing == epoch) || peer_epochs.insert(peer, epoch).is_some() {
|
if epoch.is_nil() || peer_epochs.values().any(|existing| *existing == epoch) || peer_epochs.insert(peer, epoch).is_some() {
|
||||||
return Err(Error::other("remote version state capability peer identity is invalid"));
|
return Err(Error::other("remote version state capability peer identity is invalid"));
|
||||||
@@ -291,11 +224,11 @@ fn insert_remote_version_state_peer(peer_epochs: &mut BTreeMap<String, Uuid>, pe
|
|||||||
pub fn start_remote_version_state_fleet_probe(topology_fingerprint: String) {
|
pub fn start_remote_version_state_fleet_probe(topology_fingerprint: String) {
|
||||||
if REMOTE_VERSION_STATE_PROBE_TOPOLOGY.set(topology_fingerprint.clone()).is_err() {
|
if REMOTE_VERSION_STATE_PROBE_TOPOLOGY.set(topology_fingerprint.clone()).is_err() {
|
||||||
if REMOTE_VERSION_STATE_PROBE_TOPOLOGY.get() != Some(&topology_fingerprint) {
|
if REMOTE_VERSION_STATE_PROBE_TOPOLOGY.get() != Some(&topology_fingerprint) {
|
||||||
for slot in [remote_version_state_fleet_proof_slot(), cross_pool_fence_fleet_proof_slot()] {
|
let mut state = remote_version_state_fleet_proof_slot()
|
||||||
let mut state = slot.write().unwrap_or_else(std::sync::PoisonError::into_inner);
|
.write()
|
||||||
state.topology_conflict = true;
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
state.proof = None;
|
state.topology_conflict = true;
|
||||||
}
|
state.proof = None;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -316,23 +249,13 @@ pub fn start_remote_version_state_fleet_probe(topology_fingerprint: String) {
|
|||||||
}
|
}
|
||||||
None => Err(Error::other("remote version state fleet capability notification system is unavailable")),
|
None => Err(Error::other("remote version state fleet capability notification system is unavailable")),
|
||||||
};
|
};
|
||||||
let fence_result = match get_global_notification_sys() {
|
|
||||||
Some(notification_sys) => timeout(
|
|
||||||
REMOTE_VERSION_STATE_PROBE_TIMEOUT,
|
|
||||||
notification_sys.probe_cross_pool_fence_fleet(&topology_fingerprint),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| Err(Error::other("cross-pool fence fleet capability probe timed out"))),
|
|
||||||
None => Err(Error::other("cross-pool fence fleet capability notification system is unavailable")),
|
|
||||||
};
|
|
||||||
let topology_conflict = remote_version_state_fleet_proof_slot()
|
let topology_conflict = remote_version_state_fleet_proof_slot()
|
||||||
.read()
|
.read()
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
.topology_conflict;
|
.topology_conflict;
|
||||||
if topology_conflict {
|
if topology_conflict {
|
||||||
replace_fleet_capability_proof(remote_version_state_fleet_proof_slot(), None);
|
replace_remote_version_state_fleet_proof(None);
|
||||||
replace_fleet_capability_proof(cross_pool_fence_fleet_proof_slot(), None);
|
} else if let Some(err) = publish_remote_version_state_probe_result(
|
||||||
} else if let Some(err) = publish_fleet_capability_probe_result(
|
|
||||||
remote_version_state_fleet_proof_slot(),
|
remote_version_state_fleet_proof_slot(),
|
||||||
&topology_fingerprint,
|
&topology_fingerprint,
|
||||||
result,
|
result,
|
||||||
@@ -340,24 +263,6 @@ pub fn start_remote_version_state_fleet_probe(topology_fingerprint: String) {
|
|||||||
) {
|
) {
|
||||||
debug!(error = %err, "remote version state fleet capability probe failed closed");
|
debug!(error = %err, "remote version state fleet capability probe failed closed");
|
||||||
}
|
}
|
||||||
if !topology_conflict
|
|
||||||
&& let Some(err) = publish_fleet_capability_probe_result(
|
|
||||||
cross_pool_fence_fleet_proof_slot(),
|
|
||||||
&topology_fingerprint,
|
|
||||||
fence_result,
|
|
||||||
Instant::now(),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
debug!(
|
|
||||||
event = EVENT_NOTIFICATION_CAPABILITY_PROBE,
|
|
||||||
component = LOG_COMPONENT_ECSTORE,
|
|
||||||
subsystem = LOG_SUBSYSTEM_NOTIFICATION,
|
|
||||||
capability = "cross_pool_fence_v1",
|
|
||||||
state = "failed_closed",
|
|
||||||
error = %err,
|
|
||||||
"notification capability probe"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
sleep(REMOTE_VERSION_STATE_PROBE_INTERVAL).await;
|
sleep(REMOTE_VERSION_STATE_PROBE_INTERVAL).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -425,27 +330,6 @@ impl NotificationSys {
|
|||||||
}
|
}
|
||||||
Ok(peer_epochs)
|
Ok(peer_epochs)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn probe_cross_pool_fence_fleet(&self, topology_fingerprint: &str) -> Result<BTreeMap<String, Uuid>> {
|
|
||||||
if self.peer_clients.len() != self.peer_topology_hosts.len() {
|
|
||||||
return Err(Error::other("cross-pool fence capability fleet membership is incomplete"));
|
|
||||||
}
|
|
||||||
let probes = self.peer_clients.iter().map(|client| async {
|
|
||||||
let client = client
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| Error::other("cross-pool fence capability peer is unreachable"))?;
|
|
||||||
client.probe_cross_pool_fence(topology_fingerprint.to_string()).await
|
|
||||||
});
|
|
||||||
let mut peer_epochs = BTreeMap::new();
|
|
||||||
for result in join_all(probes).await {
|
|
||||||
let (peer, version, epoch) = result?;
|
|
||||||
if version < CROSS_POOL_FENCE_SUPPORTED_VERSION {
|
|
||||||
return Err(Error::other("cross-pool fence capability version is unsupported"));
|
|
||||||
}
|
|
||||||
insert_remote_version_state_peer(&mut peer_epochs, peer, epoch)?;
|
|
||||||
}
|
|
||||||
Ok(peer_epochs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NotificationPeerErr {
|
pub struct NotificationPeerErr {
|
||||||
@@ -1623,7 +1507,6 @@ impl NotificationSys {
|
|||||||
workers.peers.remove(host);
|
workers.peers.remove(host);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "asserted by this file's tests (backlog#1823)")]
|
|
||||||
fn tier_config_reload_worker_active(&self, host: &str) -> bool {
|
fn tier_config_reload_worker_active(&self, host: &str) -> bool {
|
||||||
self.tier_config_reload_workers
|
self.tier_config_reload_workers
|
||||||
.lock()
|
.lock()
|
||||||
@@ -1797,7 +1680,6 @@ where
|
|||||||
.map_err(|_| Error::other(format!("scanner activity peer {host} timed out after {timeout_duration:?}")))?
|
.map_err(|_| Error::other(format!("scanner activity peer {host} timed out after {timeout_duration:?}")))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "asserted by this file's tests (backlog#1823)")]
|
|
||||||
async fn call_peer_with_timeout<F, Fut>(
|
async fn call_peer_with_timeout<F, Fut>(
|
||||||
timeout_dur: Duration,
|
timeout_dur: Duration,
|
||||||
host_label: &str,
|
host_label: &str,
|
||||||
@@ -2263,16 +2145,16 @@ mod tests {
|
|||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let mut peer_epochs = BTreeMap::new();
|
let mut peer_epochs = BTreeMap::new();
|
||||||
peer_epochs.insert("peer-a".to_string(), Uuid::new_v4());
|
peer_epochs.insert("peer-a".to_string(), Uuid::new_v4());
|
||||||
let proof = FleetCapabilityProof {
|
let proof = RemoteVersionStateFleetProof {
|
||||||
topology_fingerprint: "topology-a".to_string(),
|
topology_fingerprint: "topology-a".to_string(),
|
||||||
peer_epochs: Arc::new(peer_epochs),
|
peer_epochs: Arc::new(peer_epochs),
|
||||||
expires_at: now + Duration::from_secs(1),
|
expires_at: now + Duration::from_secs(1),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(fleet_capability_proof_valid_at(Some(&proof), "topology-a", now));
|
assert!(remote_version_state_fleet_proof_valid_at(Some(&proof), "topology-a", now));
|
||||||
assert!(!fleet_capability_proof_valid_at(Some(&proof), "topology-b", now));
|
assert!(!remote_version_state_fleet_proof_valid_at(Some(&proof), "topology-b", now));
|
||||||
assert!(!fleet_capability_proof_valid_at(Some(&proof), "topology-a", proof.expires_at));
|
assert!(!remote_version_state_fleet_proof_valid_at(Some(&proof), "topology-a", proof.expires_at));
|
||||||
assert!(!fleet_capability_proof_valid_at(None, "topology-a", now));
|
assert!(!remote_version_state_fleet_proof_valid_at(None, "topology-a", now));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2286,25 +2168,25 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn remote_version_state_fleet_proof_accepts_single_node_membership() {
|
fn remote_version_state_fleet_proof_accepts_single_node_membership() {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let proof = FleetCapabilityProof {
|
let proof = RemoteVersionStateFleetProof {
|
||||||
topology_fingerprint: "topology-a".to_string(),
|
topology_fingerprint: "topology-a".to_string(),
|
||||||
peer_epochs: Arc::new(BTreeMap::new()),
|
peer_epochs: Arc::new(BTreeMap::new()),
|
||||||
expires_at: now + Duration::from_secs(1),
|
expires_at: now + Duration::from_secs(1),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(fleet_capability_proof_valid_at(Some(&proof), "topology-a", now));
|
assert!(remote_version_state_fleet_proof_valid_at(Some(&proof), "topology-a", now));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn remote_version_state_fleet_proof_token_changes_with_process_epoch() {
|
fn remote_version_state_fleet_proof_token_changes_with_process_epoch() {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let proof = FleetCapabilityProof {
|
let proof = RemoteVersionStateFleetProof {
|
||||||
topology_fingerprint: "topology-a".to_string(),
|
topology_fingerprint: "topology-a".to_string(),
|
||||||
peer_epochs: Arc::new(BTreeMap::from([("peer-a".to_string(), Uuid::new_v4())])),
|
peer_epochs: Arc::new(BTreeMap::from([("peer-a".to_string(), Uuid::new_v4())])),
|
||||||
expires_at: now + Duration::from_secs(1),
|
expires_at: now + Duration::from_secs(1),
|
||||||
};
|
};
|
||||||
let captured = proof.token();
|
let captured = proof.token();
|
||||||
let restarted = FleetCapabilityProof {
|
let restarted = RemoteVersionStateFleetProof {
|
||||||
topology_fingerprint: proof.topology_fingerprint.clone(),
|
topology_fingerprint: proof.topology_fingerprint.clone(),
|
||||||
peer_epochs: Arc::new(BTreeMap::from([("peer-a".to_string(), Uuid::new_v4())])),
|
peer_epochs: Arc::new(BTreeMap::from([("peer-a".to_string(), Uuid::new_v4())])),
|
||||||
expires_at: proof.expires_at,
|
expires_at: proof.expires_at,
|
||||||
@@ -2315,11 +2197,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn remote_version_state_fleet_proof_renewal_preserves_only_same_epoch_token() {
|
fn remote_version_state_fleet_proof_renewal_preserves_only_same_epoch_token() {
|
||||||
let slot = std::sync::RwLock::new(FleetCapabilityProofState::default());
|
let slot = std::sync::RwLock::new(RemoteVersionStateFleetProofState::default());
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let epoch = Uuid::new_v4();
|
let epoch = Uuid::new_v4();
|
||||||
let peers = BTreeMap::from([("peer-a".to_string(), epoch)]);
|
let peers = BTreeMap::from([("peer-a".to_string(), epoch)]);
|
||||||
assert!(publish_fleet_capability_probe_result(&slot, "topology-a", Ok(peers.clone()), now).is_none());
|
assert!(publish_remote_version_state_probe_result(&slot, "topology-a", Ok(peers.clone()), now).is_none());
|
||||||
let original = slot
|
let original = slot
|
||||||
.read()
|
.read()
|
||||||
.expect("proof slot should not poison")
|
.expect("proof slot should not poison")
|
||||||
@@ -2328,7 +2210,9 @@ mod tests {
|
|||||||
.expect("successful probe should publish proof")
|
.expect("successful probe should publish proof")
|
||||||
.token();
|
.token();
|
||||||
|
|
||||||
assert!(publish_fleet_capability_probe_result(&slot, "topology-a", Ok(peers), now + Duration::from_millis(1)).is_none());
|
assert!(
|
||||||
|
publish_remote_version_state_probe_result(&slot, "topology-a", Ok(peers), now + Duration::from_millis(1)).is_none()
|
||||||
|
);
|
||||||
let renewed = slot
|
let renewed = slot
|
||||||
.read()
|
.read()
|
||||||
.expect("proof slot should not poison")
|
.expect("proof slot should not poison")
|
||||||
@@ -2340,7 +2224,8 @@ mod tests {
|
|||||||
|
|
||||||
let restarted = BTreeMap::from([("peer-a".to_string(), Uuid::new_v4())]);
|
let restarted = BTreeMap::from([("peer-a".to_string(), Uuid::new_v4())]);
|
||||||
assert!(
|
assert!(
|
||||||
publish_fleet_capability_probe_result(&slot, "topology-a", Ok(restarted), now + Duration::from_millis(2)).is_none()
|
publish_remote_version_state_probe_result(&slot, "topology-a", Ok(restarted), now + Duration::from_millis(2))
|
||||||
|
.is_none()
|
||||||
);
|
);
|
||||||
let replaced = slot
|
let replaced = slot
|
||||||
.read()
|
.read()
|
||||||
@@ -2355,18 +2240,18 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn remote_version_state_fleet_proof_conflict_revokes_atomic_snapshot() {
|
fn remote_version_state_fleet_proof_conflict_revokes_atomic_snapshot() {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let mut state = FleetCapabilityProofState {
|
let mut state = RemoteVersionStateFleetProofState {
|
||||||
proof: Some(FleetCapabilityProof {
|
proof: Some(RemoteVersionStateFleetProof {
|
||||||
topology_fingerprint: "topology-a".to_string(),
|
topology_fingerprint: "topology-a".to_string(),
|
||||||
peer_epochs: Arc::new(BTreeMap::new()),
|
peer_epochs: Arc::new(BTreeMap::new()),
|
||||||
expires_at: now + Duration::from_secs(1),
|
expires_at: now + Duration::from_secs(1),
|
||||||
}),
|
}),
|
||||||
topology_conflict: false,
|
topology_conflict: false,
|
||||||
};
|
};
|
||||||
assert!(acquire_fleet_capability_proof_from(&state, "topology-a", now).is_some());
|
assert!(acquire_remote_version_state_fleet_proof_from(&state, "topology-a", now).is_some());
|
||||||
|
|
||||||
state.topology_conflict = true;
|
state.topology_conflict = true;
|
||||||
assert!(acquire_fleet_capability_proof_from(&state, "topology-a", now).is_none());
|
assert!(acquire_remote_version_state_fleet_proof_from(&state, "topology-a", now).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2382,19 +2267,19 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn remote_version_state_fleet_probe_failure_revokes_previous_proof() {
|
fn remote_version_state_fleet_probe_failure_revokes_previous_proof() {
|
||||||
let slot = std::sync::RwLock::new(FleetCapabilityProofState::default());
|
let slot = std::sync::RwLock::new(RemoteVersionStateFleetProofState::default());
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let peer_epochs = BTreeMap::from([("node-a:9000".to_string(), Uuid::new_v4())]);
|
let peer_epochs = BTreeMap::from([("node-a:9000".to_string(), Uuid::new_v4())]);
|
||||||
assert!(publish_fleet_capability_probe_result(&slot, "topology-a", Ok(peer_epochs), now).is_none());
|
assert!(publish_remote_version_state_probe_result(&slot, "topology-a", Ok(peer_epochs), now).is_none());
|
||||||
assert!(slot.read().expect("proof slot should not poison").proof.is_some());
|
assert!(slot.read().expect("proof slot should not poison").proof.is_some());
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
publish_fleet_capability_probe_result(&slot, "topology-a", Err(Error::other("peer unavailable")), now,).is_some()
|
publish_remote_version_state_probe_result(&slot, "topology-a", Err(Error::other("peer unavailable")), now,).is_some()
|
||||||
);
|
);
|
||||||
assert!(slot.read().expect("proof slot should not poison").proof.is_none());
|
assert!(slot.read().expect("proof slot should not poison").proof.is_none());
|
||||||
|
|
||||||
let peer_epochs = BTreeMap::from([("node-a:9000".to_string(), Uuid::new_v4())]);
|
let peer_epochs = BTreeMap::from([("node-a:9000".to_string(), Uuid::new_v4())]);
|
||||||
assert!(publish_fleet_capability_probe_result(&slot, "topology-a", Ok(peer_epochs), now).is_none());
|
assert!(publish_remote_version_state_probe_result(&slot, "topology-a", Ok(peer_epochs), now).is_none());
|
||||||
assert!(slot.read().expect("proof slot should not poison").proof.is_some());
|
assert!(slot.read().expect("proof slot should not poison").proof.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -864,10 +864,6 @@ pub(super) fn merge_rebalance_meta(remote: &mut RebalanceMeta, local: &Rebalance
|
|||||||
RebalanceMetaMergeOutcome::Merged
|
RebalanceMetaMergeOutcome::Merged
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "stop-transition helper retained beside stop_rebalance_meta_snapshot; no caller yet (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(super) fn mark_started_rebalance_pools_stopped(meta: &mut RebalanceMeta, stop_time: OffsetDateTime) {
|
pub(super) fn mark_started_rebalance_pools_stopped(meta: &mut RebalanceMeta, stop_time: OffsetDateTime) {
|
||||||
for pool_stat in meta.pool_stats.iter_mut() {
|
for pool_stat in meta.pool_stats.iter_mut() {
|
||||||
if pool_stat.info.status == RebalStatus::Started {
|
if pool_stat.info.status == RebalStatus::Started {
|
||||||
@@ -968,7 +964,6 @@ pub(super) fn rollback_rebalance_start_meta_snapshot_for_id(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "asserted by this file's tests (backlog#1823)")]
|
|
||||||
pub(super) fn stop_rebalance_meta_snapshot(meta: Option<&mut RebalanceMeta>, now: OffsetDateTime) -> Option<RebalanceMeta> {
|
pub(super) fn stop_rebalance_meta_snapshot(meta: Option<&mut RebalanceMeta>, now: OffsetDateTime) -> Option<RebalanceMeta> {
|
||||||
let meta = meta?;
|
let meta = meta?;
|
||||||
stop_rebalance_state(meta, now);
|
stop_rebalance_state(meta, now);
|
||||||
|
|||||||
@@ -171,7 +171,6 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
#[allow(dead_code, reason = "asserted by this file's tests (backlog#1823)")]
|
|
||||||
pub(super) async fn migrate_entry_version_with_retry_wait<Backend, F, Fut, D, DFut, W, WFut>(
|
pub(super) async fn migrate_entry_version_with_retry_wait<Backend, F, Fut, D, DFut, W, WFut>(
|
||||||
set: &Backend,
|
set: &Backend,
|
||||||
bucket: String,
|
bucket: String,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Arc;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
@@ -31,6 +32,8 @@ pub struct RebalanceStats {
|
|||||||
pub cleanup_warnings: RebalanceCleanupWarnings,
|
pub cleanup_warnings: RebalanceCleanupWarnings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type RStats = Vec<Arc<RebalanceStats>>;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub(super) struct RebalanceBucketConfigs {
|
pub(super) struct RebalanceBucketConfigs {
|
||||||
pub(super) bucket_incarnation_id: Option<uuid::Uuid>,
|
pub(super) bucket_incarnation_id: Option<uuid::Uuid>,
|
||||||
|
|||||||
@@ -30,5 +30,6 @@ pub mod warm_backend_minio;
|
|||||||
pub mod warm_backend_r2;
|
pub mod warm_backend_r2;
|
||||||
pub mod warm_backend_rustfs;
|
pub mod warm_backend_rustfs;
|
||||||
pub mod warm_backend_s3;
|
pub mod warm_backend_s3;
|
||||||
|
pub mod warm_backend_s3sdk;
|
||||||
pub mod warm_backend_tencent;
|
pub mod warm_backend_tencent;
|
||||||
pub mod warm_backend_wasabi;
|
pub mod warm_backend_wasabi;
|
||||||
|
|||||||
@@ -488,7 +488,6 @@ impl TierCandidateMutation {
|
|||||||
targets
|
targets
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "asserted by this file's tests (backlog#1823)")]
|
|
||||||
fn affected_targets(
|
fn affected_targets(
|
||||||
&self,
|
&self,
|
||||||
manager: &TierConfigMgr,
|
manager: &TierConfigMgr,
|
||||||
@@ -803,7 +802,6 @@ fn tier_persisted_reference_blocks_any_target(
|
|||||||
.any(|target| tier_persisted_reference_blocks_target(tier_name, backend_identity, target))
|
.any(|target| tier_persisted_reference_blocks_target(tier_name, backend_identity, target))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "asserted by this file's tests (backlog#1823)")]
|
|
||||||
fn tier_object_blocks_target_rebind(object: &ObjectInfo, target: &TierMutationIntentTarget) -> io::Result<bool> {
|
fn tier_object_blocks_target_rebind(object: &ObjectInfo, target: &TierMutationIntentTarget) -> io::Result<bool> {
|
||||||
tier_object_blocks_any_target_rebind(object, std::slice::from_ref(target))
|
tier_object_blocks_any_target_rebind(object, std::slice::from_ref(target))
|
||||||
}
|
}
|
||||||
@@ -2728,6 +2726,14 @@ impl TierConfigMgr {
|
|||||||
Self::publish_candidate_owned(handle, candidate, driver_tier.map(str::to_string), update).await
|
Self::publish_candidate_owned(handle, candidate, driver_tier.map(str::to_string), update).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn begin_publish_transition(
|
||||||
|
handle: &Arc<RwLock<Self>>,
|
||||||
|
manager: &mut Self,
|
||||||
|
candidate: &Self,
|
||||||
|
) -> std::result::Result<TierPublishTransition, AdminError> {
|
||||||
|
Self::begin_publish_transition_with_allowed_mutation_blocks(handle, manager, candidate, None)
|
||||||
|
}
|
||||||
|
|
||||||
fn begin_publish_transition_with_allowed_mutation_blocks(
|
fn begin_publish_transition_with_allowed_mutation_blocks(
|
||||||
handle: &Arc<RwLock<Self>>,
|
handle: &Arc<RwLock<Self>>,
|
||||||
manager: &mut Self,
|
manager: &mut Self,
|
||||||
@@ -2813,6 +2819,14 @@ impl TierConfigMgr {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn publish_candidate_inner(
|
||||||
|
handle: &Arc<RwLock<Self>>,
|
||||||
|
candidate: Self,
|
||||||
|
driver_tier: Option<&str>,
|
||||||
|
) -> std::result::Result<(), AdminError> {
|
||||||
|
Self::publish_candidate_inner_with_allowed_mutation_blocks(handle, candidate, driver_tier, None).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn publish_candidate_inner_with_allowed_mutation_blocks(
|
async fn publish_candidate_inner_with_allowed_mutation_blocks(
|
||||||
handle: &Arc<RwLock<Self>>,
|
handle: &Arc<RwLock<Self>>,
|
||||||
candidate: Self,
|
candidate: Self,
|
||||||
@@ -2925,7 +2939,6 @@ impl TierConfigMgr {
|
|||||||
admin_err
|
admin_err
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "reached only through #[cfg(test)] helpers in this file (backlog#1823)")]
|
|
||||||
async fn publish_candidate_owned(
|
async fn publish_candidate_owned(
|
||||||
handle: &Arc<RwLock<Self>>,
|
handle: &Arc<RwLock<Self>>,
|
||||||
candidate: Self,
|
candidate: Self,
|
||||||
@@ -3528,7 +3541,6 @@ impl TierConfigMgr {
|
|||||||
Self::update_candidate_with_config_lock(handle, api, TierCandidateMutation::Remove(tier_name.to_string(), force)).await
|
Self::update_candidate_with_config_lock(handle, api, TierCandidateMutation::Remove(tier_name.to_string(), force)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "reached only through #[cfg(test)] helpers in this file (backlog#1823)")]
|
|
||||||
async fn remove_and_save_with<S>(
|
async fn remove_and_save_with<S>(
|
||||||
handle: &Arc<RwLock<Self>>,
|
handle: &Arc<RwLock<Self>>,
|
||||||
api: Arc<S>,
|
api: Arc<S>,
|
||||||
@@ -3562,7 +3574,6 @@ impl TierConfigMgr {
|
|||||||
Self::update_candidate_with_config_lock(handle, api, TierCandidateMutation::Clear(force)).await
|
Self::update_candidate_with_config_lock(handle, api, TierCandidateMutation::Clear(force)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "reached only through #[cfg(test)] helpers in this file (backlog#1823)")]
|
|
||||||
async fn clear_and_save_with<S>(
|
async fn clear_and_save_with<S>(
|
||||||
handle: &Arc<RwLock<Self>>,
|
handle: &Arc<RwLock<Self>>,
|
||||||
api: Arc<S>,
|
api: Arc<S>,
|
||||||
@@ -3601,10 +3612,6 @@ impl TierConfigMgr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "lease accounting asserted by a bucket_lifecycle_ops test behind `--features test-util` (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) async fn active_operation_lease_count(handle: &Arc<RwLock<Self>>, tier_name: &str) -> usize {
|
pub(crate) async fn active_operation_lease_count(handle: &Arc<RwLock<Self>>, tier_name: &str) -> usize {
|
||||||
let manager = handle.read().await;
|
let manager = handle.read().await;
|
||||||
let Some(runtime) = registered_tier_driver_runtime(&manager) else {
|
let Some(runtime) = registered_tier_driver_runtime(&manager) else {
|
||||||
@@ -3710,6 +3717,10 @@ impl TierConfigMgr {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn retire_driver(&mut self, tier_name: &str) {
|
||||||
|
self.revoke_driver(tier_name);
|
||||||
|
}
|
||||||
|
|
||||||
fn revoke_all_drivers(&mut self) {
|
fn revoke_all_drivers(&mut self) {
|
||||||
if let Some(runtime) = registered_tier_driver_runtime(self) {
|
if let Some(runtime) = registered_tier_driver_runtime(self) {
|
||||||
let mut runtime = lock_unpoisoned(&runtime);
|
let mut runtime = lock_unpoisoned(&runtime);
|
||||||
@@ -3873,7 +3884,6 @@ impl TierConfigMgr {
|
|||||||
self.save_config(api, &config_file, data).await
|
self.save_config(api, &config_file, data).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "reached only through #[cfg(test)] helpers in this file (backlog#1823)")]
|
|
||||||
async fn save_tiering_config_if_current<S>(
|
async fn save_tiering_config_if_current<S>(
|
||||||
&self,
|
&self,
|
||||||
api: Arc<S>,
|
api: Arc<S>,
|
||||||
|
|||||||
@@ -305,10 +305,6 @@ impl TierMutationIntent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "intent-record persistence asserted by store::init tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) fn tier_mutation_intent_record_object_name(mutation_id: Uuid) -> Result<String> {
|
pub(crate) fn tier_mutation_intent_record_object_name(mutation_id: Uuid) -> Result<String> {
|
||||||
tier_mutation_intent_record_object_name_with_prefix(TIER_MUTATION_INTENT_RECORD_PREFIX, mutation_id)
|
tier_mutation_intent_record_object_name_with_prefix(TIER_MUTATION_INTENT_RECORD_PREFIX, mutation_id)
|
||||||
}
|
}
|
||||||
@@ -321,10 +317,6 @@ fn tier_mutation_intent_record_object_name_with_prefix(prefix: &str, mutation_id
|
|||||||
Ok(format!("{}/{}/{}/{}.json", prefix, &mutation_key[..2], &mutation_key[2..4], mutation_key))
|
Ok(format!("{}/{}/{}/{}.json", prefix, &mutation_key[..2], &mutation_key[2..4], mutation_key))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "intent-record persistence asserted by store::init tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) fn tier_mutation_intent_id_from_record_object_name(object: &str) -> Result<Uuid> {
|
pub(crate) fn tier_mutation_intent_id_from_record_object_name(object: &str) -> Result<Uuid> {
|
||||||
tier_mutation_intent_id_from_record_object_name_with_prefix(TIER_MUTATION_INTENT_RECORD_PREFIX, object)
|
tier_mutation_intent_id_from_record_object_name_with_prefix(TIER_MUTATION_INTENT_RECORD_PREFIX, object)
|
||||||
}
|
}
|
||||||
@@ -363,10 +355,6 @@ fn tier_mutation_intent_id_from_record_object_name_with_prefix(prefix: &str, obj
|
|||||||
Uuid::parse_str(mutation_key).map_err(|_| TierMutationIntentError::Corrupt("intent record path has invalid uuid"))
|
Uuid::parse_str(mutation_key).map_err(|_| TierMutationIntentError::Corrupt("intent record path has invalid uuid"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "intent-record persistence asserted by store::init tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) async fn save_tier_mutation_intent_record<S>(api: Arc<S>, intent: &TierMutationIntent) -> EcstoreResult<()>
|
pub(crate) async fn save_tier_mutation_intent_record<S>(api: Arc<S>, intent: &TierMutationIntent) -> EcstoreResult<()>
|
||||||
where
|
where
|
||||||
S: EcstoreObjectIO,
|
S: EcstoreObjectIO,
|
||||||
@@ -458,10 +446,6 @@ where
|
|||||||
Ok((intent, etag))
|
Ok((intent, etag))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
|
||||||
dead_code,
|
|
||||||
reason = "intent-record persistence asserted by store::init tests (backlog#1823)"
|
|
||||||
)]
|
|
||||||
pub(crate) async fn save_tier_mutation_intent_record_if_current<S>(
|
pub(crate) async fn save_tier_mutation_intent_record_if_current<S>(
|
||||||
api: Arc<S>,
|
api: Arc<S>,
|
||||||
intent: &TierMutationIntent,
|
intent: &TierMutationIntent,
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ use crate::services::tier::{
|
|||||||
};
|
};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
|
const MAX_MULTIPART_PUT_OBJECT_SIZE: i64 = 1024 * 1024 * 1024 * 1024 * 5;
|
||||||
|
const MAX_PARTS_COUNT: i64 = 10000;
|
||||||
const _MAX_PART_SIZE: i64 = 1024 * 1024 * 1024 * 5;
|
const _MAX_PART_SIZE: i64 = 1024 * 1024 * 1024 * 5;
|
||||||
|
const MIN_PART_SIZE: i64 = 1024 * 1024 * 128;
|
||||||
|
|
||||||
fn parse_generation(remote_version: &str) -> Result<Option<i64>, Error> {
|
fn parse_generation(remote_version: &str) -> Result<Option<i64>, Error> {
|
||||||
if remote_version.is_empty() {
|
if remote_version.is_empty() {
|
||||||
@@ -61,6 +64,7 @@ pub struct WarmBackendGCS {
|
|||||||
pub control: Arc<StorageControl>,
|
pub control: Arc<StorageControl>,
|
||||||
pub bucket: String,
|
pub bucket: String,
|
||||||
pub prefix: String,
|
pub prefix: String,
|
||||||
|
pub storage_class: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WarmBackendGCS {
|
impl WarmBackendGCS {
|
||||||
@@ -100,6 +104,7 @@ impl WarmBackendGCS {
|
|||||||
control,
|
control,
|
||||||
bucket: conf.bucket.clone(),
|
bucket: conf.bucket.clone(),
|
||||||
prefix: conf.prefix.strip_suffix("/").unwrap_or(&conf.prefix).to_owned(),
|
prefix: conf.prefix.strip_suffix("/").unwrap_or(&conf.prefix).to_owned(),
|
||||||
|
storage_class: "".to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user