refactor: clean config storage boundaries (#3489)

This commit is contained in:
安正超
2026-06-16 06:35:32 +08:00
committed by GitHub
parent fbab160c2b
commit c26593fa7a
5 changed files with 33 additions and 311 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ use crate::client::{
}; };
use crate::{ use crate::{
disk::DiskAPI, disk::DiskAPI,
store_api::{GetObjectReader, ObjectInfo, StorageAPI}, store_api::{GetObjectReader, ObjectInfo},
}; };
use rustfs_utils::hash::EMPTY_STRING_SHA256_HASH; use rustfs_utils::hash::EMPTY_STRING_SHA256_HASH;
+1 -279
View File
@@ -1217,12 +1217,7 @@ mod tests {
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::global::{is_dist_erasure, is_erasure, is_erasure_sd, update_erasure_type}; use crate::global::{is_dist_erasure, is_erasure, is_erasure_sd, update_erasure_type};
use crate::set_disk::SetDisks; use crate::set_disk::SetDisks;
use crate::store_api::{ use crate::store_api::{GetObjectReader, HTTPRangeSpec, NamespaceLocking, ObjectIO, ObjectInfo, ObjectOptions, PutObjReader};
BucketInfo, BucketOperations, BucketOptions, CompletePart, DeleteBucketOptions, DeletedObject, GetObjectReader,
HTTPRangeSpec, HealOperations, ListMultipartsInfo, ListObjectVersionsInfo, ListObjectsV2Info, ListOperations,
MakeBucketOptions, MultipartInfo, MultipartOperations, MultipartUploadResult, NamespaceLocking, ObjectIO, ObjectInfo,
ObjectOperations, ObjectOptions, ObjectToDelete, PartInfo, PutObjReader, StorageAPI, WalkOptions,
};
use http::HeaderMap; use http::HeaderMap;
use rustfs_config::audit::{AUDIT_AMQP_SUB_SYS, AUDIT_KAFKA_SUB_SYS, AUDIT_MQTT_SUB_SYS, AUDIT_WEBHOOK_SUB_SYS}; use rustfs_config::audit::{AUDIT_AMQP_SUB_SYS, AUDIT_KAFKA_SUB_SYS, AUDIT_MQTT_SUB_SYS, AUDIT_WEBHOOK_SUB_SYS};
use rustfs_config::notify::{ use rustfs_config::notify::{
@@ -1233,7 +1228,6 @@ mod tests {
use rustfs_config::{ use rustfs_config::{
DEFAULT_DELIMITER, ENABLE_KEY, EnableState, MYSQL_DSN_STRING, MYSQL_MAX_OPEN_CONNECTIONS, MYSQL_QUEUE_DIR, MYSQL_TABLE, DEFAULT_DELIMITER, ENABLE_KEY, EnableState, MYSQL_DSN_STRING, MYSQL_MAX_OPEN_CONNECTIONS, MYSQL_QUEUE_DIR, MYSQL_TABLE,
}; };
use rustfs_filemeta::FileInfo;
use rustfs_lock::client::LockClient; use rustfs_lock::client::LockClient;
use rustfs_lock::client::local::LocalClient; use rustfs_lock::client::local::LocalClient;
use rustfs_lock::{LockError, LockInfo, LockResponse, LockStats}; use rustfs_lock::{LockError, LockInfo, LockResponse, LockStats};
@@ -1249,7 +1243,6 @@ mod tests {
use time::OffsetDateTime; use time::OffsetDateTime;
use tokio::io::{AsyncRead, ReadBuf}; use tokio::io::{AsyncRead, ReadBuf};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken;
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct FailingClient; struct FailingClient;
@@ -1457,277 +1450,6 @@ mod tests {
} }
} }
#[async_trait::async_trait]
impl BucketOperations for LockingConfigStorage {
async fn make_bucket(&self, _bucket: &str, _opts: &MakeBucketOptions) -> Result<()> {
panic!("unused in test")
}
async fn get_bucket_info(&self, _bucket: &str, _opts: &BucketOptions) -> Result<BucketInfo> {
panic!("unused in test")
}
async fn list_bucket(&self, _opts: &BucketOptions) -> Result<Vec<BucketInfo>> {
panic!("unused in test")
}
async fn delete_bucket(&self, _bucket: &str, _opts: &DeleteBucketOptions) -> Result<()> {
panic!("unused in test")
}
}
#[async_trait::async_trait]
impl ObjectOperations for LockingConfigStorage {
async fn get_object_info(&self, _bucket: &str, _object: &str, _opts: &ObjectOptions) -> Result<ObjectInfo> {
panic!("unused in test")
}
async fn verify_object_integrity(&self, _bucket: &str, _object: &str, _opts: &ObjectOptions) -> Result<()> {
panic!("unused in test")
}
async fn copy_object(
&self,
_src_bucket: &str,
_src_object: &str,
_dst_bucket: &str,
_dst_object: &str,
_src_info: &mut ObjectInfo,
_src_opts: &ObjectOptions,
_dst_opts: &ObjectOptions,
) -> Result<ObjectInfo> {
panic!("unused in test")
}
async fn delete_object_version(
&self,
_bucket: &str,
_object: &str,
_fi: &FileInfo,
_force_del_marker: bool,
) -> Result<()> {
panic!("unused in test")
}
async fn delete_object(&self, _bucket: &str, _object: &str, _opts: ObjectOptions) -> Result<ObjectInfo> {
panic!("unused in test")
}
async fn delete_objects(
&self,
_bucket: &str,
_objects: Vec<ObjectToDelete>,
_opts: ObjectOptions,
) -> (Vec<DeletedObject>, Vec<Option<Error>>) {
panic!("unused in test")
}
async fn put_object_metadata(&self, _bucket: &str, _object: &str, _opts: &ObjectOptions) -> Result<ObjectInfo> {
panic!("unused in test")
}
async fn get_object_tags(&self, _bucket: &str, _object: &str, _opts: &ObjectOptions) -> Result<String> {
panic!("unused in test")
}
async fn put_object_tags(&self, _bucket: &str, _object: &str, _tags: &str, _opts: &ObjectOptions) -> Result<ObjectInfo> {
panic!("unused in test")
}
async fn delete_object_tags(&self, _bucket: &str, _object: &str, _opts: &ObjectOptions) -> Result<ObjectInfo> {
panic!("unused in test")
}
async fn add_partial(&self, _bucket: &str, _object: &str, _version_id: &str) -> Result<()> {
panic!("unused in test")
}
async fn transition_object(&self, _bucket: &str, _object: &str, _opts: &ObjectOptions) -> Result<()> {
panic!("unused in test")
}
async fn restore_transitioned_object(self: Arc<Self>, _bucket: &str, _object: &str, _opts: &ObjectOptions) -> Result<()> {
panic!("unused in test")
}
}
#[async_trait::async_trait]
impl ListOperations for LockingConfigStorage {
async fn list_objects_v2(
self: Arc<Self>,
_bucket: &str,
_prefix: &str,
_continuation_token: Option<String>,
_delimiter: Option<String>,
_max_keys: i32,
_fetch_owner: bool,
_start_after: Option<String>,
_incl_deleted: bool,
) -> Result<ListObjectsV2Info> {
panic!("unused in test")
}
async fn list_object_versions(
self: Arc<Self>,
_bucket: &str,
_prefix: &str,
_marker: Option<String>,
_version_marker: Option<String>,
_delimiter: Option<String>,
_max_keys: i32,
) -> Result<ListObjectVersionsInfo> {
panic!("unused in test")
}
async fn walk(
self: Arc<Self>,
_rx: CancellationToken,
_bucket: &str,
_prefix: &str,
_result: tokio::sync::mpsc::Sender<crate::store_api::ObjectInfoOrErr>,
_opts: WalkOptions,
) -> Result<()> {
panic!("unused in test")
}
}
#[async_trait::async_trait]
impl MultipartOperations for LockingConfigStorage {
async fn list_multipart_uploads(
&self,
_bucket: &str,
_prefix: &str,
_key_marker: Option<String>,
_upload_id_marker: Option<String>,
_delimiter: Option<String>,
_max_uploads: usize,
) -> Result<ListMultipartsInfo> {
panic!("unused in test")
}
async fn new_multipart_upload(
&self,
_bucket: &str,
_object: &str,
_opts: &ObjectOptions,
) -> Result<MultipartUploadResult> {
panic!("unused in test")
}
async fn copy_object_part(
&self,
_src_bucket: &str,
_src_object: &str,
_dst_bucket: &str,
_dst_object: &str,
_upload_id: &str,
_part_id: usize,
_start_offset: i64,
_length: i64,
_src_info: &ObjectInfo,
_src_opts: &ObjectOptions,
_dst_opts: &ObjectOptions,
) -> Result<()> {
panic!("unused in test")
}
async fn put_object_part(
&self,
_bucket: &str,
_object: &str,
_upload_id: &str,
_part_id: usize,
_data: &mut PutObjReader,
_opts: &ObjectOptions,
) -> Result<PartInfo> {
panic!("unused in test")
}
async fn get_multipart_info(
&self,
_bucket: &str,
_object: &str,
_upload_id: &str,
_opts: &ObjectOptions,
) -> Result<MultipartInfo> {
panic!("unused in test")
}
async fn list_object_parts(
&self,
_bucket: &str,
_object: &str,
_upload_id: &str,
_part_number_marker: Option<usize>,
_max_parts: usize,
_opts: &ObjectOptions,
) -> Result<crate::store_api::ListPartsInfo> {
panic!("unused in test")
}
async fn abort_multipart_upload(
&self,
_bucket: &str,
_object: &str,
_upload_id: &str,
_opts: &ObjectOptions,
) -> Result<()> {
panic!("unused in test")
}
async fn complete_multipart_upload(
self: Arc<Self>,
_bucket: &str,
_object: &str,
_upload_id: &str,
_uploaded_parts: Vec<CompletePart>,
_opts: &ObjectOptions,
) -> Result<ObjectInfo> {
panic!("unused in test")
}
}
#[async_trait::async_trait]
impl HealOperations for LockingConfigStorage {
async fn heal_format(&self, _dry_run: bool) -> Result<(rustfs_madmin::heal_commands::HealResultItem, Option<Error>)> {
panic!("unused in test")
}
async fn heal_bucket(
&self,
_bucket: &str,
_opts: &rustfs_common::heal_channel::HealOpts,
) -> Result<rustfs_madmin::heal_commands::HealResultItem> {
panic!("unused in test")
}
async fn heal_object(
&self,
_bucket: &str,
_object: &str,
_version_id: &str,
_opts: &rustfs_common::heal_channel::HealOpts,
) -> Result<(rustfs_madmin::heal_commands::HealResultItem, Option<Error>)> {
panic!("unused in test")
}
async fn get_pool_and_set(&self, _id: &str) -> Result<(Option<usize>, Option<usize>, Option<usize>)> {
panic!("unused in test")
}
async fn check_abandoned_parts(
&self,
_bucket: &str,
_object: &str,
_opts: &rustfs_common::heal_channel::HealOpts,
) -> Result<()> {
panic!("unused in test")
}
}
#[async_trait::async_trait]
impl StorageAPI for LockingConfigStorage {}
#[async_trait::async_trait] #[async_trait::async_trait]
impl NamespaceLocking for LockingConfigStorage { impl NamespaceLocking for LockingConfigStorage {
async fn new_ns_lock(&self, bucket: &str, object: &str) -> Result<rustfs_lock::NamespaceLockWrapper> { async fn new_ns_lock(&self, bucket: &str, object: &str) -> Result<rustfs_lock::NamespaceLockWrapper> {
+2 -2
View File
@@ -1552,8 +1552,8 @@ fn get_quorum_disk_infos(disks: &[DiskStore], infos: &[DiskInfo], read_quorum: u
for (i, info) in infos.iter().enumerate() { for (i, info) in infos.iter().enumerate() {
let mutations = info.metrics.total_deletes + info.metrics.total_writes; let mutations = info.metrics.total_deletes + info.metrics.total_writes;
if mutations >= common_mutations { if mutations >= common_mutations {
new_disks.push(disks[i].clone()); // Assuming StorageAPI derives Clone new_disks.push(disks[i].clone());
new_infos.push(infos[i].clone()); // Assuming DiskInfo derives Clone new_infos.push(infos[i].clone());
} }
} }
@@ -80,7 +80,7 @@ behind narrower contracts.
| `DEFAULT_KVS` | `crates/ecstore/src/config/mod.rs` | Registry for defaults across storage class, scanner, notify, audit, and OIDC. | Move defaults only after an explicit registration contract exists. | | `DEFAULT_KVS` | `crates/ecstore/src/config/mod.rs` | Registry for defaults across storage class, scanner, notify, audit, and OIDC. | Move defaults only after an explicit registration contract exists. |
| `GLOBAL_SERVER_CONFIG` | `crates/ecstore/src/config/mod.rs` | Process-wide mutable server config snapshot. | Migrate readers behind `AppContext` or a server-config provider before changing storage. | | `GLOBAL_SERVER_CONFIG` | `crates/ecstore/src/config/mod.rs` | Process-wide mutable server config snapshot. | Migrate readers behind `AppContext` or a server-config provider before changing storage. |
| `ConfigSys::init` | `crates/ecstore/src/config/mod.rs` | Reads persisted config, looks up derived config, and stores the global snapshot. | Startup order must remain unchanged until the lifecycle contract owns this dependency. | | `ConfigSys::init` | `crates/ecstore/src/config/mod.rs` | Reads persisted config, looks up derived config, and stores the global snapshot. | Startup order must remain unchanged until the lifecycle contract owns this dependency. |
| `read_config_without_migrate` | `crates/ecstore/src/config/com.rs` | Loads persisted server config through `StorageAPI`. | Persistence stays in `ecstore` until pure model and persistence are separated. | | `read_config_without_migrate` | `crates/ecstore/src/config/com.rs` | Loads persisted server config through ECStore-owned object I/O and storage-admin contracts. | Persistence stays in `ecstore` until pure model and persistence are separated. |
| `save_server_config` | `crates/ecstore/src/config/com.rs` | Persists the canonical server config object. | Preserve external object shape and config-history behavior. | | `save_server_config` | `crates/ecstore/src/config/com.rs` | Persists the canonical server config object. | Preserve external object shape and config-history behavior. |
| `get_global_server_config` / `set_global_server_config` | `crates/ecstore/src/config/mod.rs` | Clone/read and replace the global server-config snapshot. | Do not remove until all runtime readers have an injected provider path. | | `get_global_server_config` / `set_global_server_config` | `crates/ecstore/src/config/mod.rs` | Clone/read and replace the global server-config snapshot. | Do not remove until all runtime readers have an injected provider path. |
@@ -91,7 +91,7 @@ behind narrower contracts.
| Files | Current usage | | Files | Current usage |
|---|---| |---|---|
| `crates/ecstore/src/config/mod.rs` | Defines `KV`, `KVS`, `Config`, defaults, global snapshot, initialization, and tests. | | `crates/ecstore/src/config/mod.rs` | Defines `KV`, `KVS`, `Config`, defaults, global snapshot, initialization, and tests. |
| `crates/ecstore/src/config/com.rs` | Encodes, decodes, reads, writes, creates, and normalizes server config objects through `StorageAPI`. | | `crates/ecstore/src/config/com.rs` | Encodes, decodes, reads, writes, creates, and normalizes server config objects through ECStore-local persistence helpers. |
| `crates/ecstore/src/config/{notify,audit,oidc,scanner,storageclass}.rs` | Register default `KVS` values and subsystem-specific parsing helpers. | | `crates/ecstore/src/config/{notify,audit,oidc,scanner,storageclass}.rs` | Register default `KVS` values and subsystem-specific parsing helpers. |
| `crates/ecstore/src/store.rs` | Exposes store-level server-config accessors that delegate to the global config snapshot. | | `crates/ecstore/src/store.rs` | Exposes store-level server-config accessors that delegate to the global config snapshot. |
@@ -162,7 +162,7 @@ behind narrower contracts.
| `KVS` is the effective target config carrier | Notify, audit, and target factories consume `KVS` after file/env merge. | Keep `KVS` API stable until target descriptor and runtime crates are behind a shared contract. | | `KVS` is the effective target config carrier | Notify, audit, and target factories consume `KVS` after file/env merge. | Keep `KVS` API stable until target descriptor and runtime crates are behind a shared contract. |
| `DEFAULT_KVS` registration is global | Defaults are initialized centrally and used by admin validation/rendering. | Add a registration contract before changing initialization order. | | `DEFAULT_KVS` registration is global | Defaults are initialized centrally and used by admin validation/rendering. | Add a registration contract before changing initialization order. |
| Global snapshot readers still exist | Server, admin, IAM, scanner, and site-replication paths can still read global config. | Migrate readers through `AppContext`/provider paths in small steps after the model contract is stable. | | Global snapshot readers still exist | Server, admin, IAM, scanner, and site-replication paths can still read global config. | Migrate readers through `AppContext`/provider paths in small steps after the model contract is stable. |
| Persistence helpers depend on `StorageAPI` | Moving them with the pure model would pull storage implementation dependencies upward. | Keep read/write helpers in `ecstore` until a storage-facing persistence contract is explicit. | | Persistence helpers depend on ECStore storage contracts | Moving them with the pure model would pull storage implementation dependencies upward. | Keep read/write helpers in `ecstore` until a storage-facing persistence contract is explicit. |
## Recommended Migration Order ## Recommended Migration Order
@@ -176,8 +176,8 @@ behind narrower contracts.
behavior, and representative admin config rendering paths. behavior, and representative admin config rendering paths.
5. Migrate global `Config` readers behind `ServerConfigInterface` or a narrower 5. Migrate global `Config` readers behind `ServerConfigInterface` or a narrower
provider in small PRs. provider in small PRs.
6. Move persistence helpers only after `StorageAPI` dependencies can stay below 6. Move persistence helpers only after object-I/O and storage-admin dependencies
the model contract. can stay below the model contract.
7. Evaluate crate split only after consumers no longer need old paths except 7. Evaluate crate split only after consumers no longer need old paths except
explicit `RUSTFS_COMPAT_TODO(<task-id>)` compatibility shims. explicit `RUSTFS_COMPAT_TODO(<task-id>)` compatibility shims.
+24 -24
View File
@@ -5,15 +5,15 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
## Current Context ## Current Context
- Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660) - Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660)
- Branch: `overtrue/arch-storage-namespace-lock-large-cleanup` - Branch: `overtrue/arch-config-storage-boundary-cleanup`
- Baseline: `origin/main` at `49c0f131205035d125271bb5b87db5b0f5bc2a6d` - Baseline: `origin/main` at `fbab160c2b09075f5e2503a669d82917ef82d40e`
- PR type for this branch: `consumer-migration` - PR type for this branch: `consumer-migration`
- Runtime behavior changes: no external behavior change expected. - Runtime behavior changes: no external behavior change expected.
- Rust code changes: narrow replication pool, resync, delete replication, and - Rust code changes: remove stale full `StorageAPI` coupling from config
object replication storage bounds away from full `StorageAPI` to their actual persistence tests, an unused S3 remove-client import, and an obsolete storage
object I/O, object operation, list, and namespace-lock capabilities. list comment.
- CI/script changes: none. - CI/script changes: none.
- Docs changes: record the current `API-012` consumer cleanup slice and its - Docs changes: refresh the ECStore config persistence inventory and current
verification state. verification state.
## Phase 0 Tasks ## Phase 0 Tasks
@@ -419,6 +419,9 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
full `StorageAPI` when the helper only needs `ObjectIO`, full `StorageAPI` when the helper only needs `ObjectIO`,
`ObjectOperations`, `BucketOperations`, `ListOperations`, or `ObjectOperations`, `BucketOperations`, `ListOperations`, or
`StorageAdminApi`. `StorageAdminApi`.
- Cleanup slice: remove stale full `StorageAPI` dependencies from config
persistence test support after the server-config persistence helpers moved
to their actual object I/O and storage-admin bounds.
- Acceptance: metadata helper contracts express the actual operation group - Acceptance: metadata helper contracts express the actual operation group
they need, while callers and persistence behavior remain unchanged. they need, while callers and persistence behavior remain unchanged.
@@ -463,10 +466,10 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
catalog backend and rebalance metadata helper consumers away from full catalog backend and rebalance metadata helper consumers away from full
`StorageAPI` where they only need object I/O, object operations, list `StorageAPI` where they only need object I/O, object operations, list
operations, and namespace locking. operations, and namespace locking.
- Current cleanup slice: narrow replication pool, resync leader-lock, delete - Completed follow-up slice: `rustfs/rustfs#3485` narrowed replication pool,
replication, object replication, and multipart replication helpers away resync leader-lock, delete replication, object replication, and multipart
from full `StorageAPI` where they only need object I/O, object operations, replication helpers away from full `StorageAPI` where they only need object
list operations, and namespace locking. I/O, object operations, list operations, and namespace locking.
- Acceptance: table catalog object backend contracts express the actual - Acceptance: table catalog object backend contracts express the actual
object read/write, metadata/delete, list, and namespace-lock capabilities object read/write, metadata/delete, list, and namespace-lock capabilities
they need; namespace-lock consumers depend on `NamespaceLocking` instead of they need; namespace-lock consumers depend on `NamespaceLocking` instead of
@@ -759,18 +762,16 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
| Expert | Status | Notes | | Expert | Status | Notes |
|---|---|---| |---|---|---|
| Quality/architecture | passed | Replication consumers now depend on a local replication storage capability boundary plus object-I/O-only helper bounds, without changing replication method bodies. | | Quality/architecture | passed | Config persistence tests now express their actual object I/O, namespace-lock, and storage-admin requirements instead of a full storage facade. |
| Migration preservation | passed | Resync leader locks, per-object replication locks, delete replication, object reader flow, multipart upload flow, MRF recovery, and replication scheduling semantics are unchanged. | | Migration preservation | passed | Config object encoding/decoding, metadata reads, namespace-lock behavior, and S3 remove-client behavior are unchanged. |
| Testing/verification | passed | Focused replication tests, storage API compatibility test, compile checks, migration/layer guards, formatting, diff hygiene, Rust risk scan, and full `make pre-commit` passed. | | Testing/verification | passed | Focused config tests, compile checks, migration/layer guards, formatting, diff hygiene, Rust risk scan, and full `make pre-commit` passed. |
## Verification Notes ## Verification Notes
Passed on `49c0f131205035d125271bb5b87db5b0f5bc2a6d`: Passed on `fbab160c2b09075f5e2503a669d82917ef82d40e`:
- `cargo check -p rustfs-ecstore`: passed. - `cargo check -p rustfs-ecstore`: passed.
- `cargo test -p rustfs-ecstore bucket::replication --no-fail-fast`: passed. - `cargo test -p rustfs-ecstore config::com --no-fail-fast`: passed.
- `cargo test -p rustfs-ecstore --test storage_api_compat_test --no-fail-fast`:
passed.
- `cargo check -p rustfs -p rustfs-ecstore`: passed. - `cargo check -p rustfs -p rustfs-ecstore`: passed.
- `./scripts/check_architecture_migration_rules.sh`: passed. - `./scripts/check_architecture_migration_rules.sh`: passed.
- `./scripts/check_layer_dependencies.sh`: passed. - `./scripts/check_layer_dependencies.sh`: passed.
@@ -783,17 +784,16 @@ Passed on `49c0f131205035d125271bb5b87db5b0f5bc2a6d`:
Notes: Notes:
- This slice keeps the existing replication method bodies unchanged while - This slice removes test-only full facade scaffolding after config persistence
narrowing the generic storage capabilities they require. helpers already moved to narrower object I/O and storage-admin contracts.
- Replication storage still depends on object I/O, object metadata/delete - The S3 remove client had a stale full facade import only; behavior remains
operations, bucket walking, and namespace locking; multipart replication unchanged.
helpers only need object reader access to stream source ranges.
- The slice does not remove the full storage facade or move traits across crate - The slice does not remove the full storage facade or move traits across crate
boundaries. boundaries.
## Handoff Notes ## Handoff Notes
- API-012 replication storage cleanup is locally verified and current with - Config storage boundary cleanup is locally verified and current with
`origin/main`. `origin/main`.
- Remaining namespace-lock cleanup can continue by migrating other consumers - Remaining storage-facade cleanup can continue by migrating other consumers
that no longer need the full storage facade. that no longer need the full storage facade.