This is an automated email from the ASF dual-hosted git repository.

dataroaring pushed a commit to branch branch-3.0
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-3.0 by this push:
     new 79fcf4fb905 branch-3.0: [fix](s3client) Avoild dead loop when storage 
not support `ListObjectsV2` #50252 (#50413)
79fcf4fb905 is described below

commit 79fcf4fb9051ab198af3a99332884e2e01a4cc34
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Apr 25 19:06:08 2025 +0800

    branch-3.0: [fix](s3client) Avoild dead loop when storage not support 
`ListObjectsV2` #50252 (#50413)
    
    Cherry-picked from #50252
    
    Co-authored-by: Lei Zhang <zhang...@selectdb.com>
---
 be/src/io/fs/s3_obj_storage_client.cpp            |   6 ++
 be/test/io/fs/s3_obj_stroage_client_mock_test.cpp | 121 ++++++++++++++++++++++
 cloud/src/recycler/s3_obj_client.cpp              |  11 ++
 cloud/test/CMakeLists.txt                         |   4 +
 cloud/test/s3_accessor_mock_test.cpp              |  43 +++++++-
 5 files changed, 183 insertions(+), 2 deletions(-)

diff --git a/be/src/io/fs/s3_obj_storage_client.cpp 
b/be/src/io/fs/s3_obj_storage_client.cpp
index e9d8d5be157..c6cd48f8386 100644
--- a/be/src/io/fs/s3_obj_storage_client.cpp
+++ b/be/src/io/fs/s3_obj_storage_client.cpp
@@ -322,6 +322,12 @@ ObjectStorageResponse 
S3ObjStorageClient::list_objects(const ObjectStoragePathOp
             files->push_back(std::move(file_info));
         }
         is_trucated = outcome.GetResult().GetIsTruncated();
+        if (is_trucated && 
outcome.GetResult().GetNextContinuationToken().empty()) {
+            return {convert_to_obj_response(Status::InternalError(
+                    "failed to list {}, is_trucated is true, but next 
continuation token is empty",
+                    opts.prefix))};
+        }
+
         
request.SetContinuationToken(outcome.GetResult().GetNextContinuationToken());
     } while (is_trucated);
     return ObjectStorageResponse::OK();
