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


##########
fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudReplica.java:
##########
@@ -669,5 +697,10 @@ public void gsonPostProcess() throws IOException {
             }
             this.primaryClusterToBackends = null;
         }
+        // outside the `bes` branch on purpose: the new `be` format 
accumulates stale entries just the same.
+        // The backends module is loaded before db/recycleBin 
(PersistMetaModules.MODULE_NAMES), and the
+        // checkpoint thread resolves Env.getCurrentEnv() to its own Env, so 
the backend set read here is
+        // the one belonging to the image being loaded.
+        removeInvalidRoutes();

Review Comment:
   [P1] Clean routes after checkpoint journal replay
   
   `gsonPostProcess()` runs during `env.loadImage()`, but checkpoint creation 
then calls `env.replayJournal(checkPointVersion)` before `saveImage()`. If 
route updates and the corresponding backend drops are newer than the last 
successful image, this hook either never sees those routes or sees their BEs as 
still present; replay then removes the BEs, and the stale routes are serialized 
into the new image. Once that image reaches the finalized journal ID, no second 
checkpoint is guaranteed, so this can preserve the exact post-image buildup 
that the fix needs to recover from. Please run one exhaustive route sweep after 
replay and before save across every serialized route-bearing object (including 
the recycle bin), and cover load -> route/drop replay -> save in a lifecycle 
test.



##########
fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudReplica.java:
##########
@@ -596,6 +597,33 @@ public void clearClusterToBe(String cluster) {
         secondaryClusterToBackends.remove(cluster);
     }
 
