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


##########
be/src/cloud/cloud_tablet.h:
##########
@@ -348,7 +348,14 @@ class CloudTablet final : public BaseTablet {
 
     const auto& rowset_map() const { return _rs_version_map; }
 
-    int64_t last_sync_time_s = 0;
+    // How long since this tablet's ROWSETS were pulled from MS. Only 
sync_rowsets() advances
+    // it, and only when it actually issues the RPC.
+    int64_t last_sync_rowsets_time_s = 0;
+    // How long since this tablet's META was pulled from MS, which is what 
carries properties
+    // such as the file cache TTL. Only sync_meta() advances it. Tracked 
separately on
+    // purpose: a tablet under continuous ingest keeps the rowsets clock 
permanently fresh,
+    // so gating meta work on that one starves the meta refresh entirely.
+    int64_t last_sync_tablet_meta_time_s = 0;

Review Comment:
   [P1] Synchronize the new metadata refresh clock
   
   The dedicated `sync_tablets_thread` reads this field in 
`CloudTabletMgr::sync_tablets()`, while 
`CloudInternalServiceImpl::sync_tablet_meta()` can run `sync_meta()` 
concurrently in `_light_work_pool` and write it. The writer holds 
`_sync_meta_lock`, but the scheduler does not, so this new state has a C++ data 
race and its due/not-due decision is undefined. Please make both scheduling 
clocks atomic (relaxed loads/stores are sufficient if they remain timestamp 
hints) or guard every scheduler/RPC/compaction access with one documented 
common lock.



##########
be/src/cloud/cloud_tablet_mgr.cpp:
##########
@@ -386,53 +391,93 @@ std::vector<std::weak_ptr<CloudTablet>> 
CloudTabletMgr::get_weak_tablets() {
 
 void CloudTabletMgr::sync_tablets(const CountDownLatch& stop_latch) {
     LOG_INFO("begin to sync tablets");
-    int64_t last_sync_time_bound = ::time(nullptr) - 
config::tablet_sync_interval_s;
 
-    auto weak_tablets = get_weak_tablets();
+    // A tablet carries two staleness clocks and each one gates a different 
RPC:
+    //
+    //   last_sync_rowsets_time_s
+    //       how long since we pulled this tablet's ROWSETS from MS. Only 
sync_rowsets()
+    //       advances it, and only when it actually issues the RPC -- a query 
whose requested
+    //       version we already hold returns early and leaves the clock 
untouched.
+    //
+    //   last_sync_tablet_meta_time_s
+    //       how long since we pulled this tablet's META from MS, which is 
what carries
+    //       properties such as the file cache TTL. Only sync_meta() advances 
it.
+    //
+    // They have to be read separately. A tablet under continuous ingest keeps 
the rowsets
+    // clock permanently fresh, so selecting meta work by it -- as this used 
to -- means such a
+    // tablet never has its meta refreshed at all, and it keeps serving 
whatever TTL it was
+    // built with.
+    const int64_t stale_before = ::time(nullptr) - 
config::tablet_sync_interval_s;
+
+    struct Work {
+        std::weak_ptr<CloudTablet> tablet;
+        bool needs_meta = false;
+        bool needs_rowsets = false;
+    };
 
-    // sort by last_sync_time
+    // Ordered by the older of the two clocks, so that if we are told to stop 
half way, the
+    // tablets that have been waiting longest have already been served.
     static auto cmp = [](const auto& a, const auto& b) { return a.first < 
b.first; };
-    std::multiset<std::pair<int64_t, std::weak_ptr<CloudTablet>>, 
decltype(cmp)>
-            sync_time_tablet_set(cmp);
+    std::multiset<std::pair<int64_t, Work>, decltype(cmp)> due(cmp);
 
-    for (auto& weak_tablet : weak_tablets) {
-        if (auto tablet = weak_tablet.lock()) {
-            int64_t last_sync_time = tablet->last_sync_time_s;
-            if (last_sync_time <= last_sync_time_bound) {
-                sync_time_tablet_set.emplace(last_sync_time, weak_tablet);
-            }
+    for (auto& weak_tablet : get_weak_tablets()) {
+        auto tablet = weak_tablet.lock();
+        if (!tablet) {
+            continue;
+        }
+        const bool needs_rowsets = tablet->last_sync_rowsets_time_s <= 
stale_before;
+        Work work {
+                .tablet = weak_tablet,
+                // Pulling rowsets implies pulling the tablet meta: the 
rowsets we are about
+                // to take are only as trustworthy as the meta they belong to, 
and this is
+                // the relationship the previous single pass had.
+                .needs_meta = needs_rowsets || 
tablet->last_sync_tablet_meta_time_s <= stale_before,

Review Comment:
   [P1] Seed the metadata clock after the cache-miss fetch
   
   A normal cache miss has just completed `get_tablet_meta()` before 
constructing this tablet, but the new clock remains zero; the initial rowset 
sync only advances the rowset clock. Consequently the next scheduler pass (600 
seconds by default) treats every newly cached tablet as metadata-stale instead 
of honoring the 1,800-second interval, adding one unnecessary GetTablet RPC per 
cached tablet after restart/cache churn (up to the default 100,000-tablet 
capacity). Stamp the clock when the fetched metadata is installed in 
`CloudTabletMgr::get_tablet()` and add a test that leaves the post-load clocks 
untouched.



##########
be/src/cloud/cloud_tablet_mgr.cpp:
##########
@@ -386,53 +391,93 @@ std::vector<std::weak_ptr<CloudTablet>> 
CloudTabletMgr::get_weak_tablets() {
 
 void CloudTabletMgr::sync_tablets(const CountDownLatch& stop_latch) {
     LOG_INFO("begin to sync tablets");
-    int64_t last_sync_time_bound = ::time(nullptr) - 
config::tablet_sync_interval_s;
 
-    auto weak_tablets = get_weak_tablets();
+    // A tablet carries two staleness clocks and each one gates a different 
RPC:
+    //
+    //   last_sync_rowsets_time_s
+    //       how long since we pulled this tablet's ROWSETS from MS. Only 
sync_rowsets()
+    //       advances it, and only when it actually issues the RPC -- a query 
whose requested
+    //       version we already hold returns early and leaves the clock 
untouched.
+    //
+    //   last_sync_tablet_meta_time_s
+    //       how long since we pulled this tablet's META from MS, which is 
what carries
+    //       properties such as the file cache TTL. Only sync_meta() advances 
it.
+    //
+    // They have to be read separately. A tablet under continuous ingest keeps 
the rowsets
+    // clock permanently fresh, so selecting meta work by it -- as this used 
to -- means such a
+    // tablet never has its meta refreshed at all, and it keeps serving 
whatever TTL it was
+    // built with.
+    const int64_t stale_before = ::time(nullptr) - 
config::tablet_sync_interval_s;
+
+    struct Work {
+        std::weak_ptr<CloudTablet> tablet;
+        bool needs_meta = false;
+        bool needs_rowsets = false;
+    };
 
-    // sort by last_sync_time
+    // Ordered by the older of the two clocks, so that if we are told to stop 
half way, the
+    // tablets that have been waiting longest have already been served.
     static auto cmp = [](const auto& a, const auto& b) { return a.first < 
b.first; };
-    std::multiset<std::pair<int64_t, std::weak_ptr<CloudTablet>>, 
decltype(cmp)>
-            sync_time_tablet_set(cmp);
+    std::multiset<std::pair<int64_t, Work>, decltype(cmp)> due(cmp);
 
-    for (auto& weak_tablet : weak_tablets) {
-        if (auto tablet = weak_tablet.lock()) {
-            int64_t last_sync_time = tablet->last_sync_time_s;
-            if (last_sync_time <= last_sync_time_bound) {
-                sync_time_tablet_set.emplace(last_sync_time, weak_tablet);
-            }
+    for (auto& weak_tablet : get_weak_tablets()) {
+        auto tablet = weak_tablet.lock();
+        if (!tablet) {
+            continue;
+        }
+        const bool needs_rowsets = tablet->last_sync_rowsets_time_s <= 
stale_before;
+        Work work {
+                .tablet = weak_tablet,
+                // Pulling rowsets implies pulling the tablet meta: the 
rowsets we are about
+                // to take are only as trustworthy as the meta they belong to, 
and this is
+                // the relationship the previous single pass had.
+                .needs_meta = needs_rowsets || 
tablet->last_sync_tablet_meta_time_s <= stale_before,
+                .needs_rowsets = needs_rowsets};
+        if (!work.needs_meta && !work.needs_rowsets) {
+            continue;
         }
+        due.emplace(
+                std::min(tablet->last_sync_tablet_meta_time_s, 
tablet->last_sync_rowsets_time_s),
+                std::move(work));
     }
 
     int num_sync = 0;
-    for (auto&& [_, weak_tablet] : sync_time_tablet_set) {
+    int num_sync_meta = 0;
+    for (auto&& [_, work] : due) {

Review Comment:
   [P2] Revalidate queued refresh work before issuing RPCs
   
   These booleans were captured while the entire due set was built, and the 
loop can wait behind many earlier tablets. During that gap a 
load/query/compaction path can refresh rowsets, or the internal-service worker 
can refresh metadata. The old code re-read the rowset clock immediately before 
work; now `sync_meta()` and `sync_rowsets(query_version=-1)` will issue the 
same RPC again after waiting for the concurrent refresh. Re-evaluate each clock 
at execution time (ideally inside the serialized operation to close the wait 
window) so a large pass does not create O(resident-tablets) obsolete MS calls.



##########
be/src/cloud/cloud_tablet_mgr.cpp:
##########
@@ -386,53 +391,93 @@ std::vector<std::weak_ptr<CloudTablet>> 
CloudTabletMgr::get_weak_tablets() {
 
 void CloudTabletMgr::sync_tablets(const CountDownLatch& stop_latch) {
     LOG_INFO("begin to sync tablets");
-    int64_t last_sync_time_bound = ::time(nullptr) - 
config::tablet_sync_interval_s;
 
-    auto weak_tablets = get_weak_tablets();
+    // A tablet carries two staleness clocks and each one gates a different 
RPC:
+    //
+    //   last_sync_rowsets_time_s
+    //       how long since we pulled this tablet's ROWSETS from MS. Only 
sync_rowsets()
+    //       advances it, and only when it actually issues the RPC -- a query 
whose requested
+    //       version we already hold returns early and leaves the clock 
untouched.
+    //
+    //   last_sync_tablet_meta_time_s
+    //       how long since we pulled this tablet's META from MS, which is 
what carries
+    //       properties such as the file cache TTL. Only sync_meta() advances 
it.
+    //
+    // They have to be read separately. A tablet under continuous ingest keeps 
the rowsets
+    // clock permanently fresh, so selecting meta work by it -- as this used 
to -- means such a
+    // tablet never has its meta refreshed at all, and it keeps serving 
whatever TTL it was
+    // built with.
+    const int64_t stale_before = ::time(nullptr) - 
config::tablet_sync_interval_s;
+
+    struct Work {
+        std::weak_ptr<CloudTablet> tablet;
+        bool needs_meta = false;
+        bool needs_rowsets = false;
+    };
 
-    // sort by last_sync_time
+    // Ordered by the older of the two clocks, so that if we are told to stop 
half way, the
+    // tablets that have been waiting longest have already been served.
     static auto cmp = [](const auto& a, const auto& b) { return a.first < 
b.first; };
-    std::multiset<std::pair<int64_t, std::weak_ptr<CloudTablet>>, 
decltype(cmp)>
-            sync_time_tablet_set(cmp);
+    std::multiset<std::pair<int64_t, Work>, decltype(cmp)> due(cmp);
 
-    for (auto& weak_tablet : weak_tablets) {
-        if (auto tablet = weak_tablet.lock()) {
-            int64_t last_sync_time = tablet->last_sync_time_s;
-            if (last_sync_time <= last_sync_time_bound) {
-                sync_time_tablet_set.emplace(last_sync_time, weak_tablet);
-            }
+    for (auto& weak_tablet : get_weak_tablets()) {
+        auto tablet = weak_tablet.lock();
+        if (!tablet) {
+            continue;
+        }
+        const bool needs_rowsets = tablet->last_sync_rowsets_time_s <= 
stale_before;
+        Work work {
+                .tablet = weak_tablet,
+                // Pulling rowsets implies pulling the tablet meta: the 
rowsets we are about
+                // to take are only as trustworthy as the meta they belong to, 
and this is
+                // the relationship the previous single pass had.
+                .needs_meta = needs_rowsets || 
tablet->last_sync_tablet_meta_time_s <= stale_before,
+                .needs_rowsets = needs_rowsets};
+        if (!work.needs_meta && !work.needs_rowsets) {
+            continue;
         }
+        due.emplace(
+                std::min(tablet->last_sync_tablet_meta_time_s, 
tablet->last_sync_rowsets_time_s),
+                std::move(work));
     }
 
     int num_sync = 0;
-    for (auto&& [_, weak_tablet] : sync_time_tablet_set) {
+    int num_sync_meta = 0;
+    for (auto&& [_, work] : due) {
         if (stop_latch.count() <= 0) {
             break;
         }
+        auto tablet = work.tablet.lock();
+        if (!tablet) {
+            continue;
+        }
 
-        if (auto tablet = weak_tablet.lock()) {
-            if (tablet->last_sync_time_s > last_sync_time_bound) {
-                continue;
-            }
-
-            ++num_sync;
+        if (work.needs_meta) {

Review Comment:
   [P1] Synchronize metadata readers before broadening periodic refresh
   
   This new periodic branch calls `sync_meta()` for actively ingested tablets 
while the separate compaction-producer thread is selecting work. `sync_meta()` 
writes `TabletMeta::_compaction_policy` (a `std::string`), the time-series 
thresholds, vertical-group size, and `TabletSchema::_disable_auto_compaction` 
under `_meta_lock`, but `get_topn_tablets_to_compact()` and 
`get_cloud_base_compaction_score()` read those fields without that lock. The 
resulting string/scalar read-write races are undefined behavior and can corrupt 
or mis-schedule compaction. The existing best-effort push already exposed the 
unsafe contract, but this PR adds the periodic writer path for the previously 
skipped population; please give every refreshable property a consistent 
lock-taking/snapshot accessor contract before enabling it.



##########
be/src/cloud/cloud_tablet_mgr.cpp:
##########
@@ -386,53 +391,93 @@ std::vector<std::weak_ptr<CloudTablet>> 
CloudTabletMgr::get_weak_tablets() {
 
 void CloudTabletMgr::sync_tablets(const CountDownLatch& stop_latch) {
     LOG_INFO("begin to sync tablets");
-    int64_t last_sync_time_bound = ::time(nullptr) - 
config::tablet_sync_interval_s;
 
-    auto weak_tablets = get_weak_tablets();
+    // A tablet carries two staleness clocks and each one gates a different 
RPC:
+    //
+    //   last_sync_rowsets_time_s
+    //       how long since we pulled this tablet's ROWSETS from MS. Only 
sync_rowsets()
+    //       advances it, and only when it actually issues the RPC -- a query 
whose requested
+    //       version we already hold returns early and leaves the clock 
untouched.
+    //
+    //   last_sync_tablet_meta_time_s
+    //       how long since we pulled this tablet's META from MS, which is 
what carries
+    //       properties such as the file cache TTL. Only sync_meta() advances 
it.
+    //
+    // They have to be read separately. A tablet under continuous ingest keeps 
the rowsets
+    // clock permanently fresh, so selecting meta work by it -- as this used 
to -- means such a
+    // tablet never has its meta refreshed at all, and it keeps serving 
whatever TTL it was
+    // built with.
+    const int64_t stale_before = ::time(nullptr) - 
config::tablet_sync_interval_s;
+
+    struct Work {
+        std::weak_ptr<CloudTablet> tablet;
+        bool needs_meta = false;
+        bool needs_rowsets = false;
+    };
 
-    // sort by last_sync_time
+    // Ordered by the older of the two clocks, so that if we are told to stop 
half way, the
+    // tablets that have been waiting longest have already been served.
     static auto cmp = [](const auto& a, const auto& b) { return a.first < 
b.first; };
-    std::multiset<std::pair<int64_t, std::weak_ptr<CloudTablet>>, 
decltype(cmp)>
-            sync_time_tablet_set(cmp);
+    std::multiset<std::pair<int64_t, Work>, decltype(cmp)> due(cmp);
 
-    for (auto& weak_tablet : weak_tablets) {
-        if (auto tablet = weak_tablet.lock()) {
-            int64_t last_sync_time = tablet->last_sync_time_s;
-            if (last_sync_time <= last_sync_time_bound) {
-                sync_time_tablet_set.emplace(last_sync_time, weak_tablet);
-            }
+    for (auto& weak_tablet : get_weak_tablets()) {
+        auto tablet = weak_tablet.lock();
+        if (!tablet) {
+            continue;
+        }
+        const bool needs_rowsets = tablet->last_sync_rowsets_time_s <= 
stale_before;
+        Work work {
+                .tablet = weak_tablet,
+                // Pulling rowsets implies pulling the tablet meta: the 
rowsets we are about
+                // to take are only as trustworthy as the meta they belong to, 
and this is
+                // the relationship the previous single pass had.
+                .needs_meta = needs_rowsets || 
tablet->last_sync_tablet_meta_time_s <= stale_before,
+                .needs_rowsets = needs_rowsets};
+        if (!work.needs_meta && !work.needs_rowsets) {
+            continue;
         }
+        due.emplace(
+                std::min(tablet->last_sync_tablet_meta_time_s, 
tablet->last_sync_rowsets_time_s),
+                std::move(work));
     }
 
     int num_sync = 0;
-    for (auto&& [_, weak_tablet] : sync_time_tablet_set) {
+    int num_sync_meta = 0;
+    for (auto&& [_, work] : due) {
         if (stop_latch.count() <= 0) {
             break;
         }
+        auto tablet = work.tablet.lock();
+        if (!tablet) {
+            continue;
+        }
 
-        if (auto tablet = weak_tablet.lock()) {
-            if (tablet->last_sync_time_s > last_sync_time_bound) {
-                continue;
-            }
-
-            ++num_sync;
+        if (work.needs_meta) {
+            ++num_sync_meta;

Review Comment:
   [P1] Do not mutate a shared cached TabletSchema in place
   
   `sync_meta()` reaches 
`mutable_tablet_schema()->set_disable_auto_compaction()`, but 
`TabletMeta::init_from_pb()` stores the process-global `TabletSchemaCache` 
result keyed only by the serialized schema. Two tablets with identical schemas 
therefore share this object. Refreshing tablet A from 
`disable_auto_compaction=false` to `true` mutates tablet B as well and leaves 
the cache's old false-schema key pointing at true contents, so later false 
lookups can also return the poisoned value. This remains wrong even with 
perfect locking. Install a schema/cache entry representing the new flag (and 
update the handle), or keep this dynamic property outside the shared schema; 
add a two-table deduplication test.



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