github-actions[bot] commented on code in PR #67971:
URL: https://github.com/apache/doris/pull/67971#discussion_r4026001367


##########
be/src/io/cache/block_file_cache_ttl_mgr.cpp:
##########
@@ -168,6 +181,83 @@ FileBlocks 
BlockFileCacheTtlMgr::get_file_blocks_from_tablet_id(int64_t tablet_i
     return result;
 }
 
+void BlockFileCacheTtlMgr::reconcile_tablet_blocks(int64_t tablet_id) {
+    // Serialize all conversions of this tablet. Whichever caller takes this 
lock last re-reads
+    // the state below and has the final say, so the update and expiration 
threads cannot fight
+    // over the same blocks and strand them in the loser's cache type.
+    std::lock_guard<std::mutex> 
transition_lock(transition_lock_for(tablet_id));
+
+    bool want_ttl = false;
+    bool blocks_promoted = false;
+    {
+        // Deliberately re-read the map rather than trust what the caller saw: 
the expiration
+        // thread picks its candidates up to a full gc interval before getting 
here.
+        std::lock_guard<std::mutex> lock(_ttl_info_mutex);
+        auto it = _ttl_info_map.find(tablet_id);
+        if (it != _ttl_info_map.end()) {
+            if (it->second.ttl == 0 && !it->second.blocks_promoted) {

Review Comment:
   [P1] `blocks_promoted=false` does not prove that there are no TTL blocks to 
demote. After an expired tablet is successfully demoted, a cache fill that 
captured the old TTL context can still publish a late TTL block (the comment 
below explicitly acknowledges this). If the property is then cleared before the 
next expiry sweep, this branch erases the entry and returns without scanning, 
leaving that block in the TTL queue until the 20-round fallback—about an hour 
at the defaults. Please require one successful NORMAL reconciliation on the 
TTL-to-zero transition before erasing, coordinated with late block publication.



##########
be/src/io/cache/block_file_cache_ttl_mgr.cpp:
##########
@@ -220,47 +312,36 @@ void 
BlockFileCacheTtlMgr::run_backgroud_update_ttl_info_map() {
                     }
                 }
 
-                // Update TTL info map
-                bool need_convert_from_ttl = false;
+                // Record the TTL this tablet currently has, then let 
reconcile_tablet_blocks()
+                // decide whether that moves its blocks between the TTL and 
normal queues.
+                bool tracked = false;
                 {
                     std::lock_guard<std::mutex> lock(_ttl_info_mutex);
+                    auto it = _ttl_info_map.find(tablet_id);
                     if (ttl > 0) {
-                        auto old_info_it = _ttl_info_map.find(tablet_id);
-                        bool was_zero_ttl = (old_info_it == 
_ttl_info_map.end() ||
-                                             old_info_it->second.ttl == 0);
-                        _ttl_info_map[tablet_id] = TtlInfo {ttl, tablet_ctime};
-
-                        // If TTL changed from 0 to non-zero, convert blocks 
to TTL type
-                        if (was_zero_ttl) {
-                            FileBlocks blocks = 
get_file_blocks_from_tablet_id(tablet_id);
-                            for (auto& block : blocks) {
-                                if (block->cache_type() != FileCacheType::TTL) 
{
-                                    auto change_status =
-                                            
block->change_cache_type(FileCacheType::TTL);
-                                    if (!change_status.ok()) {
-                                        LOG(WARNING) << "Failed to convert 
block to TTL cache_type";
-                                    }
-                                }
-                            }
+                        if (it == _ttl_info_map.end()) {
+                            _ttl_info_map.emplace(tablet_id, TtlInfo {ttl, 
tablet_ctime});
+                            update_ttl_info_map_size_metrics();
+                        } else {
+                            // Keep blocks_promoted: it records what we did to 
the blocks, not
+                            // what the tablet meta says.
+                            it->second.ttl = ttl;
+                            it->second.tablet_ctime = tablet_ctime;
                         }
-                    } else {
-                        // Periodically reconcile blocks restored from 
persisted TTL metadata,
-                        // because _ttl_info_map is rebuilt only in memory 
after restart.
-                        need_convert_from_ttl =
-                                _ttl_info_map.erase(tablet_id) > 0 || 
need_full_reconcile;
+                        tracked = true;
+                    } else if (it != _ttl_info_map.end()) {
+                        // Hold on to the entry until the blocks are actually 
demoted; it
+                        // drops itself once the tablet has settled back to 
NORMAL.
+                        it->second.ttl = 0;
+                        tracked = true;
                     }
                 }
 
-                if (need_convert_from_ttl) {
-                    FileBlocks blocks = 
get_file_blocks_from_tablet_id(tablet_id);
-                    for (auto& block : blocks) {
-                        if (block->cache_type() == FileCacheType::TTL) {
-                            auto st = 
block->change_cache_type(FileCacheType::NORMAL);
-                            if (!st.ok()) {
-                                LOG(WARNING) << "Failed to convert block back 
to NORMAL cache_type";
-                            }
-                        }
-                    }
+                // An untracked tablet reconciles to NORMAL, which is how TTL 
blocks restored
+                // from persisted metadata are cleaned up after a restart. 
Gated on the periodic
+                // round so that ordinary non-TTL tablets are not walked every 
time.
+                if (tracked || need_full_reconcile) {

Review Comment:
   [P1] Seed the reconcile set from restored metadata. `need_full_reconcile` 
still iterates only `tablet_ids_to_process`, copied from `_tablet_id_set`, but 
the async RocksDB restore paths add restored cells directly with `add_cell()` 
and never call `register_tablet_id()`; a fully restored cache hit also only 
calls `use_cell()`. Thus after a real restart the set can remain empty and the 
expired persisted TTL block this branch is meant to demote is never visited. 
The new test masks this by manually registering the ID instead of recreating 
the cache. Please register/seed restored tablet IDs in the production lifecycle 
and cover that lifecycle without the manual call.



##########
be/test/io/cache/block_file_cache_ttl_mgr_test.cpp:
##########
@@ -462,4 +478,280 @@ TEST_F(BlockFileCacheTtlMgrTest, 
TabletTtlRemovedMovesBlocksBackToNormal) {
                                    std::chrono::seconds(5)));
 }
 
+TEST_F(BlockFileCacheTtlMgrTest, ExpiredTtlExtendedMovesBlocksBackToTtl) {
+    constexpr int64_t kTabletId = 6006;
+    auto tablet = std::make_shared<FakeTablet>(UnixSeconds(), 120);
+    fake_engine()->add_tablet(kTabletId, tablet);
+
+    UInt128Wrapper hash;
+    auto block = create_block(kTabletId, "ttl-extend-after-expire", 0, 1024, 
&hash);
+    persist_block_meta(kTabletId, hash, block->range().left, 
block->range().size());
+
+    _ttl_mgr = std::make_unique<BlockFileCacheTtlMgr>(_cache.get(), 
_meta_store.get());
+    _ttl_mgr->register_tablet_id(kTabletId);
+
+    ASSERT_TRUE(wait_for_condition([&]() { return block->cache_type() == 
FileCacheType::TTL; },

Review Comment:
   [P1] Use a synchronized cache-type observation in these waits. 
`FileBlock::cache_type()` is a plain read of `_key.meta.type`, while the TTL 
manager writes that field under the cache and block mutexes. Polling it 
concurrently here is a C++ data race, and the same unlocked predicate appears 
throughout the new transition tests. Please observe through a lock-safe path 
such as `BlockFileCache::dump_single_cache_type()`, or add a helper that takes 
the required lock.



##########
be/src/io/cache/block_file_cache_ttl_mgr.cpp:
##########
@@ -279,29 +360,26 @@ void 
BlockFileCacheTtlMgr::run_backgroud_expiration_check() {
 
     while (!_stop_background.load(std::memory_order_acquire)) {
         try {
-            std::map<int64_t, TtlInfo> ttl_info_copy;
-
-            // Copy TTL info for processing
+            // Collect tablets whose TTL has run out.
+            std::vector<int64_t> expired_tablet_ids;
             {
                 std::lock_guard<std::mutex> lock(_ttl_info_mutex);
-                ttl_info_copy = _ttl_info_map;
+                uint64_t current_time = UnixSeconds();
+                for (const auto& [tablet_id, ttl_info] : _ttl_info_map) {
+                    if (ttl_info.ttl > 0 && 
!ttl_info.is_ttl_active(current_time)) {

Review Comment:
   [P2] Avoid giving unchanged expired tablets to both sweepers. An expired 
entry keeps a nonzero TTL, so the updater treats it as `tracked` and performs 
the level-triggered scan every update round; this expiration loop then selects 
the same entry and performs the same scan again. With both defaults at 180 
seconds, every expired tablet is prefix-scanned twice every three minutes 
indefinitely. Each prefix row also calls `get_blocks_by_key()`, which copies 
every offset for that hash under the global cache mutex, making multi-block 
files quadratic per scan. Please assign expiry sweeping to one owner and 
coalesce/index the lookup.



##########
be/src/io/cache/block_file_cache_ttl_mgr.cpp:
##########
@@ -168,6 +181,83 @@ FileBlocks 
BlockFileCacheTtlMgr::get_file_blocks_from_tablet_id(int64_t tablet_i
     return result;
 }
 
+void BlockFileCacheTtlMgr::reconcile_tablet_blocks(int64_t tablet_id) {
+    // Serialize all conversions of this tablet. Whichever caller takes this 
lock last re-reads
+    // the state below and has the final say, so the update and expiration 
threads cannot fight
+    // over the same blocks and strand them in the loser's cache type.
+    std::lock_guard<std::mutex> 
transition_lock(transition_lock_for(tablet_id));
+
+    bool want_ttl = false;
+    bool blocks_promoted = false;
+    {
+        // Deliberately re-read the map rather than trust what the caller saw: 
the expiration
+        // thread picks its candidates up to a full gc interval before getting 
here.
+        std::lock_guard<std::mutex> lock(_ttl_info_mutex);
+        auto it = _ttl_info_map.find(tablet_id);
+        if (it != _ttl_info_map.end()) {
+            if (it->second.ttl == 0 && !it->second.blocks_promoted) {
+                // No TTL, and none of its blocks were put in the TTL queue by 
us: nothing left
+                // to track. Dropped here rather than after a conversion, so 
that a tablet which
+                // settles without needing one still stops being walked on 
every round.
+                _ttl_info_map.erase(it);
+                update_ttl_info_map_size_metrics();
+                return;
+            }
+            want_ttl = it->second.is_ttl_active(UnixSeconds());
+            blocks_promoted = it->second.blocks_promoted;
+        }
+    }
+
+    // The two directions are not symmetric.
+    //
+    // Promotion is edge triggered: once the blocks are in the TTL queue 
nothing takes them out
+    // behind our back, so rescanning a tablet whose TTL is still running is 
pure waste. This is
+    // also what makes a TTL rewritten to another still-valid value free, 
which matters where
+    // the property is rewritten on a schedule.
+    //
+    // Demotion is level triggered: blocks can still land in the TTL queue 
after a tablet was
+    // demoted, and what is recorded here is per tablet, so it cannot tell 
whether any have.
+    // Rescanning is the only way to collect them.
+    if (want_ttl && blocks_promoted) {
+        return;
+    }
+
+    // Scan and convert outside _ttl_info_mutex: this walks the meta store and 
takes the cache
+    // lock once per block, which is far too long to hold a mutex the other 
thread needs.
+    const auto target_type = want_ttl ? FileCacheType::TTL : 
FileCacheType::NORMAL;
+    FileBlocks blocks = get_file_blocks_from_tablet_id(tablet_id);
+    size_t converted = 0;
+    bool all_converted = true;
+    for (auto& block : blocks) {
+        if (block->cache_type() == target_type) {
+            continue;
+        }
+        auto st = block->change_cache_type(target_type);
+        if (st.ok()) {
+            ++converted;
+        } else {
+            all_converted = false;
+            LOG(WARNING) << "Failed to convert block to " << 
cache_type_to_string(target_type)
+                         << " cache_type, tablet_id=" << tablet_id << ", err=" 
<< st;
+        }
+    }
+    if (converted > 0) {
+        LOG(INFO) << "converted cached blocks to " << 
cache_type_to_string(target_type)
+                  << ", tablet_id=" << tablet_id << ", block_num=" << converted
+                  << ", scanned=" << blocks.size();
+    }
+
+    {
+        std::lock_guard<std::mutex> lock(_ttl_info_mutex);
+        auto it = _ttl_info_map.find(tablet_id);
+        if (it != _ttl_info_map.end() && all_converted) {

Review Comment:
   [P1] Do not latch promotion from this incomplete scan. A NORMAL cell is 
registered before it finishes downloading, but `range_get()` cannot see its 
pending metadata PUT and `get_blocks_by_key()` filters out non-DOWNLOADED 
cells. The updater can therefore scan zero blocks, leave `all_converted=true`, 
and set `blocks_promoted=true`; the block then finalizes with its original 
NORMAL key, and every later active-TTL round returns at line 221. Please make 
the state generation/dirty-aware (or cover in-memory and pending blocks) so 
creation/finalization invalidates promotion completion, and add a deterministic 
in-flight-block test.



##########
be/test/io/cache/block_file_cache_ttl_mgr_test.cpp:
##########
@@ -462,4 +478,280 @@ TEST_F(BlockFileCacheTtlMgrTest, 
TabletTtlRemovedMovesBlocksBackToNormal) {
                                    std::chrono::seconds(5)));
 }
 
+TEST_F(BlockFileCacheTtlMgrTest, ExpiredTtlExtendedMovesBlocksBackToTtl) {
+    constexpr int64_t kTabletId = 6006;
+    auto tablet = std::make_shared<FakeTablet>(UnixSeconds(), 120);
+    fake_engine()->add_tablet(kTabletId, tablet);
+
+    UInt128Wrapper hash;
+    auto block = create_block(kTabletId, "ttl-extend-after-expire", 0, 1024, 
&hash);
+    persist_block_meta(kTabletId, hash, block->range().left, 
block->range().size());
+
+    _ttl_mgr = std::make_unique<BlockFileCacheTtlMgr>(_cache.get(), 
_meta_store.get());
+    _ttl_mgr->register_tablet_id(kTabletId);
+
+    ASSERT_TRUE(wait_for_condition([&]() { return block->cache_type() == 
FileCacheType::TTL; },
+                                   std::chrono::seconds(5)));
+
+    // Let the TTL expire. The block goes back to NORMAL while the manager 
keeps a non-zero TTL
+    // recorded for the tablet, which is the state that used to wedge the 
promotion path.
+    tablet->set_creation_time(UnixSeconds() - 120);
+    tablet->set_ttl_seconds(1);
+    ASSERT_TRUE(wait_for_condition([&]() { return block->cache_type() == 
FileCacheType::NORMAL; },
+                                   std::chrono::seconds(5)));
+
+    // Extending an already expired TTL to one that has not expired has to 
bring the block back.
+    tablet->set_ttl_seconds(30758400);
+    ASSERT_TRUE(wait_for_condition([&]() { return block->cache_type() == 
FileCacheType::TTL; },
+                                   std::chrono::seconds(5)));
+}
+
+TEST_F(BlockFileCacheTtlMgrTest, 
ExtendedTtlThatIsStillExpiredKeepsBlocksNormal) {
+    constexpr int64_t kTabletId = 7007;
+    const int64_t creation_time = UnixSeconds() - 7200;
+    auto tablet = std::make_shared<FakeTablet>(creation_time, 60);
+    fake_engine()->add_tablet(kTabletId, tablet);
+
+    UInt128Wrapper hash;
+    auto block = create_block(kTabletId, "ttl-extend-still-expired", 0, 1024, 
&hash);
+    persist_block_meta(kTabletId, hash, block->range().left, 
block->range().size());
+
+    _ttl_mgr = std::make_unique<BlockFileCacheTtlMgr>(_cache.get(), 
_meta_store.get());
+    _ttl_mgr->register_tablet_id(kTabletId);
+
+    int64_t call_count = fake_engine()->get_tablet_meta_call_count();
+    ASSERT_TRUE(wait_for_condition(
+            [&]() { return fake_engine()->get_tablet_meta_call_count() >= 
call_count + 2; },
+            std::chrono::seconds(5)));
+    ASSERT_EQ(FileCacheType::NORMAL, block->cache_type());
+
+    // A longer TTL that is still in the past must not promote anything.
+    tablet->set_ttl_seconds(120);
+    call_count = fake_engine()->get_tablet_meta_call_count();
+    ASSERT_TRUE(wait_for_condition(
+            [&]() { return fake_engine()->get_tablet_meta_call_count() >= 
call_count + 3; },
+            std::chrono::seconds(5)));
+    EXPECT_EQ(FileCacheType::NORMAL, block->cache_type());
+}
+
+TEST_F(BlockFileCacheTtlMgrTest, 
RewritingTtlToAnotherValidValueDoesNotRescanBlocks) {
+    constexpr int64_t kTabletId = 8008;
+    auto tablet = std::make_shared<FakeTablet>(UnixSeconds(), 3600);
+    fake_engine()->add_tablet(kTabletId, tablet);
+
+    UInt128Wrapper hash;
+    auto block = create_block(kTabletId, "ttl-rewrite-valid", 0, 1024, &hash);
+    persist_block_meta(kTabletId, hash, block->range().left, 
block->range().size());
+
+    _ttl_mgr = std::make_unique<BlockFileCacheTtlMgr>(_cache.get(), 
_meta_store.get());
+    _ttl_mgr->register_tablet_id(kTabletId);
+
+    ASSERT_TRUE(wait_for_condition([&]() { return block->cache_type() == 
FileCacheType::TTL; },
+                                   std::chrono::seconds(5)));
+
+    // The promotion is recorded only after every block has been converted, so 
the manager has
+    // not necessarily finished with the tablet at the moment the type flips. 
Let a couple of
+    // rounds pass before counting, or the tail of the promotion is charged to 
the rewrites.
+    int64_t settled_after = fake_engine()->get_tablet_meta_call_count();
+    ASSERT_TRUE(wait_for_condition(
+            [&]() { return fake_engine()->get_tablet_meta_call_count() >= 
settled_after + 2; },
+            std::chrono::seconds(5)));
+
+    // Held by value in the callback: the background threads outlive this 
stack frame, and
+    // neither the guard nor disable_processing() synchronizes with a callback 
in flight.
+    auto block_scan_count = std::make_shared<std::atomic<int64_t>>(0);
+    auto* sync_point = SyncPoint::get_instance();
+    sync_point->clear_all_call_backs();
+    sync_point->clear_trace();
+    SyncPoint::CallbackGuard guard;
+    sync_point->set_call_back(
+            "BlockFileCacheTtlMgr::get_file_blocks_from_tablet_id",
+            [block_scan_count](std::vector<std::any>&& args) {
+                if (doris::try_any_cast<int64_t>(args[0]) == kTabletId) {
+                    block_scan_count->fetch_add(1, std::memory_order_relaxed);
+                }
+            },
+            &guard);
+    sync_point->enable_processing();
+
+    // Automated jobs rewrite this property regularly. As long as the tablet 
stays in the same
+    // state, none of those rewrites may trigger another walk of the meta 
store.
+    for (int64_t ttl : {7200, 1800, 5400}) {
+        tablet->set_ttl_seconds(ttl);
+        int64_t call_count = fake_engine()->get_tablet_meta_call_count();
+        ASSERT_TRUE(wait_for_condition(
+                [&]() { return fake_engine()->get_tablet_meta_call_count() >= 
call_count + 2; },
+                std::chrono::seconds(5)));
+    }
+
+    // Join the background threads before the callback and its captures go 
away.
+    _ttl_mgr.reset();
+    sync_point->disable_processing();
+    sync_point->clear_trace();
+
+    EXPECT_EQ(0, block_scan_count->load(std::memory_order_relaxed));
+    EXPECT_EQ(FileCacheType::TTL, block->cache_type());
+}
+
+TEST_F(BlockFileCacheTtlMgrTest, TtlExtensionWinsOverConcurrentExpirationScan) 
{
+    constexpr int64_t kTabletId = 9009;
+    auto tablet = std::make_shared<FakeTablet>(UnixSeconds(), 3600);
+    fake_engine()->add_tablet(kTabletId, tablet);
+
+    UInt128Wrapper hash;
+    auto block = create_block(kTabletId, "ttl-extend-during-demote", 0, 1024, 
&hash);
+    persist_block_meta(kTabletId, hash, block->range().left, 
block->range().size());
+
+    _ttl_mgr = std::make_unique<BlockFileCacheTtlMgr>(_cache.get(), 
_meta_store.get());
+    _ttl_mgr->register_tablet_id(kTabletId);
+
+    ASSERT_TRUE(wait_for_condition([&]() { return block->cache_type() == 
FileCacheType::TTL; },
+                                   std::chrono::seconds(5)));
+
+    // Stall the demotion scan midway so the TTL can be extended underneath 
it, reproducing the
+    // window where the expiration check acts on a view of the tablet that is 
already stale.
+    // Held by value in the callback rather than captured by reference: the 
background threads
+    // outlive this stack frame, and neither the guard nor 
disable_processing() synchronizes
+    // with a callback already in flight. A callback that stalls makes that 
window wide.
+    auto demote_scan_entered = std::make_shared<std::atomic<bool>>(false);
+    auto release_demote_scan = std::make_shared<std::atomic<bool>>(false);
+    auto* sync_point = SyncPoint::get_instance();
+    sync_point->clear_all_call_backs();
+    sync_point->clear_trace();
+    SyncPoint::CallbackGuard guard;
+    sync_point->set_call_back(
+            "BlockFileCacheTtlMgr::get_file_blocks_from_tablet_id",
+            [demote_scan_entered, release_demote_scan](std::vector<std::any>&& 
args) {
+                if (doris::try_any_cast<int64_t>(args[0]) != kTabletId) {
+                    return;
+                }
+                if (demote_scan_entered->exchange(true, 
std::memory_order_acq_rel)) {
+                    return;
+                }
+                while (!release_demote_scan->load(std::memory_order_acquire)) {
+                    std::this_thread::sleep_for(std::chrono::milliseconds(5));
+                }
+            },
+            &guard);
+    sync_point->enable_processing();
+
+    tablet->set_creation_time(UnixSeconds() - 3600);
+    tablet->set_ttl_seconds(1);
+
+    bool scan_stalled = wait_for_condition(
+            [&]() { return 
demote_scan_entered->load(std::memory_order_acquire); },
+            std::chrono::seconds(10));
+
+    // Extend the TTL while the demotion is still in flight.
+    tablet->set_creation_time(UnixSeconds());
+    tablet->set_ttl_seconds(30758400);
+    std::this_thread::sleep_for(std::chrono::milliseconds(200));
+    // Must come before the join below, or stop() waits on a thread parked in 
the callback.
+    release_demote_scan->store(true, std::memory_order_release);
+
+    bool ends_as_ttl = wait_for_condition(

Review Comment:
   [P1] Wait for the conflicting scan to finish before accepting TTL. At this 
point the block is still TTL from setup, and the stalled callback sleeps in 5 
ms increments. The first predicate evaluation after `release_demote_scan=true` 
can therefore succeed before the demotion runs; `_ttl_mgr.reset()` may then 
join a scan that leaves the block NORMAL, while the test still asserts the 
previously captured `true`. Please add an explicit completion barrier for the 
released reconciliation (and proof that the updater observed the extension), 
then assert `block->cache_type()` after the final transition rather than 
relying on this pre-existing state.



##########
be/test/io/cache/block_file_cache_ttl_mgr_test.cpp:
##########
@@ -462,4 +478,280 @@ TEST_F(BlockFileCacheTtlMgrTest, 
TabletTtlRemovedMovesBlocksBackToNormal) {
                                    std::chrono::seconds(5)));
 }
 
+TEST_F(BlockFileCacheTtlMgrTest, ExpiredTtlExtendedMovesBlocksBackToTtl) {
+    constexpr int64_t kTabletId = 6006;
+    auto tablet = std::make_shared<FakeTablet>(UnixSeconds(), 120);
+    fake_engine()->add_tablet(kTabletId, tablet);
+
+    UInt128Wrapper hash;
+    auto block = create_block(kTabletId, "ttl-extend-after-expire", 0, 1024, 
&hash);
+    persist_block_meta(kTabletId, hash, block->range().left, 
block->range().size());
+
+    _ttl_mgr = std::make_unique<BlockFileCacheTtlMgr>(_cache.get(), 
_meta_store.get());
+    _ttl_mgr->register_tablet_id(kTabletId);
+
+    ASSERT_TRUE(wait_for_condition([&]() { return block->cache_type() == 
FileCacheType::TTL; },
+                                   std::chrono::seconds(5)));
+
+    // Let the TTL expire. The block goes back to NORMAL while the manager 
keeps a non-zero TTL
+    // recorded for the tablet, which is the state that used to wedge the 
promotion path.
+    tablet->set_creation_time(UnixSeconds() - 120);
+    tablet->set_ttl_seconds(1);
+    ASSERT_TRUE(wait_for_condition([&]() { return block->cache_type() == 
FileCacheType::NORMAL; },
+                                   std::chrono::seconds(5)));
+
+    // Extending an already expired TTL to one that has not expired has to 
bring the block back.
+    tablet->set_ttl_seconds(30758400);
+    ASSERT_TRUE(wait_for_condition([&]() { return block->cache_type() == 
FileCacheType::TTL; },
+                                   std::chrono::seconds(5)));
+}
+
+TEST_F(BlockFileCacheTtlMgrTest, 
ExtendedTtlThatIsStillExpiredKeepsBlocksNormal) {
+    constexpr int64_t kTabletId = 7007;
+    const int64_t creation_time = UnixSeconds() - 7200;
+    auto tablet = std::make_shared<FakeTablet>(creation_time, 60);
+    fake_engine()->add_tablet(kTabletId, tablet);
+
+    UInt128Wrapper hash;
+    auto block = create_block(kTabletId, "ttl-extend-still-expired", 0, 1024, 
&hash);
+    persist_block_meta(kTabletId, hash, block->range().left, 
block->range().size());
+
+    _ttl_mgr = std::make_unique<BlockFileCacheTtlMgr>(_cache.get(), 
_meta_store.get());
+    _ttl_mgr->register_tablet_id(kTabletId);
+
+    int64_t call_count = fake_engine()->get_tablet_meta_call_count();
+    ASSERT_TRUE(wait_for_condition(
+            [&]() { return fake_engine()->get_tablet_meta_call_count() >= 
call_count + 2; },
+            std::chrono::seconds(5)));
+    ASSERT_EQ(FileCacheType::NORMAL, block->cache_type());
+
+    // A longer TTL that is still in the past must not promote anything.
+    tablet->set_ttl_seconds(120);
+    call_count = fake_engine()->get_tablet_meta_call_count();
+    ASSERT_TRUE(wait_for_condition(
+            [&]() { return fake_engine()->get_tablet_meta_call_count() >= 
call_count + 3; },
+            std::chrono::seconds(5)));
+    EXPECT_EQ(FileCacheType::NORMAL, block->cache_type());
+}
+
+TEST_F(BlockFileCacheTtlMgrTest, 
RewritingTtlToAnotherValidValueDoesNotRescanBlocks) {
+    constexpr int64_t kTabletId = 8008;
+    auto tablet = std::make_shared<FakeTablet>(UnixSeconds(), 3600);
+    fake_engine()->add_tablet(kTabletId, tablet);
+
+    UInt128Wrapper hash;
+    auto block = create_block(kTabletId, "ttl-rewrite-valid", 0, 1024, &hash);
+    persist_block_meta(kTabletId, hash, block->range().left, 
block->range().size());
+
+    _ttl_mgr = std::make_unique<BlockFileCacheTtlMgr>(_cache.get(), 
_meta_store.get());
+    _ttl_mgr->register_tablet_id(kTabletId);
+
+    ASSERT_TRUE(wait_for_condition([&]() { return block->cache_type() == 
FileCacheType::TTL; },
+                                   std::chrono::seconds(5)));
+
+    // The promotion is recorded only after every block has been converted, so 
the manager has
+    // not necessarily finished with the tablet at the moment the type flips. 
Let a couple of
+    // rounds pass before counting, or the tail of the promotion is charged to 
the rewrites.
+    int64_t settled_after = fake_engine()->get_tablet_meta_call_count();
+    ASSERT_TRUE(wait_for_condition(
+            [&]() { return fake_engine()->get_tablet_meta_call_count() >= 
settled_after + 2; },
+            std::chrono::seconds(5)));
+
+    // Held by value in the callback: the background threads outlive this 
stack frame, and
+    // neither the guard nor disable_processing() synchronizes with a callback 
in flight.
+    auto block_scan_count = std::make_shared<std::atomic<int64_t>>(0);
+    auto* sync_point = SyncPoint::get_instance();
+    sync_point->clear_all_call_backs();
+    sync_point->clear_trace();
+    SyncPoint::CallbackGuard guard;
+    sync_point->set_call_back(
+            "BlockFileCacheTtlMgr::get_file_blocks_from_tablet_id",
+            [block_scan_count](std::vector<std::any>&& args) {
+                if (doris::try_any_cast<int64_t>(args[0]) == kTabletId) {
+                    block_scan_count->fetch_add(1, std::memory_order_relaxed);
+                }
+            },
+            &guard);
+    sync_point->enable_processing();
+
+    // Automated jobs rewrite this property regularly. As long as the tablet 
stays in the same
+    // state, none of those rewrites may trigger another walk of the meta 
store.
+    for (int64_t ttl : {7200, 1800, 5400}) {
+        tablet->set_ttl_seconds(ttl);
+        int64_t call_count = fake_engine()->get_tablet_meta_call_count();
+        ASSERT_TRUE(wait_for_condition(
+                [&]() { return fake_engine()->get_tablet_meta_call_count() >= 
call_count + 2; },
+                std::chrono::seconds(5)));
+    }
+
+    // Join the background threads before the callback and its captures go 
away.
+    _ttl_mgr.reset();
+    sync_point->disable_processing();
+    sync_point->clear_trace();
+
+    EXPECT_EQ(0, block_scan_count->load(std::memory_order_relaxed));
+    EXPECT_EQ(FileCacheType::TTL, block->cache_type());
+}
+
+TEST_F(BlockFileCacheTtlMgrTest, TtlExtensionWinsOverConcurrentExpirationScan) 
{
+    constexpr int64_t kTabletId = 9009;
+    auto tablet = std::make_shared<FakeTablet>(UnixSeconds(), 3600);
+    fake_engine()->add_tablet(kTabletId, tablet);
+
+    UInt128Wrapper hash;
+    auto block = create_block(kTabletId, "ttl-extend-during-demote", 0, 1024, 
&hash);
+    persist_block_meta(kTabletId, hash, block->range().left, 
block->range().size());
+
+    _ttl_mgr = std::make_unique<BlockFileCacheTtlMgr>(_cache.get(), 
_meta_store.get());
+    _ttl_mgr->register_tablet_id(kTabletId);
+
+    ASSERT_TRUE(wait_for_condition([&]() { return block->cache_type() == 
FileCacheType::TTL; },
+                                   std::chrono::seconds(5)));
+
+    // Stall the demotion scan midway so the TTL can be extended underneath 
it, reproducing the
+    // window where the expiration check acts on a view of the tablet that is 
already stale.
+    // Held by value in the callback rather than captured by reference: the 
background threads
+    // outlive this stack frame, and neither the guard nor 
disable_processing() synchronizes
+    // with a callback already in flight. A callback that stalls makes that 
window wide.
+    auto demote_scan_entered = std::make_shared<std::atomic<bool>>(false);
+    auto release_demote_scan = std::make_shared<std::atomic<bool>>(false);
+    auto* sync_point = SyncPoint::get_instance();
+    sync_point->clear_all_call_backs();
+    sync_point->clear_trace();
+    SyncPoint::CallbackGuard guard;
+    sync_point->set_call_back(
+            "BlockFileCacheTtlMgr::get_file_blocks_from_tablet_id",
+            [demote_scan_entered, release_demote_scan](std::vector<std::any>&& 
args) {
+                if (doris::try_any_cast<int64_t>(args[0]) != kTabletId) {
+                    return;
+                }
+                if (demote_scan_entered->exchange(true, 
std::memory_order_acq_rel)) {
+                    return;
+                }
+                while (!release_demote_scan->load(std::memory_order_acquire)) {
+                    std::this_thread::sleep_for(std::chrono::milliseconds(5));
+                }
+            },
+            &guard);
+    sync_point->enable_processing();
+
+    tablet->set_creation_time(UnixSeconds() - 3600);

Review Comment:
   [P1] Do not mutate `creation_time` while the manager thread is reading it. 
`TabletMeta::creation_time()` and `set_creation_time()` are plain, unlocked 
accesses to `_creation_time`, so this write races the update thread at 
`tablet_meta->creation_time()` and makes the test undefined/flaky (the same 
pattern appears in the other new expiry tests). Use a fixed past creation time 
before starting the manager and drive active/expired/extended states only 
through the locked TTL setter.



-- 
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