github-actions[bot] commented on code in PR #66312:
URL: https://github.com/apache/doris/pull/66312#discussion_r3694583403
##########
fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileManagerTest.java:
##########
@@ -1207,4 +1210,175 @@ void testArchivePendingTimeoutIntegration() throws
Exception {
Config.profile_archive_batch_size = originalBatchSize;
}
}
+
+ @Test
+ public void testOnlyOneProfileLoaderWhenTriggeredConcurrently() throws
Exception {
+ int numProfiles = 30;
+ for (int i = 0; i < numProfiles; i++) {
+ UUID taskId = UUID.randomUUID();
+ TUniqueId queryId = new TUniqueId(taskId.getMostSignificantBits(),
taskId.getLeastSignificantBits());
+ String profileId = DebugUtil.printId(queryId);
+ Profile profile = constructProfile(profileId);
+ profile.writeToStorage(ProfileManager.PROFILE_STORAGE_PATH);
+ }
+
+ profileManager.isProfileLoaded = false;
+
+ AtomicInteger maxProfileLoaderThreads = new AtomicInteger(0);
+ Thread monitorThread = new Thread(() -> {
+ while (!Thread.currentThread().isInterrupted()) {
+ long loaderCount = Thread.getAllStackTraces().keySet().stream()
Review Comment:
[P2] Make the loader-count assertion deterministic
This samples all JVM threads by a shared name every 10 ms without holding
the tested loader active. A fast load can start and finish between samples
(leaving the maximum at zero), while a loader left by another `ProfileManager`
can inflate the count; brief overlaps can also be missed. Instrument the load
entry with an invocation counter and entry/release latches, hold the winner
while all trigger calls finish, and assert one entry scoped to this manager
instead of sampling global thread names.
##########
fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileManagerTest.java:
##########
@@ -1207,4 +1210,175 @@ void testArchivePendingTimeoutIntegration() throws
Exception {
Config.profile_archive_batch_size = originalBatchSize;
}
}
+
+ @Test
+ public void testOnlyOneProfileLoaderWhenTriggeredConcurrently() throws
Exception {
+ int numProfiles = 30;
+ for (int i = 0; i < numProfiles; i++) {
+ UUID taskId = UUID.randomUUID();
+ TUniqueId queryId = new TUniqueId(taskId.getMostSignificantBits(),
taskId.getLeastSignificantBits());
+ String profileId = DebugUtil.printId(queryId);
+ Profile profile = constructProfile(profileId);
+ profile.writeToStorage(ProfileManager.PROFILE_STORAGE_PATH);
+ }
+
+ profileManager.isProfileLoaded = false;
+
+ AtomicInteger maxProfileLoaderThreads = new AtomicInteger(0);
+ Thread monitorThread = new Thread(() -> {
+ while (!Thread.currentThread().isInterrupted()) {
+ long loaderCount = Thread.getAllStackTraces().keySet().stream()
+ .filter(thread ->
"profile-loader".equals(thread.getName()))
+ .count();
+ maxProfileLoaderThreads.updateAndGet(cur -> Math.max(cur,
(int) loaderCount));
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ });
+ monitorThread.start();
+
+ int concurrentTriggers = 20;
+ CountDownLatch startLatch = new CountDownLatch(1);
+ Thread[] triggerThreads = new Thread[concurrentTriggers];
+ for (int i = 0; i < concurrentTriggers; i++) {
+ triggerThreads[i] = new Thread(() -> {
+ try {
+ startLatch.await();
+ profileManager.loadProfilesFromStorageIfFirstTime(false);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ });
+ triggerThreads[i].start();
+ }
+ startLatch.countDown();
+
+ for (Thread triggerThread : triggerThreads) {
+ triggerThread.join(30000);
+ }
+
+ long waitStart = System.currentTimeMillis();
+ while (!profileManager.isProfileLoaded && System.currentTimeMillis() -
waitStart < 30000) {
+ Thread.sleep(100);
+ }
+
+ monitorThread.interrupt();
+ monitorThread.join();
+
+ Assertions.assertTrue(profileManager.isProfileLoaded);
+ Assertions.assertEquals(1, maxProfileLoaderThreads.get(),
+ "Only one profile-loader thread should be active at a time");
+ Assertions.assertEquals(numProfiles,
profileManager.queryIdToProfileMap.size());
+ }
+
+ @Test
+ void testSyncLoadWaitsWhenAsyncLoadInProgress() throws Exception {
+ CountDownLatch loadingEntered = new CountDownLatch(1);
+ CountDownLatch allowComplete = new CountDownLatch(1);
+ ProfileManager pm = new ProfileManager() {
+ @Override
+ protected List<String> getOnStorageProfileInfos() {
+ loadingEntered.countDown();
+ try {
+ if (!allowComplete.await(30, TimeUnit.SECONDS)) {
+ throw new RuntimeException("timed out waiting to
complete profile load");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ return super.getOnStorageProfileInfos();
+ }
+ };
+ pm.isProfileLoaded = false;
+
+ Thread asyncLoader = new Thread(() ->
pm.loadProfilesFromStorageIfFirstTime(false));
+ asyncLoader.start();
+ Assertions.assertTrue(loadingEntered.await(10, TimeUnit.SECONDS));
+
+ long waitStart = System.currentTimeMillis();
+ pm.loadProfilesFromStorageIfFirstTime(true);
+ long waitedMs = System.currentTimeMillis() - waitStart;
+
+ allowComplete.countDown();
+ asyncLoader.join(10000);
+
+ Assertions.assertTrue(pm.isProfileLoaded);
+ Assertions.assertTrue(waitedMs >= 50,
+ "sync load should wait for in-progress async load to finish");
+ }
+
+ @Test
+ void testLoadProfilesFromStorageFailure() throws Exception {
+ Profile profile = constructProfile("fail-load");
+ profile.writeToStorage(ProfileManager.PROFILE_STORAGE_PATH);
+
+ ProfileManager pm = new ProfileManager() {
+ @Override
+ public void pushProfile(Profile profile) {
+ throw new RuntimeException("simulated load failure");
+ }
+ };
+ pm.isProfileLoaded = false;
+
+ pm.loadProfilesFromStorageIfFirstTime(true);
+
+ Assertions.assertTrue(pm.isProfileLoaded);
+ Assertions.assertTrue(pm.queryIdToProfileMap.isEmpty());
+ }
+
+ @Test
+ void testReadProfileIOExceptionInBatch() throws Exception {
+ ProfileManager pm = new ProfileManager();
+ pm.isProfileLoaded = false;
+
+ // Malformed (non-zip) profile files: real Profile.read tolerates them
and returns null,
+ // so the batch load must still finish and mark isProfileLoaded=true
without loading any.
+ for (int i = 0; i < 3; i++) {
+ File profileFile = new File(tempDir, System.currentTimeMillis() +
"_badprofile" + i);
+ profileFile.createNewFile();
+ }
+
+ pm.loadProfilesFromStorageIfFirstTime(true);
+
+ Assertions.assertTrue(pm.isProfileLoaded);
+ Assertions.assertEquals(0, pm.queryIdToProfileMap.size());
+ }
+
+ @Test
+ void testWaitForProfileLoadInterrupted() throws Exception {
+ CountDownLatch loadingEntered = new CountDownLatch(1);
+ CountDownLatch holdLoad = new CountDownLatch(1);
+ ProfileManager pm = new ProfileManager() {
+ @Override
+ protected List<String> getOnStorageProfileInfos() {
+ loadingEntered.countDown();
+ try {
+ holdLoad.await(30, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ return super.getOnStorageProfileInfos();
+ }
+ };
+ pm.isProfileLoaded = false;
+
+ Thread asyncLoader = new Thread(() ->
pm.loadProfilesFromStorageIfFirstTime(false));
+ asyncLoader.start();
+ Assertions.assertTrue(loadingEntered.await(10, TimeUnit.SECONDS));
+
+ Thread waiterThread = new Thread(() ->
pm.loadProfilesFromStorageIfFirstTime(true));
+ waiterThread.start();
+ Thread.sleep(200);
+ waiterThread.interrupt();
+ waiterThread.join(5000);
+
+ Assertions.assertFalse(waiterThread.isAlive());
+
+ holdLoad.countDown();
+ asyncLoader.join(10000);
Review Comment:
[P2] Wait for the actual loader before test teardown
`asyncLoader` only invokes the asynchronous API; it returns as soon as that
API starts the separate `profile-loader` thread. Joining `asyncLoader`
therefore does not wait for the loader released by `holdLoad.countDown()`. The
test can finish while that loader is still entering
`super.getOnStorageProfileInfos()`, and `@AfterEach` can restore the static
storage path/delete the temp directory underneath it (or leave it visible to
another loader-count test). Wait on a completion latch or the loader's final
state after releasing `holdLoad` before returning.
##########
fe/fe-core/src/main/java/org/apache/doris/common/profile/ProfileManager.java:
##########
@@ -602,78 +605,104 @@ protected void
loadProfilesFromStorageIfFirstTime(boolean sync) {
if (checkIfProfileLoaded()) {
return;
}
+ if (!isProfileLoading.compareAndSet(false, true)) {
+ if (sync) {
+ waitForProfileLoadFinish();
+ }
+ return;
+ }
- // Create a new thread to load profiles
- Thread loadThread = new Thread(() -> {
+ Runnable loadTask = () -> {
long startTime = System.currentTimeMillis();
-
+ boolean loadSucceeded = false;
try {
- List<String> profileDirAbsPaths = getOnStorageProfileInfos();
- LOG.info("Reading {} profiles from {}",
profileDirAbsPaths.size(),
- PROFILE_STORAGE_PATH);
- // Newest profile first
- profileDirAbsPaths.sort(Collections.reverseOrder());
-
- // Process profiles in batches
- for (int i = 0; i < profileDirAbsPaths.size(); i +=
BATCH_SIZE) {
- // Thread safe list
- List<Profile> profiles = Collections.synchronizedList(new
ArrayList<>());
- int end = Math.min(i + BATCH_SIZE,
profileDirAbsPaths.size());
- List<String> batch = profileDirAbsPaths.subList(i, end);
-
- // List of profile io futures for current batch
- List<Future<?>> profileIOFutures = Lists.newArrayList();
-
- // Create and add tasks for current batch to executor
- for (String profileDirAbsPath : batch) {
- Thread thread = new Thread(() -> {
- Profile profile = Profile.read(profileDirAbsPath);
- if (profile != null) {
- profiles.add(profile);
- }
- });
- profileIOFutures.add(profileIOExecutor.submit(thread));
- }
+ loadProfilesFromStorage();
+ loadSucceeded = true;
+ LOG.info("Load profiles into memory finished, costs {}ms",
+ System.currentTimeMillis() - startTime);
+ } catch (Exception e) {
+ LOG.error("Failed to load query profile from storage", e);
+ } finally {
+ // Mark loaded even on failure to avoid spawning a new
profile-loader every second.
+ markProfileLoaded();
+ isProfileLoading.set(false);
+ if (!loadSucceeded) {
+ LOG.warn("Profile loading did not complete successfully, "
+ + "will not retry until FE restarts. Loaded
profile count in memory: {}",
+ queryIdToProfileMap.size());
+ }
+ }
+ };
- // Wait for all futures in current batch to complete
- for (Future<?> future : profileIOFutures) {
- try {
- future.get();
- } catch (Exception e) {
- LOG.warn("Failed to read profile from storage", e);
- }
- }
+ if (sync) {
+ loadTask.run();
+ } else {
+ Thread loadThread = new Thread(loadTask, "profile-loader");
+ loadThread.setDaemon(true);
+ loadThread.start();
Review Comment:
[P2] Release loader ownership if thread launch fails
`isProfileLoading` is set before constructing/starting this thread, but its
only reset is inside `loadTask`. If native-thread allocation or `start()` fails
under the resource pressure this change targets, the task never runs;
`Daemon.run()` catches the thrown `Throwable` and continues, while every later
cycle sees `isProfileLoading=true` and permanently skips the cold load. Guard
the launch so failure resets/transitions the ownership state before propagating
or recording a terminal failure.
##########
fe/fe-core/src/main/java/org/apache/doris/common/profile/ProfileManager.java:
##########
@@ -602,78 +605,104 @@ protected void
loadProfilesFromStorageIfFirstTime(boolean sync) {
if (checkIfProfileLoaded()) {
return;
}
+ if (!isProfileLoading.compareAndSet(false, true)) {
+ if (sync) {
+ waitForProfileLoadFinish();
+ }
+ return;
+ }
- // Create a new thread to load profiles
- Thread loadThread = new Thread(() -> {
+ Runnable loadTask = () -> {
long startTime = System.currentTimeMillis();
-
+ boolean loadSucceeded = false;
try {
- List<String> profileDirAbsPaths = getOnStorageProfileInfos();
- LOG.info("Reading {} profiles from {}",
profileDirAbsPaths.size(),
- PROFILE_STORAGE_PATH);
- // Newest profile first
- profileDirAbsPaths.sort(Collections.reverseOrder());
-
- // Process profiles in batches
- for (int i = 0; i < profileDirAbsPaths.size(); i +=
BATCH_SIZE) {
- // Thread safe list
- List<Profile> profiles = Collections.synchronizedList(new
ArrayList<>());
- int end = Math.min(i + BATCH_SIZE,
profileDirAbsPaths.size());
- List<String> batch = profileDirAbsPaths.subList(i, end);
-
- // List of profile io futures for current batch
- List<Future<?>> profileIOFutures = Lists.newArrayList();
-
- // Create and add tasks for current batch to executor
- for (String profileDirAbsPath : batch) {
- Thread thread = new Thread(() -> {
- Profile profile = Profile.read(profileDirAbsPath);
- if (profile != null) {
- profiles.add(profile);
- }
- });
- profileIOFutures.add(profileIOExecutor.submit(thread));
- }
+ loadProfilesFromStorage();
+ loadSucceeded = true;
+ LOG.info("Load profiles into memory finished, costs {}ms",
+ System.currentTimeMillis() - startTime);
+ } catch (Exception e) {
+ LOG.error("Failed to load query profile from storage", e);
+ } finally {
+ // Mark loaded even on failure to avoid spawning a new
profile-loader every second.
+ markProfileLoaded();
Review Comment:
[P1] Keep failed loads from enabling disk cleanup
`isProfileLoaded` is also the safety gate in `deleteBrokenProfiles()`: once
it is true, every stored profile missing from `queryIdToProfileMap` is
classified as broken and deleted. If this load fails before indexing every
file—for example when the bounded executor's `BlockedPolicy` times out, or when
`pushProfile()` throws as the new test intends to simulate—this `finally` still
makes the partial map authoritative. The same or next `runAfterCatalogReady()`
cycle can then delete every valid unread profile. Please keep failed/attempted
state separate from the “disk index is complete” state so failures can suppress
loader proliferation without enabling destructive cleanup.
##########
fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileManagerTest.java:
##########
@@ -1207,4 +1210,175 @@ void testArchivePendingTimeoutIntegration() throws
Exception {
Config.profile_archive_batch_size = originalBatchSize;
}
}
+
+ @Test
+ public void testOnlyOneProfileLoaderWhenTriggeredConcurrently() throws
Exception {
+ int numProfiles = 30;
+ for (int i = 0; i < numProfiles; i++) {
+ UUID taskId = UUID.randomUUID();
+ TUniqueId queryId = new TUniqueId(taskId.getMostSignificantBits(),
taskId.getLeastSignificantBits());
+ String profileId = DebugUtil.printId(queryId);
+ Profile profile = constructProfile(profileId);
+ profile.writeToStorage(ProfileManager.PROFILE_STORAGE_PATH);
+ }
+
+ profileManager.isProfileLoaded = false;
+
+ AtomicInteger maxProfileLoaderThreads = new AtomicInteger(0);
+ Thread monitorThread = new Thread(() -> {
+ while (!Thread.currentThread().isInterrupted()) {
+ long loaderCount = Thread.getAllStackTraces().keySet().stream()
+ .filter(thread ->
"profile-loader".equals(thread.getName()))
+ .count();
+ maxProfileLoaderThreads.updateAndGet(cur -> Math.max(cur,
(int) loaderCount));
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ });
+ monitorThread.start();
+
+ int concurrentTriggers = 20;
+ CountDownLatch startLatch = new CountDownLatch(1);
+ Thread[] triggerThreads = new Thread[concurrentTriggers];
+ for (int i = 0; i < concurrentTriggers; i++) {
+ triggerThreads[i] = new Thread(() -> {
+ try {
+ startLatch.await();
+ profileManager.loadProfilesFromStorageIfFirstTime(false);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ });
+ triggerThreads[i].start();
+ }
+ startLatch.countDown();
+
+ for (Thread triggerThread : triggerThreads) {
+ triggerThread.join(30000);
+ }
+
+ long waitStart = System.currentTimeMillis();
+ while (!profileManager.isProfileLoaded && System.currentTimeMillis() -
waitStart < 30000) {
+ Thread.sleep(100);
+ }
+
+ monitorThread.interrupt();
+ monitorThread.join();
+
+ Assertions.assertTrue(profileManager.isProfileLoaded);
+ Assertions.assertEquals(1, maxProfileLoaderThreads.get(),
+ "Only one profile-loader thread should be active at a time");
+ Assertions.assertEquals(numProfiles,
profileManager.queryIdToProfileMap.size());
+ }
+
+ @Test
+ void testSyncLoadWaitsWhenAsyncLoadInProgress() throws Exception {
+ CountDownLatch loadingEntered = new CountDownLatch(1);
+ CountDownLatch allowComplete = new CountDownLatch(1);
+ ProfileManager pm = new ProfileManager() {
+ @Override
+ protected List<String> getOnStorageProfileInfos() {
+ loadingEntered.countDown();
+ try {
+ if (!allowComplete.await(30, TimeUnit.SECONDS)) {
+ throw new RuntimeException("timed out waiting to
complete profile load");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ return super.getOnStorageProfileInfos();
+ }
+ };
+ pm.isProfileLoaded = false;
+
+ Thread asyncLoader = new Thread(() ->
pm.loadProfilesFromStorageIfFirstTime(false));
+ asyncLoader.start();
+ Assertions.assertTrue(loadingEntered.await(10, TimeUnit.SECONDS));
+
+ long waitStart = System.currentTimeMillis();
+ pm.loadProfilesFromStorageIfFirstTime(true);
Review Comment:
[P2] Release the loader before waiting synchronously
This call blocks until the async loader finishes, but that loader is waiting
on `allowComplete`, which this test only opens after this call returns. Normal
completion is therefore impossible: after about 30 seconds the loader's await
times out and throws, the production failure path marks it loaded, and only
then does this assertion sequence continue. Run the synchronous caller in a
separate thread/future, assert it remains blocked, release `allowComplete`, and
join it so the test covers a successful handoff without a timeout-driven pass.
##########
fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileManagerTest.java:
##########
@@ -1207,4 +1210,175 @@ void testArchivePendingTimeoutIntegration() throws
Exception {
Config.profile_archive_batch_size = originalBatchSize;
}
}
+
+ @Test
+ public void testOnlyOneProfileLoaderWhenTriggeredConcurrently() throws
Exception {
+ int numProfiles = 30;
+ for (int i = 0; i < numProfiles; i++) {
+ UUID taskId = UUID.randomUUID();
+ TUniqueId queryId = new TUniqueId(taskId.getMostSignificantBits(),
taskId.getLeastSignificantBits());
+ String profileId = DebugUtil.printId(queryId);
+ Profile profile = constructProfile(profileId);
+ profile.writeToStorage(ProfileManager.PROFILE_STORAGE_PATH);
+ }
+
+ profileManager.isProfileLoaded = false;
+
+ AtomicInteger maxProfileLoaderThreads = new AtomicInteger(0);
+ Thread monitorThread = new Thread(() -> {
+ while (!Thread.currentThread().isInterrupted()) {
+ long loaderCount = Thread.getAllStackTraces().keySet().stream()
+ .filter(thread ->
"profile-loader".equals(thread.getName()))
+ .count();
+ maxProfileLoaderThreads.updateAndGet(cur -> Math.max(cur,
(int) loaderCount));
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ });
+ monitorThread.start();
+
+ int concurrentTriggers = 20;
+ CountDownLatch startLatch = new CountDownLatch(1);
+ Thread[] triggerThreads = new Thread[concurrentTriggers];
+ for (int i = 0; i < concurrentTriggers; i++) {
+ triggerThreads[i] = new Thread(() -> {
+ try {
+ startLatch.await();
+ profileManager.loadProfilesFromStorageIfFirstTime(false);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ });
+ triggerThreads[i].start();
+ }
+ startLatch.countDown();
+
+ for (Thread triggerThread : triggerThreads) {
+ triggerThread.join(30000);
+ }
+
+ long waitStart = System.currentTimeMillis();
+ while (!profileManager.isProfileLoaded && System.currentTimeMillis() -
waitStart < 30000) {
+ Thread.sleep(100);
+ }
+
+ monitorThread.interrupt();
+ monitorThread.join();
+
+ Assertions.assertTrue(profileManager.isProfileLoaded);
+ Assertions.assertEquals(1, maxProfileLoaderThreads.get(),
+ "Only one profile-loader thread should be active at a time");
+ Assertions.assertEquals(numProfiles,
profileManager.queryIdToProfileMap.size());
+ }
+
+ @Test
+ void testSyncLoadWaitsWhenAsyncLoadInProgress() throws Exception {
+ CountDownLatch loadingEntered = new CountDownLatch(1);
+ CountDownLatch allowComplete = new CountDownLatch(1);
+ ProfileManager pm = new ProfileManager() {
+ @Override
+ protected List<String> getOnStorageProfileInfos() {
+ loadingEntered.countDown();
+ try {
+ if (!allowComplete.await(30, TimeUnit.SECONDS)) {
+ throw new RuntimeException("timed out waiting to
complete profile load");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ return super.getOnStorageProfileInfos();
+ }
+ };
+ pm.isProfileLoaded = false;
+
+ Thread asyncLoader = new Thread(() ->
pm.loadProfilesFromStorageIfFirstTime(false));
+ asyncLoader.start();
+ Assertions.assertTrue(loadingEntered.await(10, TimeUnit.SECONDS));
+
+ long waitStart = System.currentTimeMillis();
+ pm.loadProfilesFromStorageIfFirstTime(true);
+ long waitedMs = System.currentTimeMillis() - waitStart;
+
+ allowComplete.countDown();
+ asyncLoader.join(10000);
+
+ Assertions.assertTrue(pm.isProfileLoaded);
+ Assertions.assertTrue(waitedMs >= 50,
+ "sync load should wait for in-progress async load to finish");
+ }
+
+ @Test
+ void testLoadProfilesFromStorageFailure() throws Exception {
+ Profile profile = constructProfile("fail-load");
Review Comment:
[P2] Make this test reach the injected failure
`constructProfile("fail-load")` is persisted as `<timestamp>_fail-load.zip`,
but `Profile.parseProfileFileName()` accepts only a TUniqueId or a numeric load
ID. `Profile.read()` therefore returns null before `pushProfile()` is called,
so this override never throws and `loadSucceeded` remains true. Use a valid
profile ID and assert that the override was entered; the current assertions
pass on an unreadable-file success path rather than testing the outer load
failure.
##########
fe/fe-core/src/main/java/org/apache/doris/common/profile/ProfileManager.java:
##########
@@ -602,78 +605,104 @@ protected void
loadProfilesFromStorageIfFirstTime(boolean sync) {
if (checkIfProfileLoaded()) {
return;
}
+ if (!isProfileLoading.compareAndSet(false, true)) {
Review Comment:
[P2] Recheck completion after acquiring loader ownership
The loaded check and this CAS are separate state transitions. A later daemon
cycle can read `isProfileLoaded=false` while loader A is finishing, pause here,
then resume after A has marked loaded and cleared `isProfileLoading`; this CAS
succeeds and starts a second full cold load even though loading already
completed. Recheck `isProfileLoaded` after winning ownership (and release
ownership if it is now true), or represent these states in one atomic state
machine so a stale pre-CAS read cannot restart the load.
--
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]