morningman commented on PR #69605:
URL: https://github.com/apache/doris/pull/69605#issuecomment-6618322006

   <!-- doris-repo-review:report:begin -->
   Review at head `f58598d426a9`. No build or test was run locally; see 
"Coverage and limits" at the end. This is not an approval.
   
   # Code Review — PR #69605: [refactor](be) Build object storage clients from 
typed, per-backend configuration
   
   | | |
   |---|---|
   | PR | https://github.com/apache/doris/pull/69605 (state: open) |
   | PR head | `f58598d426a9d97a110dba1f1e85f6279f0607b9` on 
`refactor/be-obj-client-config` of `xylaaaaa/doris` |
   | Diff range | 
`7acfd9bfc27f01f0b953e8ac0cdc3482a46e3c59...f58598d426a9d97a110dba1f1e85f6279f0607b9`
 (three-dot, merge base `7acfd9bfc27f01f0b953e8ac0cdc3482a46e3c59`) |
   | Scope | 69 files, +3541/-1724 lines, 14 commits (the head is a merge of 
master) |
   | Verdict | **REQUEST_CHANGES** |
   | Rounds | 2 of max 3; converged (verdict stable since round 1; round 2 
added no finding and refined F-01 and F-02) |
   | Builds on | `719de6a` (2026-10-26) and `af4964b` (2026-10-31) |
   
   ## Summary
   
   The PR replaces the flat `S3Conf` / `S3ClientConf` with a typed 
configuration that holds exactly one backend, and builds S3 and Azure clients 
through per-backend providers. This is the third review. Since `af4964b` the 
author has fixed all 20 findings of the previous review. The BE no longer 
aborts on an Azure resource that carries a native credential. The gitleaks gate 
is green, and so is BE UT with master merged. The connection-limit scope is 
declared. Each test gap the previous review named now has a test that fails on 
the named mutation. Every TeamCity gate passed at this head.
   
   Two Majors remain, and both are in the new code that applies a backend 
change. The fix for the old F-04 makes `S3FileSystem::refresh` build a second 
file system. Rowsets that already resolved the first one keep it until BE 
restart, so a revert or a later key rotation never reaches them (F-01). The 
cloud vault refresh did not get that fix and still only resets. A vault id that 
is handed out again for the other provider therefore keeps the dropped vault's 
target, and loads write there (F-02). The rest is a false rationale in two 
comments (F-03) and three test gaps (F-04 to F-06).
   
   ## Findings at a glance
   
   | ID | Severity | Regression | Finding |
   |---|---|---|---|
   | F-01 | Major | yes | A provider change across S3 and Azure leaves 
already-bound rowsets on the old file system, so a revert or a key rotation no 
longer reaches them |
   | F-02 | Major | yes | A vault id that is handed out again for another 
provider keeps the dropped vault's file system, and loads write into the old 
bucket |
   | F-03 | Minor | no | Two new comments give a false reason why Recycler does 
not check the Azure key pair |
   | F-04 | Minor | no | No test that runs in CI pins which value becomes the 
Azure SharedKey account and which the key |
   | F-05 | Nit | no | `ObjClientFactoryTest` counts builds through a 
process-wide cache and cannot be repeated in one process |
   | F-06 | Nit | no | The refresh test does not pin a vendor change inside the 