+    /**
+     * Drop the route entries whose backend has been dropped from the cluster.
+     *
+     * Such an entry is already dead weight: getBackendIdImpl() resolves the 
backend id, gets null and
+     * falls back to hashReplicaToBe(), so removing it does not change 
routing. But nothing ever removes
+     * it either -- dropCluster() only touches CloudSystemInfoService, and the 
rebalancer only walks the
+     * compute groups that currently exist -- so entries of dropped compute 
groups pile up forever, both
+     * in FE heap and in the image (the `bes`/`be` field).
+     *
+     * @return how many entries were dropped
+     */
+    public int removeInvalidRoutes() {
+        if (!Config.enable_cloud_replica_stale_route_clean || 
FeConstants.runningUnitTest) {
+            return 0;
+        }
+        SystemInfoService systemInfo = Env.getCurrentSystemInfo();
+        int before = primaryClusterToBackend.size() + 
secondaryClusterToBackends.size();
+        secondaryClusterToBackends.entrySet().removeIf(e -> 
systemInfo.getBackend(e.getValue().key()) == null);
+        // Keep a dead primary whose compute group still has a live secondary. 
With
+        // enable_immediate_be_assign=false that is the normal failover state, 
and the lazy fetch path in
+        // FrontendServiceImpl.getTabletReplicaInfos() enumerates secondaries 
through the primary key set,
+        // so dropping the key would hide a live secondary from the peer cache 
candidates.
+        primaryClusterToBackend.entrySet().removeIf(e -> 
systemInfo.getBackend(e.getValue()) == null

Review Comment:
   [P2] Revalidate the secondary atomically before removing its primary key
   
   The two maps are updated concurrently outside an exclusive table lock. A 
query can add a live secondary after this predicate observes `containsKey == 
false` but before `removeIf` removes the dead primary, leaving a live secondary 
with no primary key. `getTabletReplicaInfos()` enumerates secondaries only 
through `getPrimaryComputeGroupIds()`, so that cache candidate is hidden until 
a later callback repairs the key. Please make the per-group primary/secondary 
decision atomic (or revalidate with a representation that cannot publish a 
secondary-only state) and add a barrier-based interleaving test.



##########
fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudReplica.java:
##########
@@ -596,6 +597,33 @@ public void clearClusterToBe(String cluster) {
         secondaryClusterToBackends.remove(cluster);
     }
 
+    /**
+     * Drop the route entries whose backend has been dropped from the cluster.
+     *
+     * Such an entry is already dead weight: getBackendIdImpl() resolves the 
backend id, gets null and
+     * falls back to hashReplicaToBe(), so removing it does not change 
routing. But nothing ever removes
+     * it either -- dropCluster() only touches CloudSystemInfoService, and the 
rebalancer only walks the
+     * compute groups that currently exist -- so entries of dropped compute 
groups pile up forever, both
+     * in FE heap and in the image (the `bes`/`be` field).
+     *
+     * @return how many entries were dropped
+     */
+    public int removeInvalidRoutes() {
+        if (!Config.enable_cloud_replica_stale_route_clean || 
FeConstants.runningUnitTest) {
+            return 0;
+        }
+        SystemInfoService systemInfo = Env.getCurrentSystemInfo();
+        int before = primaryClusterToBackend.size() + 
secondaryClusterToBackends.size();
+        secondaryClusterToBackends.entrySet().removeIf(e -> 
systemInfo.getBackend(e.getValue().key()) == null);
+        // Keep a dead primary whose compute group still has a live secondary. 
With
+        // enable_immediate_be_assign=false that is the normal failover state, 
and the lazy fetch path in
+        // FrontendServiceImpl.getTabletReplicaInfos() enumerates secondaries 
through the primary key set,
+        // so dropping the key would hide a live secondary from the peer cache 
candidates.
+        primaryClusterToBackend.entrySet().removeIf(e -> 
systemInfo.getBackend(e.getValue()) == null
+                && !secondaryClusterToBackends.containsKey(e.getKey()));
+        return before - primaryClusterToBackend.size() - 
secondaryClusterToBackends.size();

Review Comment:
   [P3] Count successful removals instead of subtracting concurrent sizes
   
   Route writers mutate both maps concurrently, so these three independent size 
samples are not a valid removal count. A secondary added after `before` can 
make this return negative, and additions can also cancel actual removals in the 
INFO aggregate. Please increment the count only when a conditional remove 
succeeds (for example `remove(key, expectedValue)`) and cover a concurrent 
writer with a barrier-based test.



##########
fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java:
##########
@@ -1017,6 +1027,15 @@ private boolean completeRouteInfo() {
             for (Tablet tablet : tablets) {
                 for (Replica r : tablet.getReplicas()) {
                     CloudReplica replica = (CloudReplica) r;
+                    // Drop routes of compute groups that no longer exist; 
gsonPostProcess() only converges
+                    // the catalog on image load, so without this the leader 
keeps them until it restarts.
+                    // No edit log op is written for the removal: the entries 
are already unroutable, every

Review Comment:
   [P1] Add a convergence boundary for follower route state
   
   This removal is neither journaled nor replayed, and `CloudTabletRebalancer` 
is started only by `startMasterOnlyDaemonThreads()`. A follower that loads a 
route and later replays that backend's drop keeps the stale route for its 
entire lifetime; pushed checkpoint images are only downloaded by `/put`, not 
loaded into the serving Env. It can therefore retain the same heap leak and 
carry it through promotion (where zero-group/multi-replica states still skip 
the sweep), contrary to the claim that every FE reaches the same conclusion. 
Please clean after serving journal replay on every FE or persist/replay an 
equivalent bounded cleanup, and test follower replay plus promotion without 
restart.



##########
fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java:
##########
@@ -1017,6 +1027,15 @@ private boolean completeRouteInfo() {
             for (Tablet tablet : tablets) {
                 for (Replica r : tablet.getReplicas()) {
                     CloudReplica replica = (CloudReplica) r;
+                    // Drop routes of compute groups that no longer exist; 
gsonPostProcess() only converges
+                    // the catalog on image load, so without this the leader 
keeps them until it restarts.
+                    // No edit log op is written for the removal: the entries 
are already unroutable, every
+                    // FE reaches the same conclusion from its own backend 
set, and the image is written by
+                    // the checkpoint Env from a fresh load, so a 
leader/follower difference never reaches
+                    // persisted state and is gone after one round on the new 
leader.
+                    if (cluster.equals(cleanupPassTicket)) {
+                        staleRouteNum[0] += replica.removeInvalidRoutes();

Review Comment:
   [P1] Sweep every retained route-bearing metadata owner
   
   This pass visits only live visible indexes. Normal drops detach 
tables/partitions into `CatalogRecycleBin`; finished `RollupJobV2` records 
retain serialized installed-index graphs that become separate copies after 
image load; and warmup can populate routes on shadow indexes that 
`IndexExtState.VISIBLE` skips. Backend drops therefore leave these maps stale 
for their retention/lifecycle windows even while normal leader balancing 
continues. Please define a bounded visitor over every retained route-bearing 
owner and reuse it for topology/post-replay cleanup, with recycle/recover, 
finished-rollup image, and warmed-shadow coverage.



##########
fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java:
##########
@@ -1006,6 +1006,16 @@ public void checkDecommissionState(Map<String, 
List<Long>> clusterToBes) {
     private boolean completeRouteInfo() {
         List<UpdateCloudReplicaInfo> updateReplicaInfos = new 
ArrayList<UpdateCloudReplicaInfo>();
         long[] assignedErrNum = {0L};
+        long[] staleRouteNum = {0L};
+        // removeInvalidRoutes() scans a replica's whole route map, which has 
nothing to do with the compute
+        // group currently being processed. But loopCloudReplica() has the 
compute group loop innermost, so
+        // it hands us every replica once per live compute group -- calling 
the cleanup unconditionally
+        // would repeat the same scan N times per replica, all of it under 
table.readLock(). Any single

Review Comment:
   [P1] Do not rescan every replica route map every second
   
   The ticket prevents N scans per live group, but this still performs two full 
map traversals and backend lookups once per replica on every default one-second 
round, all while `loopCloudReplica()` holds the table's fair read lock. After 
the first cleanup, that O(replicas * retained routes) work is entirely 
redundant and directly lengthens metadata-writer waits. Please gate or 
rate-limit the sweep on a backend-topology generation (and gather replica 
references without holding the lock for the expensive scan), with scale 
coverage that bounds lock hold time.



##########
fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java:
##########
@@ -1006,6 +1006,16 @@ public void checkDecommissionState(Map<String, 
List<Long>> clusterToBes) {
     private boolean completeRouteInfo() {
         List<UpdateCloudReplicaInfo> updateReplicaInfos = new 
ArrayList<UpdateCloudReplicaInfo>();
         long[] assignedErrNum = {0L};
+        long[] staleRouteNum = {0L};
+        // removeInvalidRoutes() scans a replica's whole route map, which has 
nothing to do with the compute
+        // group currently being processed. But loopCloudReplica() has the 
compute group loop innermost, so
+        // it hands us every replica once per live compute group -- calling 
the cleanup unconditionally
+        // would repeat the same scan N times per replica, all of it under 
table.readLock(). Any single
+        // compute group id works as a ticket to run it exactly once per 
replica per round; which one is
+        // irrelevant. If clusterToBes is empty the callback is never invoked 
at all, so the serving
+        // catalog keeps the stale entries until it reloads the image -- there 
is nothing to route in that
+        // state anyway.
+        String cleanupPassTicket = 
clusterToBes.keySet().stream().findFirst().orElse(null);

Review Comment:
   [P1] Make serving cleanup independent of a live-group callback
   
   When `clusterToBes` is empty, `loopCloudReplica()` never invokes this 
callback, so after the last compute group is dropped every stale per-replica 
route remains in the serving FE indefinitely. The same sweep is bypassed 
entirely by the earlier `enable_cloud_multi_replica` return, even though that 
setting is mutable and old single-replica routes can remain allocated. 'Nothing 
to route' does not address this PR's heap/image-leak goal. Please run a 
replica-only cleanup boundary even with zero groups and before the 
multi-replica balancing early return, with integration coverage for both states.



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