refactor(storage): narrow metadata bounds (#3343)

This commit is contained in:
安正超
2026-06-11 12:04:02 +08:00
committed by GitHub
parent 6b86e3875c
commit 686068d8bd
5 changed files with 108 additions and 94 deletions
+15 -10
View File
@@ -17,7 +17,9 @@
use crate::bucket::metadata::BUCKET_METADATA_FILE;
use crate::bucket::replication::{decode_resync_file, encode_resync_file};
use crate::disk::{BUCKET_META_PREFIX, MIGRATING_META_BUCKET, RUSTFS_META_BUCKET};
use crate::store_api::{BucketOptions, ObjectOptions, PutObjReader, StorageAPI};
use crate::store_api::{
BucketOperations, BucketOptions, ListOperations, ObjectIO, ObjectOperations, ObjectOptions, PutObjReader,
};
use http::HeaderMap;
use rustfs_policy::auth::UserIdentity;
use rustfs_policy::policy::PolicyDoc;
@@ -176,7 +178,10 @@ fn normalize_bucket_meta_blob(path: &str, data: &[u8]) -> std::result::Result<Op
/// Uses list_bucket (from disk volumes) to get bucket names, since list_objects_v2 on the legacy
/// meta bucket may not work (legacy format differs from object layer expectations).
/// Skips buckets that already exist in RustFS (idempotent).
pub async fn try_migrate_bucket_metadata<S: StorageAPI>(store: Arc<S>) {
pub async fn try_migrate_bucket_metadata<S>(store: Arc<S>)
where
S: BucketOperations + ObjectIO + ObjectOperations,
{
let buckets_list = match store
.list_bucket(&BucketOptions {
no_metadata: true,
@@ -218,13 +223,10 @@ pub async fn try_migrate_bucket_metadata<S: StorageAPI>(store: Arc<S>) {
}
}
async fn migrate_one_if_missing<S: StorageAPI>(
store: Arc<S>,
opts: &ObjectOptions,
headers: &HeaderMap,
path: &str,
label: &str,
) {
async fn migrate_one_if_missing<S>(store: Arc<S>, opts: &ObjectOptions, headers: &HeaderMap, path: &str, label: &str)
where
S: ObjectIO + ObjectOperations,
{
if store
.get_object_info(RUSTFS_META_BUCKET, path, &ObjectOptions::default())
.await
@@ -275,7 +277,10 @@ async fn migrate_one_if_missing<S: StorageAPI>(
/// Lists all objects under the IAM prefix in the source, copies each to the target if not present.
/// Skips objects that already exist in RustFS (idempotent).
/// If list_objects_v2 on the legacy bucket fails (e.g. format differs), migration is skipped.
pub async fn try_migrate_iam_config<S: StorageAPI>(store: Arc<S>) {
pub async fn try_migrate_iam_config<S>(store: Arc<S>)
where
S: ListOperations + ObjectIO + ObjectOperations,
{
let opts = ObjectOptions {
max_parity: true,
no_lock: true,
+19 -16
View File
@@ -16,7 +16,7 @@ use crate::config::{Config, KVS, audit, notify, oidc, set_global_storage_class,
use crate::disk::{MIGRATING_META_BUCKET, RUSTFS_META_BUCKET};
use crate::error::{Error, Result};
use crate::global::is_first_cluster_node_local;
use crate::store_api::{ObjectInfo, ObjectOptions, PutObjReader, StorageAPI};
use crate::store_api::{ObjectIO, ObjectInfo, ObjectOperations, ObjectOptions, PutObjReader};
use http::HeaderMap;
use rustfs_config::audit::{
AUDIT_AMQP_KEYS, AUDIT_AMQP_SUB_SYS, AUDIT_KAFKA_KEYS, AUDIT_KAFKA_SUB_SYS, AUDIT_MQTT_KEYS, AUDIT_MQTT_SUB_SYS,
@@ -181,12 +181,12 @@ fn audit_target_descriptors() -> [TargetConfigDescriptor; 9] {
}
#[instrument(skip(api))]
pub async fn read_config<S: StorageAPI>(api: Arc<S>, file: &str) -> Result<Vec<u8>> {
pub async fn read_config<S: ObjectIO>(api: Arc<S>, file: &str) -> Result<Vec<u8>> {
let (data, _obj) = read_config_with_metadata(api, file, &ObjectOptions::default()).await?;
Ok(data)
}
pub async fn read_config_no_lock<S: StorageAPI>(api: Arc<S>, file: &str) -> Result<Vec<u8>> {
pub async fn read_config_no_lock<S: ObjectIO>(api: Arc<S>, file: &str) -> Result<Vec<u8>> {
let (data, _obj) = read_config_with_metadata(
api,
file,
@@ -199,7 +199,7 @@ pub async fn read_config_no_lock<S: StorageAPI>(api: Arc<S>, file: &str) -> Resu
Ok(data)
}
pub async fn read_config_with_metadata<S: StorageAPI>(
pub async fn read_config_with_metadata<S: ObjectIO>(
api: Arc<S>,
file: &str,
opts: &ObjectOptions,
@@ -227,7 +227,7 @@ pub async fn read_config_with_metadata<S: StorageAPI>(
}
#[instrument(skip(api, data))]
pub async fn save_config<S: StorageAPI>(api: Arc<S>, file: &str, data: Vec<u8>) -> Result<()> {
pub async fn save_config<S: ObjectIO>(api: Arc<S>, file: &str, data: Vec<u8>) -> Result<()> {
save_config_with_opts(
api,
file,
@@ -241,7 +241,7 @@ pub async fn save_config<S: StorageAPI>(api: Arc<S>, file: &str, data: Vec<u8>)
}
#[instrument(skip(api))]
pub async fn delete_config<S: StorageAPI>(api: Arc<S>, file: &str) -> Result<()> {
pub async fn delete_config<S: ObjectOperations>(api: Arc<S>, file: &str) -> Result<()> {
match api
.delete_object(
RUSTFS_META_BUCKET,
@@ -265,7 +265,7 @@ pub async fn delete_config<S: StorageAPI>(api: Arc<S>, file: &str) -> Result<()>
}
}
pub async fn save_config_with_opts<S: StorageAPI>(api: Arc<S>, file: &str, data: Vec<u8>, opts: &ObjectOptions) -> Result<()> {
pub async fn save_config_with_opts<S: ObjectIO>(api: Arc<S>, file: &str, data: Vec<u8>, opts: &ObjectOptions) -> Result<()> {
let mut put_data = PutObjReader::from_vec(data);
if let Err(err) = api.put_object(RUSTFS_META_BUCKET, file, &mut put_data, opts).await {
error!("save_config_with_opts: err: {:?}, file: {}", err, file);
@@ -280,7 +280,7 @@ fn new_server_config() -> Config {
async fn new_and_save_server_config<S>(api: Arc<S>) -> Result<Config>
where
S: StorageAPI + StorageAdminApi,
S: ObjectIO + StorageAdminApi,
{
let mut cfg = new_server_config();
lookup_configs(&mut cfg, api.clone()).await;
@@ -982,7 +982,10 @@ fn is_object_not_found(err: &Error) -> bool {
*err == Error::FileNotFound || matches!(err, Error::ObjectNotFound(_, _) | Error::BucketNotFound(_))
}
pub async fn try_migrate_server_config<S: StorageAPI>(api: Arc<S>) {
pub async fn try_migrate_server_config<S>(api: Arc<S>)
where
S: ObjectIO + ObjectOperations,
{
let config_file = get_config_file();
match api
.get_object_info(
@@ -1065,7 +1068,7 @@ pub async fn try_migrate_server_config<S: StorageAPI>(api: Arc<S>) {
/// Handle the situation where the configuration file does not exist, create and save a new configuration
async fn handle_missing_config<S>(api: Arc<S>, context: &str) -> Result<Config>
where
S: StorageAPI + StorageAdminApi,
S: ObjectIO + StorageAdminApi,
{
warn!("Configuration not found ({}): Start initializing new configuration", context);
let cfg = if is_first_cluster_node_local().await {
@@ -1087,7 +1090,7 @@ fn handle_config_read_error(err: Error, file_path: &str) -> Result<Config> {
pub async fn read_config_without_migrate<S>(api: Arc<S>) -> Result<Config>
where
S: StorageAPI + StorageAdminApi,
S: ObjectIO + StorageAdminApi,
{
let config_file = get_config_file();
@@ -1101,7 +1104,7 @@ where
async fn read_server_config<S>(api: Arc<S>, data: &[u8]) -> Result<Config>
where
S: StorageAPI + StorageAdminApi,
S: ObjectIO + StorageAdminApi,
{
// If the provided data is empty, try to read from the file again
if data.is_empty() {
@@ -1125,7 +1128,7 @@ where
Ok(cfg.merge())
}
pub async fn save_server_config<S: StorageAPI>(api: Arc<S>, cfg: &Config) -> Result<()> {
pub async fn save_server_config<S: ObjectIO>(api: Arc<S>, cfg: &Config) -> Result<()> {
let config_file = get_config_file();
let existing = match read_config(api.clone(), &config_file).await {
Ok(v) => Some(v),
@@ -1156,7 +1159,7 @@ pub async fn save_server_config<S: StorageAPI>(api: Arc<S>, cfg: &Config) -> Res
pub async fn lookup_configs<S>(cfg: &mut Config, api: Arc<S>)
where
S: StorageAPI + StorageAdminApi,
S: StorageAdminApi,
{
// TODO: from etcd
if let Err(err) = apply_dynamic_config(cfg, api).await {
@@ -1166,7 +1169,7 @@ where
async fn apply_dynamic_config<S>(cfg: &mut Config, api: Arc<S>) -> Result<()>
where
S: StorageAPI + StorageAdminApi,
S: StorageAdminApi,
{
for key in SUB_SYSTEMS_DYNAMIC.iter() {
apply_dynamic_config_for_sub_sys(cfg, api.clone(), key).await?;
@@ -1177,7 +1180,7 @@ where
async fn apply_dynamic_config_for_sub_sys<S>(cfg: &mut Config, api: Arc<S>, subsys: &str) -> Result<()>
where
S: StorageAPI + StorageAdminApi,
S: StorageAdminApi,
{
let set_drive_counts = StorageAdminApi::set_drive_counts(api.as_ref());
if subsys == STORAGE_CLASS_SUB_SYS {
+4 -4
View File
@@ -566,11 +566,11 @@ impl RebalanceMeta {
pub fn new() -> Self {
Self::default()
}
pub async fn load<S: StorageAPI>(&mut self, store: Arc<S>) -> Result<()> {
pub async fn load<S: ObjectIO>(&mut self, store: Arc<S>) -> Result<()> {
self.load_with_opts(store, ObjectOptions::default()).await
}
pub async fn load_with_opts<S: StorageAPI>(&mut self, store: Arc<S>, opts: ObjectOptions) -> Result<()> {
pub async fn load_with_opts<S: ObjectIO>(&mut self, store: Arc<S>, opts: ObjectOptions) -> Result<()> {
let (data, _) = read_config_with_metadata(store, REBAL_META_NAME, &opts).await?;
if data.is_empty() {
info!("rebalanceMeta load_with_opts: no data");
@@ -599,11 +599,11 @@ impl RebalanceMeta {
Ok(())
}
pub async fn save<S: StorageAPI>(&self, store: Arc<S>) -> Result<()> {
pub async fn save<S: ObjectIO>(&self, store: Arc<S>) -> Result<()> {
self.save_with_opts(store, ObjectOptions::default()).await
}
pub async fn save_with_opts<S: StorageAPI>(&self, store: Arc<S>, opts: ObjectOptions) -> Result<()> {
pub async fn save_with_opts<S: ObjectIO>(&self, store: Arc<S>, opts: ObjectOptions) -> Result<()> {
if self.pool_stats.is_empty() {
info!("rebalanceMeta save_with_opts: no pool stats");
return Ok(());
+11 -9
View File
@@ -46,12 +46,11 @@ use crate::tier::{
warm_backend::{check_warm_backend, new_warm_backend},
};
use crate::{
StorageAPI,
config::com::{CONFIG_PREFIX, read_config},
disk::{MIGRATING_META_BUCKET, RUSTFS_META_BUCKET},
global::is_first_cluster_node_local,
store::ECStore,
store_api::{ObjectIO as _, ObjectOptions, PutObjReader},
store_api::{ObjectIO, ObjectOperations, ObjectOptions, PutObjReader},
};
use rustfs_rio::HashReader;
use rustfs_utils::path::{SLASH_SEPARATOR, path_join};
@@ -1033,14 +1032,14 @@ impl TierConfigMgr {
self.save_tiering_config(api).await
}
pub async fn save_tiering_config<S: StorageAPI>(&self, api: Arc<S>) -> std::result::Result<(), std::io::Error> {
pub async fn save_tiering_config<S: ObjectIO>(&self, api: Arc<S>) -> std::result::Result<(), std::io::Error> {
let data = encode_external_tiering_config_blob(self)?;
let config_file = tier_config_path(TIER_CONFIG_FILE);
self.save_config(api, &config_file, data).await
}
pub async fn save_config<S: StorageAPI>(
pub async fn save_config<S: ObjectIO>(
&self,
api: Arc<S>,
file: &str,
@@ -1058,7 +1057,7 @@ impl TierConfigMgr {
.await
}
pub async fn save_config_with_opts<S: StorageAPI>(
pub async fn save_config_with_opts<S: ObjectIO>(
&self,
api: Arc<S>,
file: &str,
@@ -1100,7 +1099,7 @@ impl TierConfigMgr {
}
}
async fn new_and_save_tiering_config<S: StorageAPI>(api: Arc<S>) -> Result<TierConfigMgr> {
async fn new_and_save_tiering_config<S: ObjectIO>(api: Arc<S>) -> Result<TierConfigMgr> {
let mut cfg = TierConfigMgr {
driver_cache: HashMap::new(),
tiers: HashMap::new(),
@@ -1159,7 +1158,7 @@ async fn load_tier_config(api: Arc<ECStore>) -> std::result::Result<TierConfigMg
}
}
async fn read_tier_config_from_bucket<S: StorageAPI>(
async fn read_tier_config_from_bucket<S: ObjectIO>(
api: Arc<S>,
bucket: &str,
path: &str,
@@ -1177,7 +1176,7 @@ async fn read_tier_config_from_bucket<S: StorageAPI>(
Ok(Some(data))
}
async fn write_tier_config_to_rustfs<S: StorageAPI>(api: Arc<S>, path: &str, data: Bytes) -> io::Result<()> {
async fn write_tier_config_to_rustfs<S: ObjectIO>(api: Arc<S>, path: &str, data: Bytes) -> io::Result<()> {
let mut put_data = PutObjReader::from_vec(data.to_vec());
api.put_object(
RUSTFS_META_BUCKET,
@@ -1193,7 +1192,10 @@ async fn write_tier_config_to_rustfs<S: StorageAPI>(api: Arc<S>, path: &str, dat
Ok(())
}
pub async fn try_migrate_tiering_config<S: StorageAPI>(api: Arc<S>) {
pub async fn try_migrate_tiering_config<S>(api: Arc<S>)
where
S: ObjectIO + ObjectOperations,
{
let target_path = tier_config_path(TIER_CONFIG_FILE);
if api
.get_object_info(
+59 -55
View File
@@ -5,15 +5,15 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
## Current Context
- Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660)
- Branch: `overtrue/arch-storage-api-next-contracts`
- Baseline: `origin/main` at `87968275a776362e9b3734eac5bd9e3233d6cbe9`
- Branch: `overtrue/arch-storage-api-bound-narrowing`
- Baseline: `origin/main` at `6b86e3875c4912f3a7c27915b0bbd43399402847`
- PR type for this branch: `dependency-migration`
- Runtime behavior changes: none.
- Rust code changes: remove duplicate admin-read methods from the old
`StorageAPI` trait after all admin inventory consumers have moved to
`StorageAdminApi`.
- Rust code changes: narrow metadata helper generic bounds away from full
`StorageAPI` where helpers only need object I/O, object metadata/delete,
bucket listing, object listing, or `StorageAdminApi`.
- CI/script changes: none.
- Docs changes: record API-007 completion and the current admin surface cleanup
- Docs changes: record API-008 completion and the current bound-narrowing
context, verification evidence, and expert review outcomes.
## Phase 0 Tasks
@@ -218,25 +218,33 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
drive-count, or disk-set inventory when the inventory-facing
`StorageAdminApi` contract represents the same read-only operation.
- [~] `API-008` Remove duplicate old-path admin surfaces.
- Current branch slice: remove admin-read methods from the old `StorageAPI`
trait and its ECStore/Sets/SetDisks test implementations after API-007
migrated their consumers.
- [x] `API-008` Remove duplicate old-path admin surfaces.
- Completed slice: `rustfs/rustfs#3340` removed duplicate admin-read methods
from the old `StorageAPI` trait and its ECStore/Sets/SetDisks/test
implementations after API-007 migrated their consumers.
- Acceptance: old `StorageAPI` keeps storage operation traits and
`new_ns_lock`, while admin inventory surfaces live only on
`StorageAdminApi`.
- Must preserve: object API trait bounds, namespace lock behavior, bucket,
object, list, multipart, and heal operations; config read/write byte shape;
dynamic storage-class lookup; object/rebalance object selection paths;
scanner cache persistence; replication/tier persistence; and storage hot
paths.
- Risk defense: do not remove `StorageAPI` itself, do not move object/config
persistence contracts, and only replace previous `SetDisks::get_disks(0, 0)`
trait calls with the same internal `disk_inventory()` data source that the
old impl returned.
- Verification: pending focused compile/tests, migration guards, Rust risk
scan, and required quality/architecture, migration-preservation, and
testing/verification review.
- [~] `API-009` Narrow metadata helper storage bounds.
- Current branch slice: narrow server config, tier config, rebalance
metadata, and startup metadata migration helper bounds away from full
`StorageAPI` when the helper only needs `ObjectIO`, `ObjectOperations`,
`BucketOperations`, `ListOperations`, or `StorageAdminApi`.
- Acceptance: metadata helper contracts express the actual operation group
they need, while callers and persistence behavior remain unchanged.
- Must preserve: config/tier/rebalance serialized byte shapes, read/write
object names, legacy bucket/IAM migration normalization and skip behavior,
lock behavior around rebalance metadata merge, dynamic storage-class lookup,
object APIs, scanner cache persistence, heal repair, replication
persistence, and storage hot paths.
- Risk defense: do not move traits to `rustfs-storage-api`, do not remove
`StorageAPI`, do not alter helper bodies or storage options, and keep
`save_rebalance_meta_with_merge` on full `StorageAPI` because it still needs
`new_ns_lock`.
- Verification: focused compile/tests, migration guards, Rust risk scan, and
required quality/architecture, migration-preservation, and
testing/verification review passed before push.
## Phase 8 Background Controller Tasks
@@ -261,28 +269,26 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
## Next PRs
1. `consumer-migration`: migrate the remaining readiness/admin/capacity
consumers to the inventory-facing admin contract one group at a time.
2. `dependency-migration`: remove duplicate old-path admin surfaces only after
consumer migration proves equivalent behavior.
3. `api-extraction`: move only the pure server-config model into
1. `dependency-migration`: narrow metadata/helper bounds away from full
`StorageAPI` where the helper only needs a specific operation trait.
2. `api-extraction`: move only the pure server-config model into
rustfs-config as CFG-003.
4. `api-extraction`: keep the old rustfs_ecstore::config::* path with
3. `api-extraction`: keep the old rustfs_ecstore::config::* path with
RUSTFS_COMPAT_TODO(CFG-004) and cleanup-register coverage.
5. `consumer-migration`: migrate external consumers one group at a time only
4. `consumer-migration`: migrate external consumers one group at a time only
after the model path and compatibility shim are stable.
6. `security-change`: make Local KMS unsafe defaults explicit development
5. `security-change`: make Local KMS unsafe defaults explicit development
opt-ins or production failures in KMSD-002.
7. `security-change`: make Vault unsafe defaults explicit development opt-ins
6. `security-change`: make Vault unsafe defaults explicit development opt-ins
or production failures in KMSD-003.
## Pre-Push Review Log
| Expert | Status | Notes |
|---|---|---|
| Quality/architecture | pass | Confirmed the old `StorageAPI` now keeps only operation traits and `new_ns_lock`, admin inventory surfaces remain on `StorageAdminApi`, dependency direction is clean, and scope/naming stay limited to API-008. |
| Migration preservation | pass | Confirmed object/bucket/list/multipart/heal contracts, namespace locks, config byte paths, dynamic storage-class behavior, rebalance selection, scanner cache, heal repair, replication/tier/config persistence, and storage hot paths are preserved. |
| Testing/verification | pass | Initially requested wider rebalance coverage; after `cargo test -p rustfs-ecstore rebalance --lib` passed, confirmed the focused matrix is sufficient for this larger dependency-migration slice while skipping full pre-commit per current instruction. |
| Quality/architecture | pass | Confirmed the Rust diff only changes imports/generic bounds, the new operation-group bounds match actual calls, `StorageAPI` and `new_ns_lock` remain where needed, and no over-abstraction, naming regression, dependency reversal, or hot-path intrusion was found. |
| Migration preservation | pass | Confirmed config/tier/rebalance byte shapes, object names, buckets, options, bucket/IAM normalization and skip behavior, dynamic storage-class lookup, and rebalance metadata merge locking remain unchanged. |
| Testing/verification | pass | Confirmed the focused compile, config/tier/rebalance/bucket migration tests, migration guards, diff hygiene, and added-line Rust risk scan are sufficient; full pre-commit can be skipped under the current larger-granularity instruction. |
## Verification Notes
@@ -292,42 +298,40 @@ Passed:
- `cargo fmt --all --check`.
- `cargo check -p rustfs-storage-api -p rustfs-ecstore -p rustfs --lib`.
- `cargo test -p rustfs-ecstore config --lib`; 59 passed.
- `cargo test -p rustfs-ecstore store::rebalance --lib`; 19 passed.
- `cargo test -p rustfs-ecstore tier --lib`; 27 passed.
- `cargo test -p rustfs-ecstore rebalance --lib`; 198 passed.
- `cargo test -p rustfs-ecstore set_disk --lib`; 86 passed.
- `cargo test -p rustfs-ecstore bucket::migration --lib`; 2 passed.
- `./scripts/check_architecture_migration_rules.sh`.
- `./scripts/check_layer_dependencies.sh`.
- `./scripts/check_metrics_migration_refs.sh`.
- `./scripts/check_unsafe_code_allowances.sh`.
- `git diff --check`.
- Rust code-quality scan on changed `.rs` files, plus added-line scan for
unwrap/expect, numeric casts, `Result<_, String>`, `Box<dyn Error>`,
println/eprintln, and `Ordering::Relaxed`.
- Rust code-quality scan on changed `.rs` files: broad full-file matches are
pre-existing test/business-code patterns; added-line scan found no new
unwrap/expect, numeric cast, `Result<_, String>`, `Box<dyn Error>`,
println/eprintln, or `Ordering::Relaxed`.
Notes:
- Full pre-commit is intentionally skipped when the focused tests and guards
pass, per the current instruction to increase PR granularity.
- The broad changed-file quality scan reports pre-existing unwrap/cast matches
and `Result<String>` business return values in touched ECStore files; the
added-line scan found no new risky code patterns.
- Old `StorageAPI` remains as the object/bucket/list/multipart/heal namespace
lock contract; only duplicate admin inventory methods are removed.
- Config dynamic storage-class lookup now reads drive counts through
`StorageAdminApi`.
- Object/rebalance object selection paths, scanner cache persistence, heal
object repair, object APIs, replication/config/tier persistence paths, and
storage hot paths must remain unchanged.
- This slice changes generic bounds and imports only; helper bodies, storage
options, object names, and metadata normalization logic are unchanged.
- `save_rebalance_meta_with_merge` intentionally still requires full
`StorageAPI` because it uses `new_ns_lock`.
- No temporary compatibility shim was added.
## Handoff Notes
- Keep this API-008 slice as a `dependency-migration` PR that only removes
duplicate admin inventory methods from old `StorageAPI`.
- Keep this API-009 slice as a `dependency-migration` PR that only narrows
metadata/helper generic bounds for config, tier, rebalance, and startup
metadata migration helpers.
- Do not remove `StorageAPI` itself, object operation traits, or
`new_ns_lock` in this PR.
- Do not migrate scanner cache writes, heal object repair, object APIs,
replication, config/tier persistence, or storage hot-path consumers in this
PR.
- Do not make the old `StorageAPI` trait inherit `StorageAdminApi`.
- Do not move traits into `rustfs-storage-api` or introduce new compatibility
shims in this PR.
- Do not alter config/tier/rebalance serialized byte shapes, legacy bucket/IAM
migration normalization, object names, storage options, scanner cache writes,
heal object repair, object APIs, replication persistence, or storage hot-path
consumers in this PR.
- Do not add temporary compatibility code unless a matching
`RUSTFS_COMPAT_TODO(<task-id>)` marker and cleanup-register entry are added.