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

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


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new 71eecdfc8b6 branch-4.2: [improvement](be) Skip stale S3 race reads and 
serve peer reads without get_or_set #69620 (#69803)
71eecdfc8b6 is described below

commit 71eecdfc8b6851eceb365ad214ee0c1ed9f64db3
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Oct 30 09:42:25 2026 +0800

    branch-4.2: [improvement](be) Skip stale S3 race reads and serve peer reads 
without get_or_set #69620 (#69803)
    
    Cherry-picked from #69620
    
    Co-authored-by: deardeng <[email protected]>
---
 be/src/cloud/cloud_internal_service.cpp            | 224 ++++++++-------
 be/src/io/cache/block_file_cache.cpp               |  17 +-
 be/src/io/cache/cached_remote_file_reader.cpp      |  15 +
 .../cache/cached_remote_file_reader_peer_test.cpp  | 303 +++++++++++++++++++++
 4 files changed, 461 insertions(+), 98 deletions(-)

diff --git a/be/src/cloud/cloud_internal_service.cpp 
b/be/src/cloud/cloud_internal_service.cpp
index 2a1974102b4..6f6fc852f61 100644
--- a/be/src/cloud/cloud_internal_service.cpp
+++ b/be/src/cloud/cloud_internal_service.cpp
@@ -446,6 +446,17 @@ void set_too_many_tasks_response(PFetchPeerDataResponse* 
response, const std::st
     response->mutable_status()->set_status_code(TStatusCode::TOO_MANY_TASKS);
 }
 
+// NOT_FOUND means this peer cannot serve the block now; the client rotates 
the candidate instead
+// of counting an RPC failure.
+Status reject_peer_block_not_found(PFetchPeerDataResponse* response, const 
std::string& msg) {
+    g_file_cache_get_by_peer_failed_num << 1;
+    g_file_cache_get_by_peer_not_downloaded_block_num << 1;
+    LOG(WARNING) << msg;
+    response->mutable_status()->add_error_msgs(msg);
+    response->mutable_status()->set_status_code(TStatusCode::NOT_FOUND);
+    return Status::NotFound<false>(msg);
+}
+
 bool try_reject_if_queue_timed_out(std::chrono::steady_clock::time_point 
enqueue_ts,
                                    PFetchPeerDataResponse* response) {
     auto wait_us = elapsed_us(enqueue_ts);
@@ -680,6 +691,87 @@ Status trigger_peer_server_fill(io::FileBlockSPtr& fb, 
int64_t fill_tablet_id,
                                      file_size, offset, size, timeout_ms));
 }
 