diff --git a/be/test/io/fs/s3_obj_stroage_client_mock_test.cpp 
b/be/test/io/fs/s3_obj_stroage_client_mock_test.cpp
new file mode 100644
index 00000000000..2fb61c92201
--- /dev/null
+++ b/be/test/io/fs/s3_obj_stroage_client_mock_test.cpp
@@ -0,0 +1,121 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <aws/core/Aws.h>
+#include <aws/s3/S3Client.h>
+#include <aws/s3/model/ListObjectsV2Request.h>
+#include <aws/s3/model/ListObjectsV2Result.h>
+#include <aws/s3/model/Object.h>
+
+#include "gmock/gmock.h"
+#include "io/fs/s3_obj_storage_client.h"
+#include "util/s3_util.h"
+
+using namespace Aws::S3::Model;
+
+namespace doris::io {
+class MockS3Client : public Aws::S3::S3Client {
+public:
+    MockS3Client() {};
+
+    MOCK_METHOD(Aws::S3::Model::ListObjectsV2Outcome, ListObjectsV2,
+                (const Aws::S3::Model::ListObjectsV2Request& request), (const, 
override));
+};
+
+class S3ObjStorageClientMockTest : public testing::Test {
+    static void SetUpTestSuite() { S3ClientFactory::instance(); };
+    static void TearDownTestSuite() {};
+
+private:
+    static Aws::SDKOptions options;
+};
+
+Aws::SDKOptions S3ObjStorageClientMockTest::options {};
+
+TEST_F(S3ObjStorageClientMockTest, list_objects_compatibility) {
+    // If storage only supports ListObjectsV1, 
s3_obj_storage_client.list_objects
+    // should return an error.
+    auto mock_s3_client = std::make_shared<MockS3Client>();
+    S3ObjStorageClient s3_obj_storage_client(mock_s3_client);
+
+    std::vector<io::FileInfo> files;
+
+    ListObjectsV2Result result;
+    result.SetIsTruncated(true);
+    EXPECT_CALL(*mock_s3_client, ListObjectsV2(testing::_))
+            .WillOnce(testing::Return(ListObjectsV2Outcome(result)));
+
+    auto response = s3_obj_storage_client.list_objects(
+            {.bucket = "dummy-bucket", .prefix = 
"S3ObjStorageClientMockTest/list_objects_test"},
+            &files);
+
+    EXPECT_EQ(response.status.code, ErrorCode::INTERNAL_ERROR);
+    files.clear();
+}
+
+ListObjectsV2Result CreatePageResult(const std::string& nextToken,
+                                     const std::vector<std::string>& keys, 
bool isTruncated) {
+    ListObjectsV2Result result;
+    result.SetIsTruncated(isTruncated);
+    result.SetNextContinuationToken(nextToken);
+    for (const auto& key : keys) {
+        Object obj;
+        obj.SetKey(key);
+        result.AddContents(std::move(obj));
+    }
+    return result;
+}
+
+TEST_F(S3ObjStorageClientMockTest, list_objects_with_pagination) {
+    auto mock_s3_client = std::make_shared<MockS3Client>();
+    S3ObjStorageClient s3_obj_storage_client(mock_s3_client);
+
+    std::vector<std::vector<std::string>> pages = {
+            {"key1", "key2"}, // page1
+            {"key3", "key4"}, // page2
+            {"key5"}          // page3
+    };
+
+    EXPECT_CALL(*mock_s3_client, ListObjectsV2(testing::_))
+            .WillOnce([&](const ListObjectsV2Request& req) {
+                // page1:no ContinuationToken
+                EXPECT_FALSE(req.ContinuationTokenHasBeenSet());
+                return Aws::S3::Model::ListObjectsV2Outcome(
+                        CreatePageResult("token1", pages[0], true));
+            })
+            .WillOnce([&](const ListObjectsV2Request& req) {
+                // page2: token1
+                EXPECT_EQ(req.GetContinuationToken(), "token1");
+                return ListObjectsV2Outcome(CreatePageResult("token2", 
pages[1], true));
+            })
+            .WillOnce([&](const ListObjectsV2Request& req) {
+                // page3: token2
+                EXPECT_EQ(req.GetContinuationToken(), "token2");
+                return ListObjectsV2Outcome(CreatePageResult("", pages[2], 
false));
+            });
+
+    std::vector<io::FileInfo> files;
+    auto response = s3_obj_storage_client.list_objects(
+            {.bucket = "dummy-bucket",
+             .prefix = 
"S3ObjStorageClientMockTest/list_objects_with_pagination"},
+            &files);
+
+    EXPECT_EQ(response.status.code, ErrorCode::OK);
+    EXPECT_EQ(files.size(), 5);
+    files.clear();
+}
+} // namespace doris::io
\ No newline at end of file
diff --git a/cloud/src/recycler/s3_obj_client.cpp 
b/cloud/src/recycler/s3_obj_client.cpp
index 0e548819d25..a5a8977e17b 100644
--- a/cloud/src/recycler/s3_obj_client.cpp
+++ b/cloud/src/recycler/s3_obj_client.cpp
@@ -107,6 +107,17 @@ public:
             return false;
         }
 
