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

gavinchou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new f0affa953df [fix](cloud) Release warm-up destination on initialization 
failure (#67924)
f0affa953df is described below

commit f0affa953dff3d0e761bd6bb2f62b231b31a9e26
Author: bobhan1 <[email protected]>
AuthorDate: Thu Sep 17 11:22:26 2026 +0800

    [fix](cloud) Release warm-up destination on initialization failure (#67924)
    
    A cloud warm-up job registers its destination compute group before
    initializing tablet batches. If initialization throws, the outer `run()`
    handler only logs the exception: the job stays `PENDING` and keeps the
    destination registration. Other ONCE/PERIODIC jobs targeting that group
    cannot start. A later successful retry can recover the original job, but
    repeated initialization failures can block the group indefinitely
    because the warm-up timeout only applies to `RUNNING` jobs.
    
    Catch initialization failures before transitioning to `RUNNING` and
    reuse `cancel(..., false)` to persist the error and release the
    destination registration. ONCE jobs become `CANCELLED`; PERIODIC jobs
    remain `PENDING` and retry at their existing interval. Initialization
    has not submitted work to BEs, so this path does not send cleanup RPCs.
    Successful initialization retains the destination registration as
    before.
    
    ### Release note
    
    Release the destination compute group when cloud warm-up initialization
    fails, allowing subsequent warm-up jobs to proceed. Report the
    initialization error and preserve periodic retry scheduling.
---
 .../org/apache/doris/cloud/CloudWarmUpJob.java     | 42 ++++++----
 .../org/apache/doris/cloud/CloudWarmUpJobTest.java | 96 ++++++++++++++++++++++
 2 files changed, 121 insertions(+), 17 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/cloud/CloudWarmUpJob.java 
b/fe/fe-core/src/main/java/org/apache/doris/cloud/CloudWarmUpJob.java
index e2f58cc49b0..6fbe6188d5e 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/cloud/CloudWarmUpJob.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/cloud/CloudWarmUpJob.java
@@ -932,23 +932,31 @@ public class CloudWarmUpJob implements Writable {
             return;
         }
 
-        // Todo: nothing to prepare yet
-        this.setJobDone = false;
-        this.lastBatchId = -1;
-        this.startTimeMs = System.currentTimeMillis();
-        // reset clients to ensure we have the latest BE info
-        this.beToThriftAddress = null;
-        this.beToClient = null;
-        this.beToAddr = null;
-        
MetricRepo.updateClusterWarmUpJobLatestStartTime(String.valueOf(jobId), 
srcClusterName,
-                dstClusterName, startTimeMs);
-        this.fetchBeToTabletIdBatches();
-        long totalTablets = beToTabletIdBatches.values().stream()
-                .flatMap(List::stream)
-                .mapToLong(List::size)
-                .sum();
-        MetricRepo.increaseClusterWarmUpJobRequestedTablets(dstClusterName, 
totalTablets);
-        MetricRepo.increaseClusterWarmUpJobExecCount(dstClusterName);
+        long totalTablets;
+        try {
+            this.setJobDone = false;
+            this.lastBatchId = -1;
+            this.startTimeMs = System.currentTimeMillis();
+            // reset clients to ensure we have the latest BE info
+            this.beToThriftAddress = null;
+            this.beToClient = null;
+            this.beToAddr = null;
+            
MetricRepo.updateClusterWarmUpJobLatestStartTime(String.valueOf(jobId), 
srcClusterName,
+                    dstClusterName, startTimeMs);
+            this.fetchBeToTabletIdBatches();
+            totalTablets = beToTabletIdBatches.values().stream()
+                    .flatMap(List::stream)
+                    .mapToLong(List::size)
+                    .sum();
+            
MetricRepo.increaseClusterWarmUpJobRequestedTablets(dstClusterName, 
totalTablets);
+            MetricRepo.increaseClusterWarmUpJobExecCount(dstClusterName);
+        } catch (Exception e) {
+            LOG.warn("failed to initialize cloud warm up job {}", jobId, e);
+            // No BE job has started. Reuse cancellation to release the 
destination registration
+            // and preserve periodic jobs for their next scheduled attempt.
+            cancel("Failed to initialize warm up job: " + e.getMessage(), 
false);
+            return;
+        }
         this.jobState = JobState.RUNNING;
         Env.getCurrentEnv().getEditLog().logModifyCloudWarmUpJob(this);
         LOG.info("warmup-lock state-transition jobId={} srcCluster={} 
dstCluster={} syncMode={} jobType={} "
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/cloud/CloudWarmUpJobTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/cloud/CloudWarmUpJobTest.java
index dce7f8e675d..f54fd9dfba3 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/cloud/CloudWarmUpJobTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/cloud/CloudWarmUpJobTest.java
@@ -41,6 +41,8 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
 import org.mockito.ArgumentCaptor;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
@@ -56,6 +58,7 @@ import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.atomic.AtomicReference;
 
 public class CloudWarmUpJobTest {
@@ -186,6 +189,88 @@ public class CloudWarmUpJobTest {
         Mockito.verify(editLog).logModifyCloudWarmUpJob(job);
     }
 
+    @ParameterizedTest
+    @EnumSource(value = SyncMode.class, names = {"ONCE", "PERIODIC"})
+    public void 
testPendingInitializationFailureReleasesDestinationLock(SyncMode syncMode) 
throws Exception {
+        CloudWarmUpJob job = Mockito.spy(createPendingJob(204L, syncMode));
+        CloudWarmUpJob nextJob = createPendingJob(205L, SyncMode.ONCE);
+        CloudEnv cloudEnv = Mockito.mock(CloudEnv.class);
+        CacheHotspotManager manager = new 
CacheHotspotManager(Mockito.mock(CloudSystemInfoService.class),
+                Mockito.mock(ThreadPoolExecutor.class));
+        EditLog editLog = Mockito.mock(EditLog.class);
+        Mockito.when(cloudEnv.getCacheHotspotMgr()).thenReturn(manager);
+        Mockito.when(cloudEnv.getEditLog()).thenReturn(editLog);
+        Mockito.doAnswer(invocation -> {
+            Assertions.assertFalse(manager.tryRegisterRunningJob(nextJob));
+            throw new IllegalStateException("initialization failed");
+        }).when(job).fetchBeToTabletIdBatches();
+
+        try (MockedStatic<Env> mockedEnv = Mockito.mockStatic(Env.class)) {
+            mockedEnv.when(Env::getCurrentEnv).thenReturn(cloudEnv);
+            job.run();
+
+            Assertions.assertTrue(manager.tryRegisterRunningJob(nextJob));
+            Assertions.assertEquals(syncMode == SyncMode.ONCE ? 
JobState.CANCELLED : JobState.PENDING,
+                    job.getJobState());
+            Assertions.assertEquals("Failed to initialize warm up job: 
initialization failed", job.getErrMsg());
+            Assertions.assertTrue(job.getStartTimeMs() > 0);
+            Assertions.assertTrue(job.getFinishedTimeMs() >= 
job.getStartTimeMs());
+            Assertions.assertEquals(syncMode == SyncMode.PERIODIC, 
job.shouldWait());
+            Mockito.verify(editLog).logModifyCloudWarmUpJob(job);
+
+            CloudWarmUpJob persistedJob = copyBySerialization(job);
+            Assertions.assertEquals(job.getJobState(), 
persistedJob.getJobState());
+            Assertions.assertEquals(job.getErrMsg(), persistedJob.getErrMsg());
+            Assertions.assertEquals(job.getStartTimeMs(), 
persistedJob.getStartTimeMs());
+            Assertions.assertEquals(job.getFinishedTimeMs(), 
persistedJob.getFinishedTimeMs());
+
+            nextJob.run();
+            Assertions.assertEquals(JobState.RUNNING, nextJob.getJobState());
+            Mockito.verifyNoInteractions(mockBackendPool);
+
+            if (syncMode == SyncMode.PERIODIC) {
+                manager.notifyJobStop(nextJob);
+                
Mockito.doCallRealMethod().when(job).fetchBeToTabletIdBatches();
+                setStartTimeMs(job, System.currentTimeMillis() - 61_000L);
+                Assertions.assertFalse(job.shouldWait());
+                job.run();
+                Assertions.assertEquals(JobState.RUNNING, job.getJobState());
+                Assertions.assertFalse(manager.tryRegisterRunningJob(nextJob));
+            } else {
+                job.run();
+                Mockito.verify(job).fetchBeToTabletIdBatches();
+                Assertions.assertEquals(JobState.CANCELLED, job.getJobState());
+            }
+        }
+    }
+
+    @ParameterizedTest
+    @EnumSource(value = SyncMode.class, names = {"ONCE", "PERIODIC"})
+    public void 
testPendingInitializationKeepsDestinationLockOnSuccess(SyncMode syncMode) {
+        CloudWarmUpJob job = createPendingJob(206L, syncMode);
+        CloudWarmUpJob nextJob = Mockito.spy(createPendingJob(207L, 
SyncMode.ONCE));
+        CloudEnv cloudEnv = Mockito.mock(CloudEnv.class);
+        CacheHotspotManager manager = new 
CacheHotspotManager(Mockito.mock(CloudSystemInfoService.class),
+                Mockito.mock(ThreadPoolExecutor.class));
+        EditLog editLog = Mockito.mock(EditLog.class);
+        Mockito.when(cloudEnv.getCacheHotspotMgr()).thenReturn(manager);
+        Mockito.when(cloudEnv.getEditLog()).thenReturn(editLog);
+
+        try (MockedStatic<Env> mockedEnv = Mockito.mockStatic(Env.class)) {
+            mockedEnv.when(Env::getCurrentEnv).thenReturn(cloudEnv);
+            job.run();
+            Assertions.assertEquals(JobState.RUNNING, job.getJobState());
+
+            nextJob.run();
+            Assertions.assertEquals(JobState.PENDING, nextJob.getJobState());
+            Assertions.assertEquals(-1L, nextJob.getStartTimeMs());
+            Assertions.assertFalse(manager.tryRegisterRunningJob(nextJob));
+            Mockito.verify(nextJob, 
Mockito.never()).fetchBeToTabletIdBatches();
+            Mockito.verify(editLog, 
Mockito.never()).logModifyCloudWarmUpJob(nextJob);
+            Mockito.verify(editLog).logModifyCloudWarmUpJob(job);
+        }
+    }
+
     @Test
     public void testEventDrivenSuccessfulRetryClearsErrMsg() throws Exception {
         CloudSystemInfoService cloudSystemInfoService = 
Mockito.mock(CloudSystemInfoService.class);
@@ -325,6 +410,17 @@ public class CloudWarmUpJobTest {
         Mockito.verify(mockBackendPool).returnObject(address, client);
     }
 
+    private CloudWarmUpJob createPendingJob(long jobId, SyncMode syncMode) {
+        return new CloudWarmUpJob.Builder()
+                .setJobId(jobId)
+                .setSrcClusterName("source_cluster")
+                .setDstClusterName("target_cluster")
+                .setJobType(JobType.CLUSTER)
+                .setSyncMode(syncMode)
+                .setSyncInterval(60L)
+                .build();
+    }
+
     private CloudWarmUpJob createRunningJob(long jobId, TNetworkAddress 
firstAddress,
             TNetworkAddress secondAddress) {
         CloudWarmUpJob job = new CloudWarmUpJob.Builder()


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

Reply via email to