S3 family |
   
   ## Status of the previous review's findings
   
   The previous review is [this 
comment](https://github.com/apache/doris/pull/69605#issuecomment-6546301394).
   
   | Prior (af4964b) | Severity | At this head |
   |---|---|---|
   | F-01 Azure plus a native credential aborts the BE | Blocker | Fixed. The 
resolvers return `Result`, and the Azure mapping returns InvalidArgument for 
all three sources 
([`obj_storage_config_resolver.cpp:111-120`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/obj_storage/obj_storage_config_resolver.cpp#L111-L120)),
 tested at 
[`obj_client_factory_test.cpp:202-236`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/test/io/obj_storage/obj_client_factory_test.cpp#L202-L236)
 |
   | F-02 BE UT breaks once merged into master | Blocker | Fixed. The CSV, JSON 
and S3 reader tests were migrated and BE UT 1090345 is green; master tip 
`7866f5a36c0` adds no user of a removed symbol |
   | F-03 required `Check for secrets` red | Blocker | Fixed. Commit 1 was 
rewritten and the check is green |
   | F-04 a running BE ignores a provider-family ALTER | Major | Fixed for the 
registry by `S3FileSystem::refresh`. That fix causes F-01 below, and the vault 
path is F-02 |
   | F-05 connection-limit scope undeclared | Major | Fixed. Item 1, the 
release note and 
[`obj_storage_config.h:49-51`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/obj_storage/obj_storage_config.h#L49-L51)
 |
   | F-06 to F-09 | Minor | Fixed: the vault sync logs the failing status, and 
the reset comment, item 2, the role_arn wording and the resolver header now 
match the code |
   | F-10 to F-13 | Minor | Fixed. Each mutation the previous review named now 
fails a test that ran in BE UT 1090345 |
   | F-14 to F-20 | Nit | Fixed |
   
   ## Findings
   
   ### F-01 · A provider change across S3 and Azure leaves already-bound 
rowsets on the old file system, so a revert or a key rotation no longer reaches 
them
   
   - **Severity**: Major
   - **Category**: functional-bug (lifecycle)
   - **Regression**: yes
   - Regression evidence: `git show 
7acfd9bfc27:be/src/io/fs/s3_file_system.cpp` lines 83-123 apply every update, 
provider included, to the one holder of a resource, and the base 
`update_s3_resource` keeps `existed_fs`. Item 3 promises "the same result as 
after a restart" and says that a policy-referenced resource "still fails on 
access, as before". It does not say that rowsets which already resolved the 
resource stay on the old backend.
   - **Where**: 
[`be/src/io/fs/s3_file_system.cpp:78-86`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/fs/s3_file_system.cpp#L78-L86)
   
   ```cpp
   Result<std::shared_ptr<S3FileSystem>> 
S3FileSystem::refresh(std::shared_ptr<S3FileSystem> fs,
                                                               ObjStorageConfig 
config) {
       const auto& holder = fs->client_holder();
       if (holder->state()->config.backend.index() != 
config.client.backend.index()) {
           return create(std::move(config), fs->id());
       }
       RETURN_IF_ERROR_RESULT(holder->reset(config.client));
       return fs;
   }
   ```
   
   **What is wrong.** For another backend, `refresh` builds a second file 
system with the same id, and `update_s3_resource` registers it 
([`task_worker_pool.cpp:1809-1828`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/agent/task_worker_pool.cpp#L1809-L1828)).
 Nothing re-points the objects that already hold the first one. A remote rowset 
binds its storage resource once, lazily on first use or eagerly at cooldown, 
and never resolves it again 
([`rowset_meta.cpp:199-201`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/storage/rowset/rowset_meta.cpp#L199-L201),
 
[`rowset_meta.cpp:220-223`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/storage/rowset/rowset_meta.cpp#L220-L223)).
 Its segments open through that file system 
([`beta_rowset.cpp:278`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/storage/rowset/beta_rowset.cpp#L278))
 and read 
 through that file system's holder 
([`s3_file_system.cpp:136`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/fs/s3_file_system.cpp#L136),
 
[`s3_file_reader.cpp:134`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/fs/s3_file_reader.cpp#L134)).
 Later pushes refresh only the registered object 
([`task_worker_pool.cpp:1872-1878`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/agent/task_worker_pool.cpp#L1872-L1878)).
 The holder exists so that cached readers follow every update 
([`obj_client_holder.h:46-48`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/obj_storage/obj_client_holder.h#L46-L48)),
 and at the merge base each resource had exactly one.
   
   **Why it happens.** Commit b343fc6cd2e assumes that only in-flight requests 
hold the old file system ("requests that hold the old one finish on it"). The 
new test asserts the split as intended 
([`obj_client_factory_test.cpp:529-530`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/test/io/obj_storage/obj_client_factory_test.cpp#L529-L530)).
 On the FE side, a resource referenced by a storage policy keeps its endpoint, 
region, root path and bucket frozen, but not its provider 
([`S3Resource.java:250-263`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/fe/fe-core/src/main/java/org/apache/doris/catalog/S3Resource.java#L250-L263)).
 The ping is skipped when `s3_validity_check` is false 
([`S3Resource.java:284-297`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/fe/fe-core/src/main/java/org/apache/doris/catalog/S3Resource.java#L284-L297)).
 When the ping runs, it goes through the first filesystem 
 plugin that accepts the properties. Plugins load in directory-name order, so 
Azure comes first 
([`DirectoryPluginRuntimeManager.java:213-216`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/fe/fe-extension-loader/src/main/java/org/apache/doris/extension/loader/DirectoryPluginRuntimeManager.java#L213-L216),
 
[`FileSystemPluginManager.java:203-209`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/fe/fe-core/src/main/java/org/apache/doris/fs/FileSystemPluginManager.java#L203-L209)).
 The Azure plugin accepts a map that says `provider=azure` or that names an 
Azure blob endpoint 
([`AzureFileSystemProvider.java:63-80`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/fe/fe-filesystem/fe-filesystem-azure/src/main/java/org/apache/doris/filesystem/azure/AzureFileSystemProvider.java#L63-L80)).
 For an S3 resource, the switch to azure therefore fails the ping and needs 
`s3_validity_check=false`. For an Azure
  resource, both steps of a round trip through `provider=S3` ping the unchanged 
Azure target and pass with every setting at its default.
   
   **When it bites.** Either of two cases triggers it on a non-cloud cluster. 
One is an Azure resource with default settings that is switched to S3 and back. 
The other is an S3 resource with `s3_validity_check=false`, a setting the 
regression suites use 47 times across 40 files, that is switched to azure and 
back. The table shows the S3 case, for a resource `r` that is referenced by a 
storage policy and has cooled rowsets.
   
   | Step | Merge base | This head |
   |---|---|---|
   | 1. `ALTER RESOURCE r PROPERTIES ("provider"="azure")` by mistake | The one 
holder becomes an Azure client on the frozen S3 endpoint, and every read of 
cooled data fails | A second, Azure file system is registered. Rowsets bound 
before the ALTER keep reading S3. Rowsets first opened now bind to the Azure 
one and fail |
   | 2. `ALTER RESOURCE r PROPERTIES ("provider"="S3")` to revert | The holder 
goes back to S3 and every reader recovers | A third, S3 file system is 
registered. The rowsets bound in step 1 keep failing until BE restart |
   | 3. Rotate `s3.access_key` / `s3.secret_key`, then revoke the old key | 
Every reader uses the new key | Only the third file system gets it. Rowsets 
bound before step 1 fail with 403 until BE restart |
   
   FE shows the corrected resource and the BE logs "successfully update s3 
resource", so nothing points at the stale binding. Rowsets can also bind during 
the window of step 1 without any user query. Cold data compaction on the 
cooldown replica reads its inputs every 1800 seconds, and rowsets loaded at a 
restart or followed from another replica start unbound. No data is lost: the 
objects stay where they were. This was traced in the code and was not run on a 
cluster.
   
   **Severity rationale.** It is a regression in `functional-bug`, so it is at 
least Major. It is not a Blocker. No data is lost and no result is wrong, 
because reads fail with an error. A BE restart heals it, and the trigger needs 
ADMIN. In one cell this head is better than the merge base. During the mistaken 
window, rowsets bound before step 1 keep working, where the base failed all of 
them. The flip side is that the broken resource stays hidden until the next 
restart.
   
   **Suggested fix.** Keep one holder for the readers as long as the storage 
target stays. The backend may change while the bucket and the normalized prefix 
stay the same; under a storage policy that is always the case, because FE 
freezes both. Then rebind the existing holder in place to a client built from 
`config.client` as it is, without merging. Validate it through 
`ObjClientFactory`, publish it with the existing generation check, and return 
`fs`. Build a new file system only when the bucket or the prefix moves, which 
is the case the old F-04 needs. A sketch, with illustrative helper names:
   
   ```diff
        const auto& holder = fs->client_holder();
   -    if (holder->state()->config.backend.index() != 
config.client.backend.index()) {
   -        return create(std::move(config), fs->id());
   -    }
   -    RETURN_IF_ERROR_RESULT(holder->reset(config.client));
   +    if (holder->state()->config.backend.index() == 
config.client.backend.index()) {
   +        RETURN_IF_ERROR_RESULT(holder->reset(config.client));
   +        return fs;
   +    }
   +    // Rowsets that already resolved this resource keep `fs` and its holder 
until BE restart,
   +    // so keep the holder while the target stays (FE freezes bucket and 
root path under a
   +    // storage policy). Only a moved target needs another file system.
   +    if (config.bucket != fs->bucket() || normalized_prefix(config.prefix) 
!= fs->prefix()) {
   +        return create(std::move(config), fs->id());
   +    }
   +    RETURN_IF_ERROR_RESULT(holder->rebind(config.client));
        return fs;
   ```
   
   Add a test that keeps `auto reader = fs->client_holder();` across S3, then 
Azure, then S3 again, with the same bucket and root path. Rotate the key 
through the registered file system and assert that `reader` has the rotated 
key. If the split is meant to stay, say so in item 3 and in 
[`s3_file_system.h:50-53`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/fs/s3_file_system.h#L50-L53):
 already-bound rowsets stay on the old backend until BE restart. As an FE 
follow-up, reject a provider change between Azure and the S3-compatible 
providers under a storage policy.
   
   ### F-02 · A vault id that is handed out again for another provider keeps 
the dropped vault's file system, and loads write into the old bucket
   
   - **Severity**: Major
   - **Category**: functional-loss (cloud storage vault refresh)
   - **Regression**: yes
   - Regression evidence: `git show 
7acfd9bfc27:be/src/cloud/cloud_storage_engine.cpp` calls the same reset. The 
base reset merges `provider` into the kept target (`git show 
7acfd9bfc27:be/src/io/fs/s3_file_system.cpp` lines 83-123). The client 
therefore switched to the new provider against the old endpoint, and every 
request failed: `git show 7acfd9bfc27:be/src/util/s3_util.cpp` lines 315-319 
build the Azure URI from the kept endpoint. Item 3 says "Storage vaults cannot 
change their provider", which holds for ALTER but not for a reused id.
   - **Where**: 
[`be/src/cloud/cloud_storage_engine.cpp:204`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/cloud/cloud_storage_engine.cpp#L204)
   
   ```cpp
           const auto& s3_conf = resolved.value();
           DCHECK_EQ(fs->type(), io::FileSystemType::S3) << id;
           auto s3_fs = std::static_pointer_cast<io::S3FileSystem>(fs);
           auto client_holder = s3_fs->client_holder();
           auto st = client_holder->reset(s3_conf.client);
           if (!st.ok()) {
               LOG(WARNING) << "failed to update s3 fs, resource_id=" << id << 
": " << st;
           }
           return st;
   ```
   
   **What is wrong.** The fix for the old F-04 went into `update_s3_resource` 
only. The vault refresh still calls `reset`, which now refuses another backend 
([`obj_storage_config.cpp:207-212`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/obj_storage/obj_storage_config.cpp#L207-L212)).
 The BE keeps the file system it registered for the id, and no other path 
replaces it.
   
   **Why it happens.** ALTER cannot change the provider of a vault 
([`meta_service_resource.cpp:1089-1096`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/cloud/src/meta-service/meta_service_resource.cpp#L1089-L1096)),
 but a drop can free its id. The MS HTTP API `drop_hdfs_vault` removes any 
vault by name 
([`http_helper.cpp:524`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/cloud/src/common/http_helper.cpp#L524),
 
[`meta_service_resource.cpp:1699-1704`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/cloud/src/meta-service/meta_service_resource.cpp#L1699-L1704),
 
[`meta_service_resource.cpp:892-915`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/cloud/src/meta-service/meta_service_resource.cpp#L892-L915)),
 and it is the only drop the MS accepts 
([`meta_service_resource.cpp:1744-1751`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/
 cloud/src/meta-service/meta_service_resource.cpp#L1744-L1751)). A new vault 
gets the largest existing id plus one 
([`meta_service_resource.cpp:673-697`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/cloud/src/meta-service/meta_service_resource.cpp#L673-L697)),
 which is the dropped id when that vault held the largest. The BE never 
unregisters a vault: `delete_storage_resource` 
([`storage_policy.cpp:127`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/storage/storage_policy.cpp#L127))
 has no caller. Sync therefore finds the old file system under the id 
([`cloud_storage_engine.cpp:496-502`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/cloud/cloud_storage_engine.cpp#L496-L502)),
 and loads resolve it 
([`cloud_rowset_builder.cpp:115`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/cloud/cloud_rowset_builder.cpp#L115)).
   
   **When it bites.** A cloud cluster in vault mode:
   
   | Step | Merge base | This head |
   |---|---|---|
   | 1. `CREATE STORAGE VAULT s3v` (S3, endpoint E1, bucket b1) gets id N | BEs 
register an S3 file system under N | Same |
   | 2. An operator drops `s3v` with `drop_hdfs_vault` and creates `azv` 
(Azure, endpoint E2, container c2), which gets N again | Sync resets the holder 
to an Azure client on E1, and every load into `azv` fails | Sync logs a WARNING 
and keeps the S3 file system. Loads into `azv` succeed, into b1 with the old 
keys |
   | 3. BE restart | `azv` is built on E2/c2 and nothing is misplaced | `azv` 
is built on E2/c2. The rowsets of step 2 are not there, and queries on them 
fail |
   
   One natural way to get here is to fix a vault created with the wrong 
provider by dropping it and creating it again. Only the cross-family cell 
changed: a same-family reuse misbinds the same way at the merge base and at 
this head. This was traced in the code and was not run on a cluster.
   
   **Severity rationale.** It is rated at the floor, for the same reason as the 
old F-04: the loud failure at the merge base protected the data, and at this 
head the data lands in the old bucket. It is not a Blocker for two reasons. The 
objects exist and can be copied by hand. And the trigger needs an operator to 
drop an object vault through an API named for HDFS vaults, a gap in the meta 
service that predates this PR.
   
   **Suggested fix.** The root fix belongs in the meta service: never hand out 
a dropped vault id again, and make `remove_hdfs_storage_vault` refuse an object 
vault. A BE change alone is not enough. Registering a new file system from 
`RefreshFSVaultVisitor`, through `S3FileSystem::refresh` and 
`put_storage_resource(id, {fs, path_format}, 0)`, would fix the registry. But 
`PackedFileManager` keeps its own file system per vault id for the life of the 
process 
([`packed_file_manager.cpp:292-296`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/fs/packed_file_manager.cpp#L292-L296)),
 so the packed small files of new loads would still go to the old bucket. A 
vault id reused from an HDFS vault also reaches this visitor with an 
`HdfsFileSystem`, so a BE fix must check `fs->type()` before the cast at 
[`cloud_storage_engine.cpp:202`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/cloud/cloud_storage_engine.cpp#L202);
 
 that cast is the same at the merge base. If the BE side changes in this PR, 
either evict that cache entry too, or make loads for an id whose backend 
changed fail instead of keeping the old target. Then qualify item 3.
   
   ### F-03 · Two new comments give a false reason why Recycler does not check 
the Azure key pair
   
   - **Severity**: Minor
   - **Category**: wording (cross-component invariant)
   - **Regression**: no
   - Regression evidence: BE and Recycler behave as at the merge base for every 
stored shape; only the stated reason is wrong.
   - **Where**: 
[`cloud/src/recycler/s3_accessor.cpp:399-401`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/cloud/src/recycler/s3_accessor.cpp#L399-L401)
 and 
[`common/cpp/obj-client/auth/azure/azure_auth.h:49-51`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/common/cpp/obj-client/auth/azure/azure_auth.h#L49-L51)
   
   ```cpp
           // Storage metadata carries the SharedKey pair as ak/sk, which the 
meta service requires
           // together for Azure when it stores them; the SDK credential comes 
from the
           // authentication shared with BE.
   ```
   
   **What is wrong.** The meta service requires the pair only when an Azure 
entry is created. ALTER STORAGE VAULT checks only that both keys are sent, not 
that both are non-empty 
([`meta_service_resource.cpp:1180-1194`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/cloud/src/meta-service/meta_service_resource.cpp#L1180-L1194)).
 The MS HTTP calls `alter_obj_info`, `legacy_update_ak_sk` and `update_ak_sk` 
do the same. So `ALTER STORAGE VAULT azv PROPERTIES ("type"="S3", 
"s3.access_key"="acct", "s3.secret_key"="")` stores an Azure vault with half a 
pair.
   
   **Why it happens.** Commit 0225126bc43 removed the check again, rightly, and 
gave this reason. The real reason is that `S3Conf::from_obj_store_info` loads 
ak and sk only as a pair 
([`s3_accessor.cpp:276-292`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/cloud/src/recycler/s3_accessor.cpp#L276-L292)),
 so Recycler never sees half a pair. A failed accessor init would also fail the 
instance's recycler init.
   
   **When it bites.** The header is where the next authentication methods (SAS, 
OAuth2, Managed Identity) are added. Someone who trusts it will skip a check on 
the assumption that the meta service already validated the input.
   
   **Suggested fix.** In 
[`s3_accessor.cpp:399-401`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/cloud/src/recycler/s3_accessor.cpp#L399-L401)
 write that `S3Conf::from_obj_store_info` loads the pair complete or not at 
all. In 
[`azure_auth.h:49-51`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/common/cpp/obj-client/auth/azure/azure_auth.h#L49-L51)
 drop "that the meta service validated when it stored it". In the PR body, say 
that the meta service requires the pair when an Azure entry is created.
   
   ### F-04 · No test that runs in CI pins which value becomes the Azure 
SharedKey account and which the key
   
   - **Severity**: Minor
   - **Category**: test-coverage (Azure SharedKey wiring)
   - **Regression**: no
   - Regression evidence: `git show 
7acfd9bfc27:common/cpp/obj-client/auth/azure_auth_factory.cpp` built the same 
credential, and its only test was the same empty case. The signature changed 
from named struct fields to adjacent positional `std::string_view` arguments, 
so a swap no longer stands out.
   - **Where**: 
[`be/test/io/fs/azure_obj_storage_client_test.cpp:85-90`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/test/io/fs/azure_obj_storage_client_test.cpp#L85-L90)
   
   ```cpp
   TEST(AzureAuthFactoryTest, AllowsEmptySharedKeyCredentials) {
       auto result = 
AzureAuthFactory::create("https://account.blob.core.windows.net/container";, "",
                                              AzureSharedKeyConfig 
{.account_key = ""}, {});
   
       EXPECT_TRUE(result);
   }
   ```
   
   **What is wrong.** Three swaps would pass both BE UT and Cloud UT: the 
account and the key in 
[`azure_auth_factory.cpp:30-31`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/common/cpp/obj-client/auth/azure/azure_auth_factory.cpp#L30-L31),
 `conf_.ak` and `conf_.sk` at 
[`s3_accessor.cpp:402-403`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/cloud/src/recycler/s3_accessor.cpp#L402-L403),
 or the container for the account at 
[`azure_client_provider.cpp:113-114`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/obj_storage/azure_client_provider.cpp#L113-L114).
 Every Azure request of BE and Recycler would then fail authentication.
   
   **Why it happens.** The only direct test passes empty values. The BE tests 
that build Azure clients check identity, the rate-limit wrapper or file-system 
fields. The suites that would authenticate are skipped in CI: in Cloud UT 
1090347 `S3AccessorTest.azure` and the four `S3AccessorClientTest.Azure*` cases 
did not run, and neither did the three `AzureObjStorageClientTest` cases in BE 
UT 1090345. Commit 0225126bc43 still names "cloud UT in CI" as the test of the 
Recycler change.
   
   **When it bites.** The next change to this factory is the SAS / OAuth2 work 
the PR body announces.
   
   **Suggested fix.** Give `AzureAuthFactoryTest` a non-empty account and key. 
Then assert `result.shared_key_credential->AccountName`, the key (BE UT is 
built without access control), and `result.container_client->GetUrl()`. Add one 
Recycler case that creates an Azure `S3Accessor` from fake ak/sk and checks the 
credential's account name; it needs no network.
   
   ### F-05 · `ObjClientFactoryTest` counts builds through a process-wide cache 
and cannot be repeated in one process
   
   - **Severity**: Nit
   - **Category**: maintainability (test isolation)
   - **Regression**: no
   - Regression evidence: new test file.
   - **Where**: 
[`be/test/io/obj_storage/obj_client_factory_test.cpp:98-103`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/test/io/obj_storage/obj_client_factory_test.cpp#L98-L103)
   
   ```cpp
           _sync_point->set_call_back(
                   "s3_client_factory::create",
                   [this](auto&& args) {
                       ++_s3_builds;
                       if (_on_s3_build) {
                           // Run once: a nested build must not re-enter the 
hook.
   ```
   
   **What is wrong.** The hook runs only on a miss in the provider cache of the 
process-wide `ObjClientFactory` 
([`s3_client_provider.cpp:42-59`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/obj_storage/s3_client_provider.cpp#L42-L59)),
 and nothing clears that cache. With `GTEST_REPEAT=2`, the second iteration 
fails `S3ProviderCachesByFullConfig`, `HolderRejectsResetMergedOntoStaleState` 
and `FactoryValidatesBeforeBuilding`. A single run is fine, because no other 
test uses these endpoints, and BE UT 1090345 ran all 16 cases.
   
   **Suggested fix.** Add a `BE_TEST`-only cache reset next to 
`set_client_creator_for_test` and call it in `SetUp`, or make the counted 
endpoints unique per test instance.
   
   ### F-06 · The refresh test does not pin a vendor change inside the S3 family
   
   - **Severity**: Nit
   - **Category**: test-coverage (refresh backend dispatch)
   - **Regression**: no
   - Regression evidence: new function and test.
   - **Where**: 
[`be/test/io/obj_storage/obj_client_factory_test.cpp:498-531`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/test/io/obj_storage/obj_client_factory_test.cpp#L498-L531)
   
   ```cpp
   TEST_F(ObjClientFactoryTest, 
RefreshResetsTheSameBackendAndRebuildsAnotherOne) {
       auto param = make_s3_param("refresh-s3.example.com");
   ```
   
   **What is wrong.** The test covers AWS to AWS and AWS to Azure. If 
[`s3_file_system.cpp:81`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/fs/s3_file_system.cpp#L81)
 compared `provider()` instead of `backend.index()`, every suite would pass. 
Every ALTER from AWS to OSS, COS or GCP would then build a new file system 
instead of resetting the existing one in place, and F-01 would apply to those 
too.
   
   **Suggested fix.** Refresh with the same parameter under provider OSS and 
another endpoint. Assert that the same `fs` comes back, that the endpoint is 
kept, and that the vendor is OSS.
   
   <details>
   <summary>Critical checkpoints</summary>
   
   | Checkpoint | Conclusion |
   |---|---|
   | Goal achieved, proven by a test | Mostly. The typed configuration, the 
per-backend clients and the Azure model shared with Recycler work and are 
tested. In BE UT 1090345 at this head `ObjClientFactoryTest` passed 16 of 16, 
`ObjStorageUriS3RulesTest` 9 of 9 and `S3ClientFactoryTest` 39 of 39. The fix 
for the old F-04 achieves only the registry half: F-01, F-02 |
   | Change is minimal, clear, focused | Large, but mostly a move. Commits 5 to 
13 are small, and each answers one earlier finding. 28854079f3e adds a Recycler 
check and 0225126bc43 takes it out again, which leaves only a comment (F-03) |
   | Concurrency: threads, shared state, locks, lock order | 
`update_s3_resource` and `S3FileSystem::refresh` run on the single 
PUSH_STORAGE_POLICY worker 
([`agent_server.cpp:145-146`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/agent/agent_server.cpp#L145-L146)).
 The registry is under its mutex, and the holder's generation check handles 
overlapping vault resets (item 2). No new lock and no new lock order |
   | Lifecycle / static init order | A backend change now publishes a second 
file system for one resource id, and readers that hold the first keep it until 
restart: F-01. No new static and no cross-TU initializer |
   | New config: dynamic? observed without restart? | n/a, no option added |
   | Incompatible change: rolling-upgrade path | No wire, persisted or config 
change, and `gensrc` is untouched. The `AzureAuthFactory` signature changed 
inside `common/cpp`, which BE and Recycler each link into their own binary; 
they exchange no Azure object |
   | Parallel code paths updated | Only two paths reset a holder: 
[`s3_file_system.cpp:84`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/fs/s3_file_system.cpp#L84)
 and 
[`cloud_storage_engine.cpp:204`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/cloud/cloud_storage_engine.cpp#L204).
 The fix went into the first only: F-02. A sweep of every file-system binding 
site in both modes found no third. The Azure plus credential rejection is now 
the same on all three flat sources |
   | Special conditionals explained | The backend-index branch of `refresh` is 
commented, but its premise ("as the next BE start would build it") does not 
hold for bound rowsets: F-01. The `has_value()` check in `vault_process_error` 
handles a real input 
([`cloud_storage_engine.cpp:134`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/cloud/cloud_storage_engine.cpp#L134))
 |
   | Test coverage (e2e, negative, unit) | The new tests close every test gap 
of the previous review. Still open: F-04, F-05, F-06. No test keeps a reader 
across a backend change (F-01) or refreshes a vault with another backend 
(F-02). No pipeline reaches Azure, so unit tests are the only gate there |
   | Test results added/modified and correct | n/a, no `.out` or groovy file 
changed |
   | Observability (logs, VLOG, metrics) | The resource WARNING now names the 
id and the name, and the vault WARNING carries the failing status. Nothing logs 
that a resource was split, and the BE logs success: F-01 |
   | Transaction / persistence / EditLog replay | n/a. The meta service and FE 
are not changed |
   | Data write atomicity, crash safety | The writer logic is untouched. F-02 
can place vault data in a dropped vault's bucket |
   | New FE↔BE variables on every send path | n/a, none added |
   | Performance and anti-patterns | No hot-path change. `refresh` adds one 
snapshot per push, and the include trimming in `be/src/util/s3_util.cpp` only 
reduces compile work |
   | Anything else | The CI `/review` run failed on model capacity (issue 
comment 6601595557), so the `code-review` status is still pending. The build 
hygiene gate passes at this head, run as a text scan |
   
   </details>
   
   <details>
   <summary>Considered and dismissed</summary>
   
   | # | Concern | Where | Why it is not an issue |
   |---|---|---|---|
   | D-01 | A rejected vault configuration changes which file system becomes 
`latest_fs`, or drops the vault | 
[`cloud_storage_engine.cpp:510`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/cloud/cloud_storage_engine.cpp#L510)
 | The entry keeps its place 
([`cloud_meta_mgr.cpp:1913-1916`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/cloud/cloud_meta_mgr.cpp#L1913-L1916)).
 Create returns the error and `get_filesystem(id)` stays null; refresh returns 
it and keeps the old file system. Both match a failed create or reset at the 
merge base, whose `S3Conf::get_s3_conf` never failed. `StorageVaultInfos` has 
no other consumer |
   | D-02 | The RFC 3986 flag set through `SDKOptions` behaves differently | 
[`s3_util.cpp:49-50`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/util/s3_util.cpp#L49-L50)
 | aws-sdk-cpp 1.11.221, the pinned version 
([`vars.sh:391-394`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/thirdparty/vars.sh#L391-L394)),
 applies `httpOptions.compliantRfc3986Encoding` inside `InitAPI` (Aws.cpp, line 
157). This is the only `InitAPI` in `be/src`, and 
`S3ClientFactoryTest.InitializesSdkWithRfc3986KeyEncoding` ran |
   | D-03 | Recycler builds its Azure client differently | 
[`s3_accessor.cpp:402-403`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/cloud/src/recycler/s3_accessor.cpp#L402-L403)
 | Same account, key and options as `git show 
7acfd9bfc27:cloud/src/recycler/s3_accessor.cpp`. The init-time check of 
28854079f3e is gone again (0225126bc43), and the branch is under `#ifdef 
USE_AZURE` |
   | D-04 | The Azure cache hash moved from crc32 to `std::hash` | 
[`azure_auth.cpp:61`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/common/cpp/obj-client/auth/azure/azure_auth.cpp#L61)
 | It is in-process only, and the client caches compare the full configuration 
([`obj_client_cache.h:53`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/obj_storage/obj_client_cache.h#L53))
 |
   | D-05 | The merge resolution, or the 12 later master commits, break compile 
or hygiene | 
[`s3_util.cpp:20-34`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/util/s3_util.cpp#L20-L34)
 | Include lines only. COMPILE 1090348 and BE UT 1090345 are green. The later 
master commits touch no header and no object-storage file, the trial merge with 
`7866f5a36c0` is clean, and the removed symbols occur only in comments |
   | D-06 | An unknown `TObjStorageType` or `TCredProviderType` reaches 
`LOG_FATAL` / `__builtin_unreachable` | 
[`obj_storage_config_resolver.cpp:403`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/obj_storage/obj_storage_config_resolver.cpp#L403)
 | Identical at the merge base (`git show 7acfd9bfc27:be/src/util/s3_util.cpp` 
lines 595-600, 658-660, 727-729). Not this PR |
   | D-07 | Unity collisions of the new internal names in `common/cpp` | 
[`CMakeLists.txt:27`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/common/cpp/CMakeLists.txt#L27)
 | `CommonCPP` sets no UNITY_BUILD property; only the BE pilot targets do |
   | D-08 | "Storage vaults cannot change their provider" is false even for 
ALTER | 
[`meta_service_resource.cpp:1093`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/cloud/src/meta-service/meta_service_resource.cpp#L1093)
 | ALTER rejects `provider`, and FE does not allow it in ALTER STORAGE VAULT. 
Only a reused id reaches the BE: F-02 |
   | D-09 | The split of F-01 misplaces cold data | 
[`tablet.cpp:2143`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/storage/tablet/tablet.cpp#L2143)
 | Cooldown, the output of cold data compaction and remote GC resolve the 
registry each time. Schema change writes through the input rowset's binding, 
whose endpoint and bucket are frozen under a storage policy. F-01 makes reads 
fail; it does not move data |
   | D-10 | `S3FileWriterTest` releases its S3 client after the SDK shuts down 
| 
[`s3_file_writer_test.cpp:332`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/test/io/fs/s3_file_writer_test.cpp#L332)
 | The upload pool drains first, and ExecEnv gets its old FileCacheFactory back 
before the owned one is destroyed. The ObjClientFactory cache that also holds 
the client is destroyed before `ShutdownAPI`. The `s3_fs.reset()` keeps the 
namespace-scope static from holding the last reference |
   | D-11 | Callers keep an `if (!built)` branch that can no longer fire | 
[`azure_client_provider.cpp:115`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/obj_storage/azure_client_provider.cpp#L115)
 | Harmless, and it becomes live with the next authentication method |
   | D-12 | A vault id reused across HDFS and S3 casts to the wrong file system 
type | 
[`cloud_storage_engine.cpp:202`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/cloud/cloud_storage_engine.cpp#L202)
 | Identical at the merge base. Not this PR; it matters for the fix of F-02 |
   | D-13 | The Azure role_arn plus half-pair cell is also reachable from the 
meta service | 
[`obj_storage_config.cpp:143-151`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/obj_storage/obj_storage_config.cpp#L143-L151)
 | True: ADD_OBJ_INFO, then `update_ak_sk`. Item 4 declares it: such a 
configuration could not authenticate before either |
   | D-14 | The first-sync connectivity check of a vault never runs | 
[`cloud_storage_engine.cpp:488`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/cloud/cloud_storage_engine.cpp#L488)
 | `first_sync_storage_vault` starts at `true` and the compare-and-swap expects 
`false`. This is the same at the merge base and not this PR; it is worth a 
separate issue |
   | D-15 | Earlier dismissals D-01 to D-17 of the af4964b review | 
[`s3_file_system.cpp:398`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/fs/s3_file_system.cpp#L398)
 | Re-checked at this head. D-02 is now at 
[`s3_file_system.cpp:398`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/fs/s3_file_system.cpp#L398),
 D-08 at 
[`agent_server.cpp:145-146`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/agent/agent_server.cpp#L145-L146)
 and D-15 at 
[`azure_client_provider.cpp:119-120`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/obj_storage/azure_client_provider.cpp#L119-L120).
 The others name byte-identical code. D-16 (check_coverage red) is obsolete, 
because it is green |
   | D-16 | The reset log prints the update rather than the merged 
configuration that is published | 
[`obj_client_holder.cpp:55`](https://github.com/apache/doris/blob/f58598d426a9d97a110dba1f1e85f6279f0607b9/be/src/io/obj_storage/obj_client_holder.cpp#L55)
 | The same at the merge base (`git show 
7acfd9bfc27:be/src/io/fs/s3_file_system.cpp` line 114 prints `conf`). 
Credentials are masked in both |
   
   </details>
   
   <details>
   <summary>Coverage and limits</summary>
   
   - Files reviewed in depth: the 31 files whose PR hunk changed since 
`af4964b` or is new (22 changed, 9 new) were read in full, or every hunk with 
its function and callers. Also read: the storage-side holders of a resource 
file system, the meta-service vault writers, and the FE resource, vault and 
filesystem-plugin code the findings depend on. None of these is changed by the 
PR.
   - Carried forward: 35 files are blob-identical to what the `af4964b` review 
read in full, at both bases and at both heads. Three more test files have the 
same PR hunk on a moved base and were checked against master's changes.
   - Not verified locally: nothing was built, no test was run, and nothing was 
executed on a cluster. The sequences in F-01 and F-02 were traced in the code. 
The only command run against the tree was the build hygiene gate, which is a 
text scan; the worktree was unchanged afterwards. CI evidence comes from 
TeamCity at this head: BE UT 1090345, Cloud UT 1090347, COMPILE 1090348 and the 
regression gates.
   - Evidence taken from outside the repository: aws-sdk-cpp tag 1.11.221 
sources (Aws.cpp, URI.cpp, URI.h) fetched through the GitHub API; Azure SDK 
headers under the installed thirdparty; the TeamCity test lists of BE UT 
1090345 and Cloud UT 1090347.
   - Inherited from earlier runs: `719de6a` (2026-10-26) and `af4964b` 
(2026-10-31). All 20 findings of `af4964b` are fixed at this head. Of its 17 
dismissals, 16 were re-confirmed and carried forward, and one is obsolete.
   - Convergence: converged after 2 rounds. The verdict has been stable since 
round 1. Round 1 produced both Majors, so it did not converge on its own. Round 
2 produced no new finding and only made the trigger of F-01 and the fix of F-02 
more precise.
   
   </details>
   <!-- doris-repo-review:report:end -->
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to