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

HappenLee 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 ce6c03a95c6 [Fix](profile) Prevent concurrent historical profile 
loaders (#67012)
ce6c03a95c6 is described below

commit ce6c03a95c6812c1683c9762a19f57bffdaf1f29
Author: linrrarity <[email protected]>
AuthorDate: Mon Aug 24 11:21:06 2026 +0800

    [Fix](profile) Prevent concurrent historical profile loaders (#67012)
    
    ### What problem does this PR solve?
    
    Problem Summary:
    
    `ProfileManager` periodically calls
    `loadProfilesFromStorageIfFirstTime(false)` after FE becomes ready.
    The method previously tracked only whether historical profiles had
    finished loading. Because the storage scan runs asynchronously, repeated
    calls made before the first scan completed could each create a new
    `profile-loader` thread. This resulted in concurrent and duplicated
    profile storage scans.
    
    This PR adds an atomic loading flag to ensure that only one historical
    profile loader can run at a time:
    
    - Atomically acquire the loading state before starting the loader.
    - Ignore repeated calls while a loader is running.
    - Clear the loading state after success or failure so failed loads can
    be retried.
    - Recheck the loaded state after acquiring the flag to handle the race
    where the previous loader finishes between the initial check and the CAS
    operation.
    
    The existing loading logic and synchronous waiting behavior remain
    unchanged.
---
 .../doris/common/profile/ProfileManager.java       | 58 +++++++---------
 .../doris/common/profile/ProfileManagerTest.java   | 81 +++++++++++++++++++---
 2 files changed, 97 insertions(+), 42 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/common/profile/ProfileManager.java 
b/fe/fe-core/src/main/java/org/apache/doris/common/profile/ProfileManager.java
index 927d5863d5a..232096be42b 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/common/profile/ProfileManager.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/common/profile/ProfileManager.java
@@ -56,6 +56,7 @@ import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
@@ -122,10 +123,14 @@ public class ProfileManager extends MasterDaemon {
         }
     }
 
-    // this variable is assigned to true the first time the profile is loaded 
from storage
-    // no further write operation, so no data race
-    private final ReentrantReadWriteLock isProfileLoadedLock = new 
ReentrantReadWriteLock();
-    volatile boolean isProfileLoaded = false;
+    enum ProfileLoadStatus {
+        UNLOADED,
+        LOADING,
+        LOADED
+    }
+
+    final AtomicReference<ProfileLoadStatus> profileLoadStatus =
+            new AtomicReference<>(ProfileLoadStatus.UNLOADED);
 
     // only protect queryIdDeque; queryIdToProfileMap is concurrent, no need 
to protect
     private ReentrantReadWriteLock lock;
@@ -573,23 +578,21 @@ public class ProfileManager extends MasterDaemon {
     // string will contain profile id and its storage timestamp
     protected List<String> getOnStorageProfileInfos() {
         List<String> res = Lists.newArrayList();
-        try {
-            File profileDir = new File(PROFILE_STORAGE_PATH);
-            if (!profileDir.exists()) {
-                LOG.warn("Profile storage directory {} does not exist", 
PROFILE_STORAGE_PATH);
-                return res;
-            }
+        File profileDir = new File(PROFILE_STORAGE_PATH);
+        if (!profileDir.exists()) {
+            LOG.warn("Profile storage directory {} does not exist", 
PROFILE_STORAGE_PATH);
+            return res;
+        }
 
-            File[] files = profileDir.listFiles();
-            if (files != null) {
-                for (File file : files) {
-                    if (file.isFile()) {
-                        res.add(file.getAbsolutePath());
-                    }
+        File[] files = profileDir.listFiles();
+        if (files != null) {
+            for (File file : files) {
+                if (file.isFile()) {
+                    res.add(file.getAbsolutePath());
                 }
             }
-        } catch (Exception e) {
-            LOG.error("Failed to get profile meta from storage", e);
+        } else {
+            throw new IllegalStateException("Failed to list profile storage 
directory: " + PROFILE_STORAGE_PATH);
         }
 
         return res;
@@ -599,7 +602,7 @@ public class ProfileManager extends MasterDaemon {
     // deserialize to an object Profile
     // push them to memory structure of ProfileManager for index
     protected void loadProfilesFromStorageIfFirstTime(boolean sync) {
-        if (checkIfProfileLoaded()) {
+        if (!profileLoadStatus.compareAndSet(ProfileLoadStatus.UNLOADED, 
ProfileLoadStatus.LOADING)) {
             return;
         }
 
@@ -653,15 +656,11 @@ public class ProfileManager extends MasterDaemon {
 
                 LOG.info("Load profiles into memory finished, costs {}ms", 
System.currentTimeMillis() - startTime);
 
-                // Set isProfileLoaded to true with write lock
-                isProfileLoadedLock.writeLock().lock();
-                try {
-                    this.isProfileLoaded = true;
-                } finally {
-                    isProfileLoadedLock.writeLock().unlock();
-                }
+                profileLoadStatus.set(ProfileLoadStatus.LOADED);
             } catch (Exception e) {
                 LOG.error("Failed to load query profile from storage", e);
+            } finally {
+                profileLoadStatus.compareAndSet(ProfileLoadStatus.LOADING, 
ProfileLoadStatus.UNLOADED);
             }
         });
 
@@ -1119,12 +1118,7 @@ public class ProfileManager extends MasterDaemon {
     }
 
     private boolean checkIfProfileLoaded() {
-        isProfileLoadedLock.readLock().lock();
-        try {
-            return isProfileLoaded;
-        } finally {
-            isProfileLoadedLock.readLock().unlock();
-        }
+        return profileLoadStatus.get() == ProfileLoadStatus.LOADED;
     }
 
     public void removeProfile(String profileId) {
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileManagerTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileManagerTest.java
index 7c681814f60..8ed48a2d63e 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileManagerTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileManagerTest.java
@@ -19,6 +19,7 @@ package org.apache.doris.common.profile;
 
 import org.apache.doris.common.Config;
 import org.apache.doris.common.profile.ProfileManager.ProfileElement;
+import org.apache.doris.common.profile.ProfileManager.ProfileLoadStatus;
 import org.apache.doris.common.util.DebugUtil;
 import org.apache.doris.planner.PlanFragmentId;
 import org.apache.doris.thrift.TCounter;
@@ -54,7 +55,10 @@ import java.util.PriorityQueue;
 import java.util.Random;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
 @ResourceLock("global")
 class ProfileManagerTest {
@@ -77,7 +81,7 @@ class ProfileManagerTest {
         originalPath = ProfileManager.PROFILE_STORAGE_PATH;
         ProfileManager.PROFILE_STORAGE_PATH = tempDir.getAbsolutePath();
         profileManager.cleanProfile();
-        profileManager.isProfileLoaded = false;
+        profileManager.profileLoadStatus.set(ProfileLoadStatus.UNLOADED);
         originMaxProfiles = Config.max_query_profile_num;
     }
 
@@ -518,7 +522,7 @@ class ProfileManagerTest {
 
     @Test
     void testLoadProfile() throws IOException {
-        profileManager.isProfileLoaded = false;
+        profileManager.profileLoadStatus.set(ProfileLoadStatus.UNLOADED);
 
         try {
             // Create some test profile files
@@ -529,7 +533,7 @@ class ProfileManagerTest {
             }
 
             profileManager.loadProfilesFromStorageIfFirstTime(true);
-            Assertions.assertTrue(profileManager.isProfileLoaded);
+            Assertions.assertEquals(ProfileLoadStatus.LOADED, 
profileManager.profileLoadStatus.get());
             Assertions.assertEquals(30, 
profileManager.queryIdToProfileMap.size());
             Assertions.assertEquals(0, 
profileManager.queryIdToExecutionProfiles.size());
         } catch (InterruptedException e) {
@@ -682,7 +686,7 @@ class ProfileManagerTest {
             }
 
             // Execute deletion
-            profileManager.isProfileLoaded = true;
+            profileManager.profileLoadStatus.set(ProfileLoadStatus.LOADED);
             profileManager.deleteOutdatedProfilesFromStorage();
 
             // Verify correct profiles were deleted
@@ -764,7 +768,7 @@ class ProfileManagerTest {
         }
 
         // Delete broken profiles
-        profileManager.isProfileLoaded = true;
+        profileManager.profileLoadStatus.set(ProfileLoadStatus.LOADED);
         profileManager.deleteBrokenProfiles();
 
         // Verify normal files still exist
@@ -799,6 +803,63 @@ class ProfileManagerTest {
         Assertions.assertEquals(numProfiles, 
profileManager.queryIdToProfileMap.size());
     }
 
+    @Test
+    public void testOnlyOneProfileLoaderCanRun() throws Exception {
+        CountDownLatch loadStarted = new CountDownLatch(1);
+        CountDownLatch duplicateLoadStarted = new CountDownLatch(1);
+        CountDownLatch allowLoadToFinish = new CountDownLatch(1);
+        AtomicInteger scanCount = new AtomicInteger();
+        ProfileManager manager = new ProfileManager() {
+            @Override
+            protected List<String> getOnStorageProfileInfos() {
+                if (scanCount.incrementAndGet() > 1) {
+                    duplicateLoadStarted.countDown();
+                }
+                loadStarted.countDown();
+                try {
+                    allowLoadToFinish.await();
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    throw new RuntimeException(e);
+                }
+                return Lists.newArrayList();
+            }
+        };
+        Thread initialLoad = new Thread(() -> 
manager.loadProfilesFromStorageIfFirstTime(true));
+
+        try {
+            initialLoad.start();
+            Assertions.assertTrue(loadStarted.await(5, TimeUnit.SECONDS));
+
+            for (int i = 0; i < 10; i++) {
+                manager.loadProfilesFromStorageIfFirstTime(false);
+            }
+
+            Assertions.assertFalse(duplicateLoadStarted.await(500, 
TimeUnit.MILLISECONDS));
+            Assertions.assertEquals(1, scanCount.get());
+        } finally {
+            allowLoadToFinish.countDown();
+            initialLoad.join(5000);
+            Assertions.assertFalse(initialLoad.isAlive());
+        }
+    }
+
+    @Test
+    public void testProfileLoaderCanRetryAfterFailure() throws IOException {
+        File invalidProfileStorage = new File(tempDir, "not_a_directory");
+        Assertions.assertTrue(invalidProfileStorage.createNewFile());
+        ProfileManager.PROFILE_STORAGE_PATH = 
invalidProfileStorage.getAbsolutePath();
+        ProfileManager manager = new ProfileManager();
+
+        manager.loadProfilesFromStorageIfFirstTime(true);
+        Assertions.assertEquals(ProfileLoadStatus.UNLOADED, 
manager.profileLoadStatus.get());
+
+        Assertions.assertTrue(invalidProfileStorage.delete());
+        ProfileManager.PROFILE_STORAGE_PATH = tempDir.getAbsolutePath();
+        manager.loadProfilesFromStorageIfFirstTime(true);
+        Assertions.assertEquals(ProfileLoadStatus.LOADED, 
manager.profileLoadStatus.get());
+    }
+
     @Test
     public void testProfileStorageLimit() throws Exception {
         // Set small storage limit
@@ -819,7 +880,7 @@ class ProfileManagerTest {
         }
 
         // Trigger cleanup
-        profileManager.isProfileLoaded = true;
+        profileManager.profileLoadStatus.set(ProfileLoadStatus.LOADED);
         profileManager.deleteOutdatedProfilesFromStorage();
 
         // Verify number of profiles is within limits
@@ -844,7 +905,7 @@ class ProfileManagerTest {
         brokenFile.createNewFile();
 
         // Trigger cleanup
-        profileManager.isProfileLoaded = true;
+        profileManager.profileLoadStatus.set(ProfileLoadStatus.LOADED);
         profileManager.deleteBrokenProfiles();
 
         // Verify broken profile is removed but valid one remains
@@ -1018,7 +1079,7 @@ class ProfileManagerTest {
             }
 
             // Trigger cleanup - should move old profiles to pending and 
possibly archive
-            profileManager.isProfileLoaded = true;
+            profileManager.profileLoadStatus.set(ProfileLoadStatus.LOADED);
             profileManager.deleteOutdatedProfilesFromStorage();
 
             // Verify storage directory only has max allowed profiles
@@ -1086,7 +1147,7 @@ class ProfileManagerTest {
             }
 
             // Trigger cleanup - should directly delete old profiles
-            profileManager.isProfileLoaded = true;
+            profileManager.profileLoadStatus.set(ProfileLoadStatus.LOADED);
             profileManager.deleteOutdatedProfilesFromStorage();
 
             // Verify delete invocations
@@ -1143,7 +1204,7 @@ class ProfileManagerTest {
 
             // Simulate periodic cleanup via runAfterCatalogReady
             // Note: The first call will trigger cleanup since 
lastArchiveCleanupTime is 0
-            profileManager.isProfileLoaded = true;
+            profileManager.profileLoadStatus.set(ProfileLoadStatus.LOADED);
             profileManager.runAfterCatalogReady();
 
             // Verify that the old archive was deleted by runAfterCatalogReady


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

Reply via email to