+// Serve one cache block of a peer request: wait for a DOWNLOADING block, fill 
an EMPTY block when
+// `do_fill` is set, then append the block to `response`. The payload goes to
+// `response_attachment` when it is non-null, otherwise into the protobuf.
+Status serve_peer_cache_block(const PFetchPeerDataRequest* request, const 
CacheBlockReqest& cb_req,
+                              io::FileBlockSPtr& fb, const io::UInt128Wrapper& 
hash,
+                              size_t file_size, bool do_fill, butil::IOBuf* 
response_attachment,
+                              PFetchPeerDataResponse* response) {
+    auto fb_state = fb->state();
+    if (fb_state == io::FileBlock::State::DOWNLOADING) {
+        if (do_fill) {
+            // Only peer fill requests should wait longer here. Plain 
peer-cache reads keep
+            // the short wait semantics so they can fail fast and let the 
client race S3.
+            [[maybe_unused]] const bool completed =
+                    wait_for_file_block_state(fb, 
config::peer_server_cache_fill_timeout_ms);
+            fb_state = fb->state();
+        } else {
+            // Wait for in-progress download to complete using the normal 
short timeout.
+            fb_state = fb->wait();
+        }
+    }
+    if (fb_state == io::FileBlock::State::EMPTY) {
+        if (!do_fill) {
+            return reject_peer_block_not_found(
+                    response, fmt::format("cache block not downloaded, {}",
+                                          
format_peer_cache_block_context(request, cb_req, fb, hash,
+                                                                          
file_size, do_fill)));
+        }
+        // Server-side fill: request.path already carries the full remote path.
+        auto fill_st = trigger_peer_server_fill(
+                fb, get_rowset_meta_tablet_id_from_request(request),
+                get_peer_cache_filename(request->path()),
+                get_rowset_meta_resource_id_from_request(request), 
request->path(),
+                request->has_file_size() ? request->file_size() : -1,
+                static_cast<int64_t>(fb->range().left), 
static_cast<int64_t>(fb->range().size()),
+                config::peer_server_cache_fill_timeout_ms);
+        if (!fill_st.ok()) {
+            g_file_cache_get_by_peer_failed_num << 1;
+            g_file_cache_get_by_peer_not_downloaded_block_num << 1;
+            if (fill_st.is<ErrorCode::TOO_MANY_TASKS>()) {
+                // Server slot exhausted: healthy but overloaded. Client must 
not rotate
+                // or evict — just fall back to S3 and retry same candidate 
next time.
+                
response->mutable_status()->add_error_msgs(std::string(fill_st.msg()));
+                
response->mutable_status()->set_status_code(TStatusCode::TOO_MANY_TASKS);
+            } else if (fill_st.is<ErrorCode::NOT_FOUND>()) {
+                // Soft miss (fill incomplete, timeout, unexpected state) — 
client rotates,
+                // not evicts.
+                
response->mutable_status()->add_error_msgs(std::string(fill_st.msg()));
+                
response->mutable_status()->set_status_code(TStatusCode::NOT_FOUND);
+            } else {
+                LOG(WARNING) << "cache block fill failed, status=" << fill_st 
<< ", "
+                             << format_peer_cache_block_context(request, 
cb_req, fb, hash,
+                                                                file_size, 
do_fill);
+                set_error_response(response, "cache block not ready");
+            }
+            return fill_st;
+        }
+        fb_state = io::FileBlock::State::DOWNLOADED;
+    }
+    if (fb_state != io::FileBlock::State::DOWNLOADED) {
+        // A concurrent download was in progress (DOWNLOADING at request time) 
but its
+        // wait() returned a non-DOWNLOADED state (e.g., timed-out while still
+        // DOWNLOADING, or some other non-EMPTY intermediate state).  The 
server is
+        // healthy; the block just isn't available yet.
+        return reject_peer_block_not_found(
+                response, fmt::format("cache block not ready after wait, {}",
+                                      format_peer_cache_block_context(request, 
cb_req, fb, hash,
+                                                                      
file_size, do_fill)));
+    }
+
+    g_file_cache_get_by_peer_blocks_num << 1;
+    doris::CacheBlockPB* out = response->add_datas();
+    // In attachment mode, metadata order must match attachment append order 
because the
+    // client consumes attachment payload sequentially using resp.datas() 
order.
+    Status read_status = read_file_block(fb, file_size, out, 
response_attachment);
+    if (!read_status.ok()) {
+        set_error_response(response, "read cache file error");
+        return read_status;
+    }
+    return Status::OK();
+}
+
 Status handle_peer_file_cache_block_request(const PFetchPeerDataRequest* 
request,
                                             PFetchPeerDataResponse* response,
                                             brpc::Controller* cntl) {
@@ -722,6 +814,7 @@ Status handle_peer_file_cache_block_request(const 
PFetchPeerDataRequest* request
     const bool use_attachment =
             cntl != nullptr && request->has_support_attachment() && 
request->support_attachment();
     response->set_data_in_attachment(use_attachment);
+    butil::IOBuf* response_attachment = use_attachment ? 
&cntl->response_attachment() : nullptr;
     if (use_attachment) {
         g_file_cache_get_by_peer_attachment_response_num << 1;
     } else {
@@ -737,8 +830,8 @@ Status handle_peer_file_cache_block_request(const 
PFetchPeerDataRequest* request
         if (offset >= file_size) {
             continue;
         }
-        // Clip tail requests before get_or_set so peer reads do not 
synthesize EMPTY blocks past
-        // EOF and then fail the whole RPC.
+        // Clip tail requests before the cache lookup so peer reads do not 
synthesize EMPTY blocks
+        // past EOF and then fail the whole RPC.
         size = std::min(size, file_size - offset);
         if (size == 0) {
             continue;
@@ -750,102 +843,41 @@ Status handle_peer_file_cache_block_request(const 
PFetchPeerDataRequest* request
                     int sleep_ms = dp->param<int>("sleep_ms", 300);
                     bthread_usleep(sleep_ms * 1000);
                 });
-        auto get_or_set_start = std::chrono::steady_clock::now();
-        auto holder = cache->get_or_set(hash, offset, size, ctx);
-        g_file_cache_get_by_peer_get_or_set_latency << 
elapsed_us(get_or_set_start);
-        ++get_or_set_calls;
-        get_or_set_blocks += holder.file_blocks.size();
-
-        for (auto& fb : holder.file_blocks) {
-            auto fb_state = fb->state();
-            if (fb_state == io::FileBlock::State::DOWNLOADING) {
-                if (do_fill) {
-                    // Only peer fill requests should wait longer here. Plain 
peer-cache reads keep
-                    // the short wait semantics so they can fail fast and let 
the client race S3.
-                    [[maybe_unused]] const bool completed = 
wait_for_file_block_state(
-                            fb, config::peer_server_cache_fill_timeout_ms);
-                    fb_state = fb->state();
-                } else {
-                    // Wait for in-progress download to complete using the 
normal short timeout.
-                    fb_state = fb->wait();
-                }
-            }
-            if (fb_state == io::FileBlock::State::EMPTY) {
-                if (!do_fill) {
-                    const std::string msg =
-                            fmt::format("cache block not downloaded, {}",
-                                        
format_peer_cache_block_context(request, cb_req, fb, hash,
-                                                                        
file_size, do_fill));
-                    g_file_cache_get_by_peer_failed_num << 1;
-                    g_file_cache_get_by_peer_not_downloaded_block_num << 1;
-                    LOG(WARNING) << msg;
-                    // Use NOT_FOUND so the client can distinguish "block not 
cached"
-                    // from an actual RPC/server error.  On NOT_FOUND the 
client rotates
-                    // the candidate to the end of its list (trying another CG 
next time)
-                    // rather than incrementing the RPC-failure eviction 
counter.
-                    response->mutable_status()->add_error_msgs(msg);
-                    
response->mutable_status()->set_status_code(TStatusCode::NOT_FOUND);
-                    return Status::NotFound<false>(msg);
-                }
-                // Server-side fill: request.path already carries the full 
remote path.
-                auto fill_st = trigger_peer_server_fill(
-                        fb, get_rowset_meta_tablet_id_from_request(request), 
cache_key_path,
-                        get_rowset_meta_resource_id_from_request(request), 
path,
-                        request->has_file_size() ? request->file_size() : -1,
-                        static_cast<int64_t>(fb->range().left),
-                        static_cast<int64_t>(fb->range().size()),
-                        config::peer_server_cache_fill_timeout_ms);
-                if (!fill_st.ok()) {
-                    g_file_cache_get_by_peer_failed_num << 1;
-                    g_file_cache_get_by_peer_not_downloaded_block_num << 1;
-                    if (fill_st.is<ErrorCode::TOO_MANY_TASKS>()) {
-                        // Server slot exhausted: healthy but overloaded. 
Client must not rotate
-                        // or evict — just fall back to S3 and retry same 
candidate next time.
-                        
response->mutable_status()->add_error_msgs(std::string(fill_st.msg()));
-                        
response->mutable_status()->set_status_code(TStatusCode::TOO_MANY_TASKS);
-                    } else if (fill_st.is<ErrorCode::NOT_FOUND>()) {
-                        // Soft miss (fill incomplete, timeout, unexpected 
state) — client rotates,
-                        // not evicts.
-                        
response->mutable_status()->add_error_msgs(std::string(fill_st.msg()));
-                        
response->mutable_status()->set_status_code(TStatusCode::NOT_FOUND);
-                    } else {
-                        LOG(WARNING) << "cache block fill failed, status=" << 
fill_st << ", "
-                                     << 
format_peer_cache_block_context(request, cb_req, fb, hash,
-                                                                        
file_size, do_fill);
-                        set_error_response(response, "cache block not ready");
-                    }
-                    return fill_st;
-                }
-                fb_state = io::FileBlock::State::DOWNLOADED;
-            }
-            if (fb_state != io::FileBlock::State::DOWNLOADED) {
-                // A concurrent download was in progress (DOWNLOADING at 
request time) but its
-                // wait() returned a non-DOWNLOADED state (e.g., timed-out 
while still
-                // DOWNLOADING, or some other non-EMPTY intermediate state).  
The server is
-                // healthy; the block just isn't available yet.  Return 
NOT_FOUND so the client
-                // rotates the candidate instead of evicting it.
-                const std::string msg =
-                        fmt::format("cache block not ready after wait, {}",
-                                    format_peer_cache_block_context(request, 
cb_req, fb, hash,
-                                                                    file_size, 
do_fill));
-                g_file_cache_get_by_peer_failed_num << 1;
-                g_file_cache_get_by_peer_not_downloaded_block_num << 1;
-                LOG(WARNING) << msg;
-                response->mutable_status()->add_error_msgs(msg);
-                
response->mutable_status()->set_status_code(TStatusCode::NOT_FOUND);
-                return Status::NotFound<false>(msg);
+        if (do_fill) {
+            // Pull-through fill needs the EMPTY cell from get_or_set: the 
fill download lands in
+            // that cell, and this handler reads it back once the download 
finishes.
+            auto get_or_set_start = std::chrono::steady_clock::now();
+            auto holder = cache->get_or_set(hash, offset, size, ctx);
+            g_file_cache_get_by_peer_get_or_set_latency << 
elapsed_us(get_or_set_start);
+            ++get_or_set_calls;
+            get_or_set_blocks += holder.file_blocks.size();
+            for (auto& fb : holder.file_blocks) {
+                RETURN_IF_ERROR(serve_peer_cache_block(request, cb_req, fb, 
hash, file_size,
+                                                       do_fill, 
response_attachment, response));
+                ++response_blocks;
             }
+            continue;
+        }
 
-            g_file_cache_get_by_peer_blocks_num << 1;
-            doris::CacheBlockPB* out = response->add_datas();
-            // In attachment mode, metadata order must match attachment append 
order because the
-            // client consumes attachment payload sequentially using 
resp.datas() order.
-            Status read_status = read_file_block(
-                    fb, file_size, out, use_attachment ? 
&cntl->response_attachment() : nullptr);
-            if (!read_status.ok()) {
-                set_error_response(response, "read cache file error");
-                return read_status;
-            }
+        // A plain peer read only serves blocks that are already downloaded. 
This lookup inserts no
+        // EMPTY cells, takes no downloader ownership, and accepts any cached 
layout, including
+        // short cells left by a pull-through fill from a client with a 
smaller cache block size.
+        // It also refreshes LRU state for the returned blocks.
+        io::FileBlocks blocks;
+        bool fully_covered = false;
+        RETURN_IF_ERROR(cache->get_downloaded_blocks_if_fully_covered(hash, 
offset, size, ctx,
+                                                                      &blocks, 
&fully_covered));
+        if (!fully_covered) {
+            return reject_peer_block_not_found(
+                    response,
+                    fmt::format(
+                            "cache block not fully downloaded, 
req_block=[offset={}, size={}], {}",
+                            cb_req.block_offset(), cb_req.block_size(),
+                            format_peer_request_context(request, hash, 
file_size)));
+        }
+        for (auto& fb : blocks) {
+            RETURN_IF_ERROR(serve_peer_cache_block(request, cb_req, fb, hash, 
file_size, do_fill,
+                                                   response_attachment, 
response));
             ++response_blocks;
         }
     }
diff --git a/be/src/io/cache/block_file_cache.cpp 
b/be/src/io/cache/block_file_cache.cpp
index e1c7fa3aad1..abd1a277dd9 100644
--- a/be/src/io/cache/block_file_cache.cpp
+++ b/be/src/io/cache/block_file_cache.cpp
@@ -55,6 +55,7 @@
 #include "io/cache/remote_scan_cache_write_limiter.h"
 #include "runtime/runtime_profile.h"
 #include "util/concurrency_stats.h"
+#include "util/defer_op.h"
 #include "util/mem_info.h"
 #include "util/stack_util.h"
 #include "util/stopwatch.hpp"
@@ -812,6 +813,14 @@ Status 
BlockFileCache::get_downloaded_blocks_if_fully_covered(const UInt128Wrapp
         return Status::OK();
     }
 
+    // Async touch defers LRU moves; queue them after the cache lock is 
released, as get_or_set()
+    // does. Declared before the lock so it runs after the lock guard is 
destroyed.
+    std::vector<FileBlockSPtr> need_update_lru_blocks;
+    Defer queue_lru_updates {[&]() {
+        for (auto& block : need_update_lru_blocks) {
+            add_need_update_lru_block(std::move(block));
+        }
+    }};
     FileBlock::Range range(offset, offset + size - 1);
     std::lock_guard cache_lock(_mutex);
     auto it = _files.find(hash);
@@ -882,9 +891,13 @@ Status 
BlockFileCache::get_downloaded_blocks_if_fully_covered(const UInt128Wrapp
     if (!*fully_covered) {
         return Status::OK();
     }
+    const bool async_touch = 
config::enable_file_cache_async_touch_on_get_or_set;
     for (const auto* cell : covered_cells) {
-        use_cell(*cell, blocks, need_to_move(cell->file_block->cache_type(), 
context.cache_type),
-                 cache_lock);
+        const bool move_iter = need_to_move(cell->file_block->cache_type(), 
context.cache_type);
+        use_cell(*cell, blocks, move_iter, cache_lock);
+        if (async_touch && move_iter) {
+            need_update_lru_blocks.push_back(cell->file_block);
+        }
     }
     return Status::OK();
 }
diff --git a/be/src/io/cache/cached_remote_file_reader.cpp 
b/be/src/io/cache/cached_remote_file_reader.cpp
index ef0c395b4c1..6a2abb5c847 100644
--- a/be/src/io/cache/cached_remote_file_reader.cpp
+++ b/be/src/io/cache/cached_remote_file_reader.cpp
@@ -104,6 +104,7 @@ bvar::PassiveStatus<int> g_active_peer_races_bvar(
 bvar::Adder<uint64_t> g_peer_race_peer_win("peer_race_peer_win");
 bvar::Adder<uint64_t> g_peer_race_s3_win("peer_race_s3_win");
 bvar::Adder<uint64_t> g_peer_race_both_fail("peer_race_both_fail");
+bvar::Adder<uint64_t> 
g_peer_race_s3_skipped_after_queue("peer_race_s3_skipped_after_queue");
 bvar::Adder<uint64_t> 
g_peer_cross_compute_group_read("peer_cross_compute_group_read");
 bvar::Adder<uint64_t> 
g_peer_same_compute_group_read("peer_same_compute_group_read");
 bvar::Adder<uint64_t> g_peer_lazy_fetch_triggered("peer_lazy_fetch_triggered");
@@ -465,6 +466,20 @@ void launch_s3_race(std::shared_ptr<RaceState> race, 
size_t empty_start, size_t
     // peer wins; passing nullptr is safe because S3FileReader::read_at_impl 
ignores it.
     auto do_s3_read = [race, empty_start, span_size, remote_reader, owner]() {
         (void)owner;
+        
TEST_SYNC_POINT("CachedRemoteFileReader::_execute_winner_race::s3_task_start");
+        {
+            // The peer may win while this task waits in PeerRaceS3ThreadPool. 
Skip the GET
+            // before allocating the span buffer or counting an S3 read.
+            std::unique_lock<bthread::Mutex> lk(race->mtx);
+            if (race->winner == 0) {
+                race->s3_done = true;
+                race->s3_status =
+                        Status::InternalError<false>("skipped: peer won before 
s3 task started");
+                race->cv.notify_all();
+                g_peer_race_s3_skipped_after_queue << 1;
+                return;
+            }
+        }
         auto s3_buf = std::make_unique<char[]>(span_size);
         size_t read_size = span_size;
         s3_read_counter << 1;
diff --git a/be/test/io/cache/cached_remote_file_reader_peer_test.cpp 
b/be/test/io/cache/cached_remote_file_reader_peer_test.cpp
index 07e83c93d96..d0a05908e52 100644
--- a/be/test/io/cache/cached_remote_file_reader_peer_test.cpp
+++ b/be/test/io/cache/cached_remote_file_reader_peer_test.cpp
@@ -28,6 +28,7 @@
 #include <condition_variable>
 #include <filesystem>
 #include <fstream>
+#include <functional>
 #include <mutex>
 #include <string>
 #include <thread>
@@ -53,6 +54,7 @@
 
 namespace doris {
 extern bvar::Adder<uint64_t> g_file_cache_get_by_peer_queue_timeout_num;
+extern bvar::Adder<uint64_t> g_file_cache_get_by_peer_get_or_set_calls;
 
 Status test_handle_peer_file_cache_block_request(const PFetchPeerDataRequest* 
request,
                                                  PFetchPeerDataResponse* 
response,
@@ -64,6 +66,9 @@ bool 
test_try_reject_if_queue_timed_out(std::chrono::steady_clock::time_point en
 namespace doris::io {
 
 extern bvar::Adder<uint64_t> peer_cache_reader_read_counter;
+extern bvar::Adder<uint64_t> s3_read_counter;
+extern bvar::Adder<uint64_t> g_peer_race_peer_win;
+extern bvar::Adder<uint64_t> g_peer_race_s3_skipped_after_queue;
 
 namespace {
 
@@ -250,6 +255,9 @@ public:
                             : "";
         }
 
+        if (before_response) {
+            before_response();
+        }
         if (_options.fail_status) {
             
response->mutable_status()->set_status_code(TStatusCode::INTERNAL_ERROR);
             return;
@@ -345,6 +353,8 @@ public:
     std::atomic<int> request_block_count {0};
     std::atomic<bool> support_attachment {false};
     std::atomic<bool> request_fill {false};
+    // Runs on the RPC thread before the response is built; tests use it to 
order the peer reply.
+    std::function<void()> before_response;
     std::string get_last_fill_resource_id() const {
         std::lock_guard lock(_request_mu);
         return last_fill_resource_id;
@@ -994,6 +1004,166 @@ TEST_F(CachedRemoteFileReaderPeerTest, 
peer_file_cache_handler_allows_cached_blo
             create_peer_cache_block_request(file_path, 0, kPeerTestBlockSize, 
content.size());
     PFetchPeerDataResponse response;
     brpc::Controller cntl;
+    const uint64_t get_or_set_before = 
doris::g_file_cache_get_by_peer_get_or_set_calls.get_value();
+
+    auto st = doris::test_handle_peer_file_cache_block_request(&request, 
&response, &cntl);
+
+    ASSERT_TRUE(st.ok()) << st;
+    EXPECT_EQ(response.status().status_code(), TStatusCode::OK);
+    ASSERT_EQ(response.datas_size(), 1);
+    EXPECT_EQ(response.datas(0).block_offset(), 0);
+    EXPECT_EQ(response.datas(0).data(), content.substr(0, kPeerTestBlockSize));
+    // A plain peer read is served by the read-only lookup and never reaches 
get_or_set().
+    EXPECT_EQ(doris::g_file_cache_get_by_peer_get_or_set_calls.get_value() - 
get_or_set_before, 0);
+}
+
+TEST_F(CachedRemoteFileReaderPeerTest, 
peer_file_cache_handler_serves_fragmented_cached_layout) {
+    const std::string content = "abcdefghijklmnop";
+    const fs::path file_path =
+            
create_peer_test_file("cached_remote_reader_peer_fragmented_layout.dat", 
content);
+    Defer cleanup_file {[&]() {
+        std::error_code ec;
+        fs::remove(file_path, ec);
+    }};
+
+    const fs::path cache_path = caches_dir / 
"cached_remote_reader_peer_fragmented_layout_cache";
+    Defer cleanup_cache {[&]() {
+        std::error_code ec;
+        fs::remove_all(cache_path, ec);
+    }};
+
+    clear_cached_remote_reader_factory();
+    BlockFileCache* cache = create_peer_test_cache(cache_path, 
kPeerTestBlockSize);
+    // A pull-through fill from a client with a smaller cache block size 
leaves short cells inside
+    // one of this cache's blocks. A block-sized plain peer read must still be 
served.
+    populate_cache_block_with_content(cache, file_path, 0, kPeerTestBlockSize 
/ 2, content);
+    populate_cache_block_with_content(cache, file_path, kPeerTestBlockSize / 2,
+                                      kPeerTestBlockSize / 2, content);
+
+    auto request =
+            create_peer_cache_block_request(file_path, 0, kPeerTestBlockSize, 
content.size());
+    PFetchPeerDataResponse response;
+    brpc::Controller cntl;
+
+    auto st = doris::test_handle_peer_file_cache_block_request(&request, 
&response, &cntl);
+
+    ASSERT_TRUE(st.ok()) << st;
+    EXPECT_EQ(response.status().status_code(), TStatusCode::OK);
+    ASSERT_EQ(response.datas_size(), 2);
+    EXPECT_EQ(response.datas(0).block_offset(), 0);
+    EXPECT_EQ(response.datas(0).data(), content.substr(0, kPeerTestBlockSize / 
2));
+    EXPECT_EQ(response.datas(1).block_offset(), kPeerTestBlockSize / 2);
+    EXPECT_EQ(response.datas(1).data(),
+              content.substr(kPeerTestBlockSize / 2, kPeerTestBlockSize / 2));
+}
+
+TEST_F(CachedRemoteFileReaderPeerTest, 
peer_file_cache_handler_queues_async_lru_touch) {
+    const bool old_async_touch = 
config::enable_file_cache_async_touch_on_get_or_set;
+    const int64_t old_update_interval_ms =
+            config::file_cache_background_block_lru_update_interval_ms;
+    Defer restore_config {[&]() {
+        config::enable_file_cache_async_touch_on_get_or_set = old_async_touch;
+        config::file_cache_background_block_lru_update_interval_ms = 
old_update_interval_ms;
+    }};
+    config::enable_file_cache_async_touch_on_get_or_set = true;
+    // Keep the background LRU worker from draining the queue before it is 
inspected.
+    config::file_cache_background_block_lru_update_interval_ms = 60 * 60 * 
1000;
+
+    const std::string content = "abcdefghijklmnop";
+    const fs::path file_path =
+            
create_peer_test_file("cached_remote_reader_peer_async_lru_touch.dat", content);
+    Defer cleanup_file {[&]() {
+        std::error_code ec;
+        fs::remove(file_path, ec);
+    }};
+
+    const fs::path cache_path = caches_dir / 
"cached_remote_reader_peer_async_lru_touch_cache";
+    Defer cleanup_cache {[&]() {
+        std::error_code ec;
+        fs::remove_all(cache_path, ec);
+    }};
+
+    clear_cached_remote_reader_factory();
+    BlockFileCache* cache = create_peer_test_cache(cache_path, 
kPeerTestBlockSize);
+    populate_cache_block_with_content(cache, file_path, 0, kPeerTestBlockSize, 
content);
+
+    auto request =
+            create_peer_cache_block_request(file_path, 0, kPeerTestBlockSize, 
content.size());
+    PFetchPeerDataResponse response;
+    brpc::Controller cntl;
+    const size_t pending_before = cache->need_update_lru_blocks_size_unsafe();
+
+    auto st = doris::test_handle_peer_file_cache_block_request(&request, 
&response, &cntl);
+
+    ASSERT_TRUE(st.ok()) << st;
+    ASSERT_EQ(response.datas_size(), 1);
+    // Async touch defers the LRU move, so a served hit must be queued like a 
get_or_set() hit.
+    EXPECT_EQ(cache->need_update_lru_blocks_size_unsafe(), pending_before + 1);
+}
+
+TEST_F(CachedRemoteFileReaderPeerTest, 
peer_file_cache_handler_miss_returns_not_found) {
+    const std::string content = "abcdefghijklmnop";
+    const fs::path file_path = 
create_peer_test_file("cached_remote_reader_peer_miss.dat", content);
+    Defer cleanup_file {[&]() {
+        std::error_code ec;
+        fs::remove(file_path, ec);
+    }};
+
+    const fs::path cache_path = caches_dir / 
"cached_remote_reader_peer_miss_cache";
+    Defer cleanup_cache {[&]() {
+        std::error_code ec;
+        fs::remove_all(cache_path, ec);
+    }};
+
+    clear_cached_remote_reader_factory();
+    BlockFileCache* cache = create_peer_test_cache(cache_path, 
kPeerTestBlockSize);
+    const auto hash = BlockFileCache::hash(file_path.filename().native());
+
+    auto request =
+            create_peer_cache_block_request(file_path, 0, kPeerTestBlockSize, 
content.size());
+    PFetchPeerDataResponse response;
+    brpc::Controller cntl;
+    const uint64_t get_or_set_before = 
doris::g_file_cache_get_by_peer_get_or_set_calls.get_value();
+
+    auto st = doris::test_handle_peer_file_cache_block_request(&request, 
&response, &cntl);
+
+    ASSERT_TRUE(st.is<ErrorCode::NOT_FOUND>()) << st;
+    EXPECT_EQ(response.status().status_code(), TStatusCode::NOT_FOUND);
+    ASSERT_GE(response.status().error_msgs_size(), 1);
+    EXPECT_NE(response.status().error_msgs(0).find("cache block not fully 
downloaded"),
+              std::string::npos);
+    EXPECT_EQ(response.datas_size(), 0);
+    EXPECT_EQ(doris::g_file_cache_get_by_peer_get_or_set_calls.get_value() - 
get_or_set_before, 0);
+    EXPECT_FALSE(cache->_files.contains(hash));
+}
+
+TEST_F(CachedRemoteFileReaderPeerTest, 
peer_file_cache_handler_serves_offset_inside_cached_block) {
+    const std::string content = "abcdefghijklmnop";
+    const fs::path file_path =
+            
create_peer_test_file("cached_remote_reader_peer_offset_inside_block.dat", 
content);
+    Defer cleanup_file {[&]() {
+        std::error_code ec;
+        fs::remove(file_path, ec);
+    }};
+
+    const fs::path cache_path = caches_dir / 
"cached_remote_reader_peer_offset_inside_block_cache";
+    Defer cleanup_cache {[&]() {
+        std::error_code ec;
+        fs::remove_all(cache_path, ec);
+    }};
+
+    clear_cached_remote_reader_factory();
+    BlockFileCache* cache = create_peer_test_cache(cache_path, 
kPeerTestBlockSize);
+    populate_cache_block_with_content(cache, file_path, 0, kPeerTestBlockSize, 
content);
+
+    // A client with a smaller cache block size can request a range that 
starts inside one of this
+    // cache's blocks. The server returns the covering block whole. This 
checks the server only:
+    // the current client rejects a response block outside its requested 
ranges, as with
+    // get_or_set().
+    auto request = create_peer_cache_block_request(file_path, 
kPeerTestBlockSize / 2,
+                                                   kPeerTestBlockSize / 2, 
content.size());
+    PFetchPeerDataResponse response;
+    brpc::Controller cntl;
 
     auto st = doris::test_handle_peer_file_cache_block_request(&request, 
&response, &cntl);
 
@@ -2260,6 +2430,107 @@ protected:
                 .get_peer_candidates(kCrossTabletId);
     }
 
+    // Let the peer win while the S3 task is held at `s3_sync_point`, wait for 
the S3 task to
+    // finish, and report how many S3 reads and queue skips it recorded. With
+    // `peer_waits_for_s3_hold`, the peer replies only after S3 reaches the 
sync point, so the
+    // peer cannot win before the S3 task starts.
+    void run_peer_win_with_s3_held_at(const std::string& name, const 
std::string& s3_sync_point,
+                                      bool peer_waits_for_s3_hold, uint64_t* 
s3_reads,
+                                      uint64_t* s3_skips) {
+        const std::string content = "abcdefghijklmnop";
+        const fs::path file_path = create_cross_cg_test_file(name + "_0.dat", 
content);
+        Defer cleanup_file {[&]() {
+            std::error_code ec;
+            fs::remove(file_path, ec);
+        }};
+
+        const fs::path cache_path = caches_dir / (name + "_cache");
+        Defer cleanup_cache {[&]() {
+            std::error_code ec;
+            fs::remove_all(cache_path, ec);
+        }};
+
+        clear_cached_remote_reader_factory();
+        create_peer_test_cache(cache_path, kPeerTestBlockSize);
+
+        // Submit S3 immediately so only the S3 task itself can observe the 
peer win.
+        const int32_t old_hedge_delay_ms = config::peer_race_hedge_delay_ms;
+        Defer restore_hedge_delay {
+                [&]() { config::peer_race_hedge_delay_ms = old_hedge_delay_ms; 
}};
+        config::peer_race_hedge_delay_ms = 0;
+
+        std::mutex s3_hold_mutex;
+        std::condition_variable s3_hold_cv;
+        bool s3_held = false;
+        MockPeerCacheService service(content);
+        if (peer_waits_for_s3_hold) {
+            service.before_response = [&]() {
+                std::unique_lock lock(s3_hold_mutex);
+                s3_hold_cv.wait_for(lock, kPeerRpcWaitTimeout, [&] { return 
s3_held; });
+            };
+        }
+        brpc::Server server;
+        auto addr = start_peer_test_server(&server, &service);
+        Defer stop_server {[&]() { stop_peer_test_server(&server); }};
+        register_cross_cg_candidate("127.0.0.1", addr.port);
+
+        const uint64_t peer_wins_before = g_peer_race_peer_win.get_value();
+        const uint64_t s3_reads_before = s3_read_counter.get_value();
+        const uint64_t s3_skips_before = 
g_peer_race_s3_skipped_after_queue.get_value();
+
+        auto* sp = SyncPoint::get_instance();
+        sp->clear_all_call_backs();
+        sp->enable_processing();
+        SyncPoint::CallbackGuard s3_hold_guard;
+        sp->set_call_back(
+                s3_sync_point,
+                [&](auto&&) {
+                    {
+                        std::lock_guard lock(s3_hold_mutex);
+                        s3_held = true;
+                    }
+                    s3_hold_cv.notify_all();
+                    // collect_race_result counts the peer win only after 
winner == 0 is set.
+                    const auto deadline = std::chrono::steady_clock::now() + 
kPeerRpcWaitTimeout;
+                    while (g_peer_race_peer_win.get_value() == 
peer_wins_before &&
+                           std::chrono::steady_clock::now() < deadline) {
+                        
std::this_thread::sleep_for(std::chrono::milliseconds(1));
+                    }
+                },
+                &s3_hold_guard);
+        Defer disable_sp {[&]() {
+            sp->disable_processing();
+            sp->clear_all_call_backs();
+        }};
+
+        FileReaderSPtr local_reader;
+        ASSERT_TRUE(global_local_filesystem()->open_file(file_path.string(), 
&local_reader).ok());
+
+        FileReaderOptions opts;
+        opts.cache_type = FileCachePolicy::FILE_BLOCK_CACHE;
+        opts.is_doris_table = true;
+        opts.mtime = 1;
+        opts.tablet_id = kCrossTabletId;
+        auto reader = std::make_shared<CachedRemoteFileReader>(local_reader, 
opts);
+
+        std::string buffer(10, '#');
+        size_t bytes_read = 0;
+        IOContext io_ctx;
+        FileCacheStatistics cache_stats;
+        io_ctx.file_cache_stats = &cache_stats;
+
+        ASSERT_TRUE(
+                reader->read_at(1, Slice(buffer.data(), buffer.size()), 
&bytes_read, &io_ctx).ok());
+        EXPECT_EQ(buffer, content.substr(1, 10));
+        EXPECT_EQ(bytes_read, 10);
+        EXPECT_EQ(cache_stats.num_peer_race_peer_win, 1);
+
+        // The losing S3 task outlives read_at(); wait for it before reading 
its counters.
+        ExecEnv::GetInstance()->peer_race_s3_thread_pool()->wait();
+        *s3_reads = s3_read_counter.get_value() - s3_reads_before;
+        *s3_skips = g_peer_race_s3_skipped_after_queue.get_value() - 
s3_skips_before;
+    }
+
 private:
     bool _old_enable_peer_s3_race = false;
     int32_t _old_max_concurrent_peer_races = 64;
@@ -3418,6 +3689,38 @@ TEST_F(CrossCGWinnerRaceTest, 
race_peer_stops_when_s3_wins) {
     EXPECT_EQ(miss_service2.rpc_count.load(), 0) << "candidate 2 should be 
skipped (S3 won)";
 }
 
+// ----------------------------------------------------------------
+// Test: an S3 task that starts after the peer has won skips its GET.
+//
+// Holding S3 at s3_task_start models a task that waited in 
PeerRaceS3ThreadPool while the peer
+// won. The task must return before counting or issuing the S3 read.
+// ----------------------------------------------------------------
+TEST_F(CrossCGWinnerRaceTest, s3_task_skips_read_when_peer_won_while_queued) {
+    uint64_t s3_reads = 0;
+    uint64_t s3_skips = 0;
+    run_peer_win_with_s3_held_at("s3_skip_queued",
+                                 
"CachedRemoteFileReader::_execute_winner_race::s3_task_start",
+                                 /*peer_waits_for_s3_hold=*/false, &s3_reads, 
&s3_skips);
+    EXPECT_EQ(s3_reads, 0);
+    EXPECT_EQ(s3_skips, 1);
+}
+
+// ----------------------------------------------------------------
+// Test: an S3 task that started before the peer won still reads.
+//
+// s3_before_read is after the queue check. The peer replies only once S3 is 
held there, so the
+// task has passed the check before the peer wins and keeps the 
read-then-discard behavior.
+// ----------------------------------------------------------------
+TEST_F(CrossCGWinnerRaceTest, s3_task_reads_when_started_before_peer_won) {
+    uint64_t s3_reads = 0;
+    uint64_t s3_skips = 0;
+    run_peer_win_with_s3_held_at("s3_read_started",
+                                 
"CachedRemoteFileReader::_execute_winner_race::s3_before_read",
+                                 /*peer_waits_for_s3_hold=*/true, &s3_reads, 
&s3_skips);
+    EXPECT_EQ(s3_reads, 1);
+    EXPECT_EQ(s3_skips, 0);
+}
+
 // ----------------------------------------------------------------
 // Test: cross-CG peer wins race and timer metrics are non-zero.
 //


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

Reply via email to