fix(storage): expose truthful storage class capabilities (#5172)

This commit is contained in:
cxymds
2026-07-24 15:28:03 +08:00
committed by GitHub
parent 6765aca3f9
commit 358caa23cb
19 changed files with 835 additions and 81 deletions
+3
View File
@@ -75,6 +75,9 @@ Default parity by drive count — `default_parity_count(N)` ([storageclass.rs](.
Two storage classes: `STANDARD` (SC) and `REDUCED_REDUNDANCY` (RRS) ([storageclass.rs](../../crates/ecstore/src/config/storageclass.rs)), configured as `"EC:<parity>"` via the `standard` / `rrs` config keys or the `RUSTFS_STORAGE_CLASS_STANDARD` / `RUSTFS_STORAGE_CLASS_RRS` env overrides. Absent config falls back to `default_parity_count` (SC) and `1`, or `0` on a single drive (RRS).
- **INVARIANT — truthful client write classes.** S3 PUT, CopyObject, and CreateMultipartUpload accept only `STANDARD` and `REDUCED_REDUNDANCY`. AWS labels such as `STANDARD_IA`, `ONEZONE_IA`, `INTELLIGENT_TIERING`, `GLACIER`, and `DEEP_ARCHIVE` are rejected with `InvalidStorageClass` because RustFS does not implement their advertised access, retrieval, or archival semantics. The supported allowlist and stable error identifier are owned by [storageclass.rs](../../crates/ecstore/src/config/storageclass.rs), not duplicated by individual handlers.
- **INVARIANT — historical label normalization.** Non-transitioned objects written by older RustFS versions may contain an AWS storage-class label without distinct physical semantics. Read and listing responses report the effective local layout (`STANDARD`, or `REDUCED_REDUNDANCY` when that layout was selected) instead of repeating a label-only promise. A completed lifecycle transition is different: its real transition tier name is preserved. Persistent metadata-fast list snapshots use the `rustfs-listobjects-key-only-v2` header; older or unknown snapshot formats are rebuilt from normalized `ObjectInfo` values instead of being served, because their stored label lacks enough information to distinguish historical metadata from a real transition tier.
- **INVARIANT — parity bounds.** Parity must satisfy `parity ≤ N/2` for both classes, and `SC parity ≥ RRS parity` when both are non-zero ([storageclass.rs](../../crates/ecstore/src/config/storageclass.rs), `validate_parity` / `validate_parity_inner`). Enforcement nuance to be aware of: `validate_parity_inner` (the path a user-configured `EC:<parity>` storage class flows through) only applies the `parity ≤ N/2` check for `N > 2`, so degenerate small-set values (e.g. `EC:2` on `N = 2`, giving `data_blocks = 0`) are not caught there; the standalone `validate_parity` enforces the bound unconditionally but is applied only to the resolved default parity. A change that lets user-configured parity reach a write path must not assume the `≤ N/2` bound was enforced for `N ≤ 2`. Parity `0` is permitted (single-drive / capacity setups); there is no non-zero minimum.
- **INVARIANT — per-pool validity.** Each pool's resolved parity must be valid for **that pool's own drive count**. A heterogeneous deployment (pools of different widths) must resolve parity per pool; applying one pool's parity to a narrower pool can drive `data_blocks = N parity` to `0` and make encoding impossible.
- Baseline defect: `main` computes `common_parity_drives` from the **first** pool only and applies it to every pool ([store/init.rs](../../crates/ecstore/src/store/init.rs), `ec_drives_no_config` at [store/init_format.rs](../../crates/ecstore/src/store/init_format.rs)); this is issue #4801 (a smaller later pool panics with `TooFewDataShards`). The correct rule is per-pool resolution.
@@ -59,3 +59,33 @@ owners through read-only providers:
Unsupported or unavailable runtime capabilities are reported as `unsupported`
or `unknown` contract states instead of activating fallback behavior.
## Storage-Class Write Contract
Authenticated clients discover the storage-class write contract from
`GET /rustfs/admin/v4/runtime/capabilities`. The additive
`storage_classes` object is versioned independently from the route:
```json
{
"storage_classes": {
"contract_version": 1,
"supported_write_classes": ["STANDARD", "REDUCED_REDUNDANCY"],
"unsupported_write_error": "InvalidStorageClass",
"legacy_label_behavior": "normalized_to_effective_class"
}
}
```
`supported_write_classes` is the complete client-selectable write allowlist.
Any other value fails before object or multipart mutation with the stable S3
error named by `unsupported_write_error`. `legacy_label_behavior` means
non-transitioned historical label-only metadata is reported as its effective
local class; actual lifecycle transition tier names remain unchanged.
The values are sourced from
[`crates/ecstore/src/config/storageclass.rs`](../../crates/ecstore/src/config/storageclass.rs),
which also owns write validation and response normalization. Consumers must
branch on `contract_version` before assigning meaning to future fields. The
admin route continues to require `ServerInfoAdminAction`; capability discovery
does not weaken authentication or authorization.
+2 -1
View File
@@ -79,10 +79,11 @@
| snowball_auto_extract_test | 6 | |
| special_chars_test | 14 | ✅ |
| stale_multipart_cleanup_cluster_test | 1 | |
| storage_class_capability_test | 4 | ✅ |
| tls_gen | 3 | |
| tls_hot_reload_test | 1 | ✅ |
| version_id_regression_test | 10 | ✅ |
`notification_webhook_test` also has 1 ignored store-and-forward regression tracked by rustfs#4852; ignored tests are excluded from the active counts above.
**Total listed: 475 tests across 65 modules · PR smoke subset: 122 tests / 30 modules** (28 full modules + 4 `reliant` tests + 20 of `replication_extension_test`) **· nightly `e2e-repl-nightly`: 27 tests** · generated 2026-07-24.
**Total listed: 479 tests across 66 modules · PR smoke subset: 126 tests / 31 modules** (29 full modules + 4 `reliant` tests + 20 of `replication_extension_test`) **· nightly `e2e-repl-nightly`: 27 tests** · generated 2026-07-24.