+        if (outcome.GetResult().GetIsTruncated() &&
+            outcome.GetResult().GetNextContinuationToken().empty()) {
+            LOG_WARNING("failed to list objects, isTruncated but no 
continuation token")
+                    .tag("endpoint", endpoint_)
+                    .tag("bucket", req_.GetBucket())
+                    .tag("prefix", req_.GetPrefix());
+
+            is_valid_ = false;
+            return false;
+        }
+
         has_more_ = outcome.GetResult().GetIsTruncated();
         req_.SetContinuationToken(std::move(
                 
const_cast<std::string&&>(outcome.GetResult().GetNextContinuationToken())));
diff --git a/cloud/test/CMakeLists.txt b/cloud/test/CMakeLists.txt
index 51affb8a46f..65c9cde561b 100644
--- a/cloud/test/CMakeLists.txt
+++ b/cloud/test/CMakeLists.txt
@@ -49,6 +49,8 @@ add_executable(fdb_injection_test fdb_injection_test.cpp)
 
 add_executable(s3_accessor_test s3_accessor_test.cpp)
 
+add_executable(s3_accessor_mock_test s3_accessor_mock_test.cpp)
+
 add_executable(hdfs_accessor_test hdfs_accessor_test.cpp)
 
 add_executable(stopwatch_test stopwatch_test.cpp)
@@ -86,6 +88,8 @@ target_link_libraries(http_encode_key_test ${TEST_LINK_LIBS})
 
 target_link_libraries(s3_accessor_test ${TEST_LINK_LIBS})
 
+target_link_libraries(s3_accessor_mock_test ${TEST_LINK_LIBS})
+
 target_link_libraries(hdfs_accessor_test ${TEST_LINK_LIBS})
 
 target_link_libraries(stopwatch_test ${TEST_LINK_LIBS})
diff --git a/cloud/test/s3_accessor_mock_test.cpp 
b/cloud/test/s3_accessor_mock_test.cpp
index b02c10ff8cc..5f1e1cc299c 100644
--- a/cloud/test/s3_accessor_mock_test.cpp
+++ b/cloud/test/s3_accessor_mock_test.cpp
@@ -15,13 +15,21 @@
 // specific language governing permissions and limitations
 // under the License.
 
+#include <aws/core/Aws.h>
+#include <aws/s3/S3Client.h>
+#include <aws/s3/model/ListObjectsV2Request.h>
+#include <aws/s3/model/ListObjectsV2Result.h>
+#include <aws/s3/model/Object.h>
+#include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
 #include "common/config.h"
 #include "common/logging.h"
 #include "cpp/sync_point.h"
+#include "recycler/s3_obj_client.h"
 
 using namespace doris;
+using namespace Aws::S3::Model;
 
 int main(int argc, char** argv) {
     const std::string conf_file = "doris_cloud.conf";
@@ -40,8 +48,39 @@ int main(int argc, char** argv) {
 
 namespace doris::cloud {
 
-TEST(S3ObjClientTest, list_objects) {
-    // TODO
+class S3AccessorMockTest : public testing::Test {
+    static void SetUpTestSuite() { Aws::InitAPI(S3AccessorMockTest::options); 
};
+    static void TearDownTestSuite() { Aws::ShutdownAPI(options); };
+
+private:
+    static Aws::SDKOptions options;
+};
+
+Aws::SDKOptions S3AccessorMockTest::options {};
+class MockS3Client : public Aws::S3::S3Client {
+public:
+    MockS3Client() {};
+
+    MOCK_METHOD(Aws::S3::Model::ListObjectsV2Outcome, ListObjectsV2,
+                (const Aws::S3::Model::ListObjectsV2Request& request), (const, 
override));
+};
+
+TEST_F(S3AccessorMockTest, list_objects_compatibility) {
+    // If storage only supports ListObjectsV1, 
s3_obj_storage_client.list_objects
+    // should return an error.
+    auto mock_s3_client = std::make_shared<MockS3Client>();
+    S3ObjClient s3_obj_client(mock_s3_client, "dummy-endpoint");
+
+    ListObjectsV2Result result;
+    result.SetIsTruncated(true);
+    EXPECT_CALL(*mock_s3_client, ListObjectsV2(testing::_))
+            .WillOnce(testing::Return(ListObjectsV2Outcome(result)));
+
+    auto response = s3_obj_client.list_objects(
+            {.bucket = "dummy-bucket", .key = 
"S3AccessorMockTest/list_objects_compatibility"});
+
+    EXPECT_FALSE(response->has_next());
+    EXPECT_FALSE(response->is_valid());
 }
 
 } // namespace doris::cloud


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@doris.apache.org
For additional commands, e-mail: commits-h...@doris.apache.org

Reply via email to