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


##########
fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java:
##########
@@ -1029,24 +1035,62 @@ boolean staleRouteSweepNeeded(Set<Long> currentBes) {
         // baseline. Advancing only after a sweep would leave the baseline at 
the pre-addition set, and
         // dropping that same backend later would compare equal to it and go 
unnoticed.
         lastSweptBackends = currentBes;
-        if (pendingSweepRounds > 0) {
+        // Outside the configured window, leave pendingSweepRounds untouched 
rather than draining it: a
+        // backend that goes away outside the window must still get its two 
rounds once the window
+        // opens, not lose them to rounds that never actually swept.
+        if (pendingSweepRounds > 0 && isStaleRouteCleanTimeAllowed()) {
             pendingSweepRounds--;

Review Comment:
   [P2] Schedule pending cleanup into the configured window
   
   Leaving `pendingSweepRounds` intact is not enough to guarantee a sweep: the 
daemon's next wake-up is still driven only by the independently mutable 
`cloud_tablet_rebalancer_interval_second`. If that cadence is longer than, or 
phase-shifted away from, a short window (for example a daily/long interval with 
a ten-minute window), every run can miss the window and the pending cleanup 
remains unboundedly deferred. Please schedule the next pending round at the 
next window opening, or validate/enforce an interval that cannot skip the 
window, and add a scheduler-level test.



##########
fe/fe-core/src/main/java/org/apache/doris/cloud/datasource/CloudInternalCatalog.java:
##########
@@ -1390,6 +1392,83 @@ public void 
replayUpdateCloudReplica(UpdateCloudReplicaInfo info) throws MetaNot
         }
     }
 
+    // The three private helpers below are each independently callable (one 
per catalog level), so this
+    // guard is checked in every one of them rather than once at the top -- a 
config flip mid-sweep then
+    // takes effect at the next db/table/partition boundary instead of only on 
the next whole-catalog call.
+    private static boolean routeCleanupDisabled() {
+        return Config.isNotCloudMode() || 
!Config.enable_cloud_replica_stale_route_clean
+                || FeConstants.runningUnitTest;
+    }
+
+    /**
+     * @param systemInfo the backend set to judge staleness against. Must come 
from the same Env this
+     *                    catalog belongs to (the serving Env during 
replay/rebalancing, or the checkpoint's
+     *                    private Env while generating an image) -- never 
resolved internally via
+     *                    Env.getCurrentSystemInfo(), so a caller cannot 
accidentally sweep this catalog's
+     *                    replicas against a different Env's backend set.
+     */
+    public long removeInvalidCloudReplicaRoutes(SystemInfoService systemInfo) {
+        if (routeCleanupDisabled()) {
+            return 0;
+        }
+        long start = System.currentTimeMillis();
+        long removed = 0;
+        for (Long dbId : getDbIds()) {
+            Database db = getDbNullable(dbId);
+            if (db == null) {
+                continue; // The database can be dropped concurrently on the 
serving Env.
+            }
+            removed += removeInvalidCloudReplicaRoutes(db, systemInfo);
+        }
+        LOG.info("swept stale cloud routes, entries dropped {}, cost {} ms",
+                removed, System.currentTimeMillis() - start);
+        return removed;
+    }
+
+    private static long removeInvalidCloudReplicaRoutes(Database db, 
SystemInfoService systemInfo) {
+        if (routeCleanupDisabled()) {
+            return 0;

Review Comment:
   [P2] Do not acknowledge a sweep aborted by a config flip
   
   These nested guards make a mid-scan disable return normally after only part 
of the catalog, but `CloudEnv.replayJournal()` cannot distinguish that from a 
complete pass and advances `cleanedBackendRemovalVersion`. If the mutable flag 
is re-enabled before the next replay call observes the disabled state, the 
removal version is unchanged and the skipped databases are never retried. 
Please snapshot the enabled state for an indivisible pass, or return an 
explicit incomplete result and advance the generation only after full 
traversal; a latch-based disable/re-enable test across database boundaries 
would cover the race.



##########
fe/fe-core/src/main/java/org/apache/doris/master/Checkpoint.java:
##########
@@ -159,6 +161,13 @@ public synchronized void doCheckpoint() throws 
CheckpointException {
             }
             env.postProcessAfterMetadataReplayed(false);
             postProcessCloudMetadata();
+            try {
+                removeInvalidCloudReplicaRoutes(env);
+            } catch (Exception e) {

Review Comment:
   [P2] Sweep every replica graph written to the image
   
   This only walks `InternalCatalog`, but later image modules write independent 
graphs containing `CloudReplica`: retained `BackupJob`s can hold a `backupMeta` 
table deep-copy, and retained `RollupJobV2`s keep `partitionIdToRollupIndex` 
even after cancellation detaches the index or the job finishes. If either graph 
captures a route while backend B exists and a later drop is replayed, this pass 
cannot remove B before `backupHandler` or `alterJob` serializes it. The `db` 
and `alterJob` modules also deserialize independently, so active-job copies are 
not alias-cleaned after a load. Verification loading may sanitize its temporary 
Env, but cannot rewrite the image already emitted. Please sweep every persisted 
replica owner (or strip these caches) and test backup and cancelled-rollup 
states.



##########
fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java:
##########
@@ -1029,24 +1035,62 @@ boolean staleRouteSweepNeeded(Set<Long> currentBes) {
         // baseline. Advancing only after a sweep would leave the baseline at 
the pre-addition set, and
         // dropping that same backend later would compare equal to it and go 
unnoticed.
         lastSweptBackends = currentBes;
-        if (pendingSweepRounds > 0) {
+        // Outside the configured window, leave pendingSweepRounds untouched 
rather than draining it: a
+        // backend that goes away outside the window must still get its two 
rounds once the window
+        // opens, not lose them to rounds that never actually swept.
+        if (pendingSweepRounds > 0 && isStaleRouteCleanTimeAllowed()) {
             pendingSweepRounds--;
             return true;
         }
         return false;
     }
 
+    /**
+     * Whether the configured cleanup window 
(cloud_tablet_rebalancer_stale_route_clean_start_time to
+     * ..._end_time) contains the current time. Equal start/end -- including 
the "00:00"/"00:00" default --
+     * means unrestricted, matching the pre-existing behavior of sweeping 
whenever staleRouteSweepNeeded()
+     * says a sweep is due. An unparseable configuration also falls back to 
unrestricted rather than
+     * silently disabling cleanup.
+     */
+    @VisibleForTesting
+    boolean isStaleRouteCleanTimeAllowed() {
+        LocalTime start = 
parseCleanTime(Config.cloud_tablet_rebalancer_stale_route_clean_start_time);
+        LocalTime end = 
parseCleanTime(Config.cloud_tablet_rebalancer_stale_route_clean_end_time);

Review Comment:
   [P2] Make cleanup-window reconfiguration atomic
   
   The start and end are separate mutable configs, but this method immediately 
interprets any observed pair as a complete window. SQL can update only one 
config per statement, and the REST handler also applies entries sequentially. 
For example, changing `23:00-23:30` to `13:00-13:30` end-first temporarily 
creates the wrapping window `23:00-13:30`; a pending one-second daemon round at 
noon can consume a full-catalog sweep outside both the old and intended 
maintenance windows. Please publish/validate the pair atomically (or reject 
mixed generations) and add a test that attempts a sweep between the endpoint 
updates.



##########
fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java:
##########
@@ -1029,24 +1035,62 @@ boolean staleRouteSweepNeeded(Set<Long> currentBes) {
         // baseline. Advancing only after a sweep would leave the baseline at 
the pre-addition set, and
         // dropping that same backend later would compare equal to it and go 
unnoticed.
         lastSweptBackends = currentBes;
-        if (pendingSweepRounds > 0) {
+        // Outside the configured window, leave pendingSweepRounds untouched 
rather than draining it: a
+        // backend that goes away outside the window must still get its two 
rounds once the window
+        // opens, not lose them to rounds that never actually swept.
+        if (pendingSweepRounds > 0 && isStaleRouteCleanTimeAllowed()) {
             pendingSweepRounds--;
             return true;
         }
         return false;
     }
 
+    /**
+     * Whether the configured cleanup window 
(cloud_tablet_rebalancer_stale_route_clean_start_time to
+     * ..._end_time) contains the current time. Equal start/end -- including 
the "00:00"/"00:00" default --
+     * means unrestricted, matching the pre-existing behavior of sweeping 
whenever staleRouteSweepNeeded()
+     * says a sweep is due. An unparseable configuration also falls back to 
unrestricted rather than
+     * silently disabling cleanup.
+     */
+    @VisibleForTesting
+    boolean isStaleRouteCleanTimeAllowed() {
+        LocalTime start = 
parseCleanTime(Config.cloud_tablet_rebalancer_stale_route_clean_start_time);
+        LocalTime end = 
parseCleanTime(Config.cloud_tablet_rebalancer_stale_route_clean_end_time);
+        if (start == null || end == null || start.equals(end)) {
+            return true;
+        }
+        return isWithinWindow(LocalTime.now(TimeUtils.getDorisZoneId()), 
start, end);
+    }
+
+    @VisibleForTesting
+    static boolean isWithinWindow(LocalTime now, LocalTime start, LocalTime 
end) {
+        if (start.isBefore(end)) {
+            return !now.isBefore(start) && !now.isAfter(end);
+        }
+        // across midnight, e.g. 23:00 - 06:00
+        return !now.isBefore(start) || !now.isAfter(end);
+    }
+
+    private static LocalTime parseCleanTime(String time) {
+        try {
+            return LocalTime.parse(time, STALE_ROUTE_CLEAN_TIME_FORMAT);
+        } catch (DateTimeParseException e) {
+            LOG.warn("invalid cloud_tablet_rebalancer_stale_route_clean time: 
{}", time);
+            return null;
+        }
+    }
+
     private boolean completeRouteInfo() {
         List<UpdateCloudReplicaInfo> updateReplicaInfos = new 
ArrayList<UpdateCloudReplicaInfo>();
         long[] assignedErrNum = {0L};
-        long[] staleRouteNum = {0L};
         boolean sweepStaleRoutes = staleRouteSweepNeeded(allBes);
-        // loopCloudReplica() has the compute group loop innermost, so it 
hands us every replica once per
-        // live compute group, while removeInvalidRoutes() scans the whole 
route map and does not care
-        // which group we are on. Pin the sweep to one arbitrary group id so a 
sweeping round still makes a
-        // single pass per replica. If clusterToBes is empty the callback 
never runs at all, so the serving
-        // catalog keeps the entries until it reloads the image -- there is 
nothing to route in that state.
-        String sweepTicket = sweepStaleRoutes ? 
clusterToBes.keySet().stream().findFirst().orElse(null) : null;
+        // Cleanup is independent of compute groups and includes shadow 
indices of active tables.
+        // Take the catalog and backend set from the same Env so the sweep can 
never mix the two.
+        Env currentEnv = Env.getCurrentEnv();
+        long staleRouteNum = sweepStaleRoutes
+                ? ((CloudInternalCatalog) currentEnv.getInternalCatalog())
+                        
.removeInvalidCloudReplicaRoutes(currentEnv.getClusterInfo())

Review Comment:
   [P2] Run stale-route cleanup when multi-replica mode skips balancing
   
   This sweep is only reached through `completeRouteInfo()`, but 
`runAfterCatalogReady()` returns before it whenever the mutable 
`enable_cloud_multi_replica` flag is set. A concrete sequence is to populate 
route maps in normal mode, enable multi-replica, and then drop a compute group: 
the live master does not replay its own `OP_DROP_BACKEND`, and the 
multi-replica query branch returns from rendezvous hashing without touching 
these legacy maps, so their dropped-backend entries remain until restart or the 
flag is disabled. Please move the cleanup trigger ahead of (or independent 
from) the balancing-only early return and cover this transition.



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