wenzhenghu commented on code in PR #65126:
URL: https://github.com/apache/doris/pull/65126#discussion_r3600085606


##########
fe/fe-core/src/main/java/org/apache/doris/datasource/metacache/MetaCacheEntry.java:
##########
@@ -222,48 +315,133 @@ private V getWithManualLoad(K key, Function<K, V> 
loadFunction) {
             return value;
         }
 
+        // Keep the slow miss load under the per-key load lock so concurrent 
misses for the same key
+        // are still deduplicated. publishLock(key) only protects the short 
publication window.
         synchronized (loadLock(key)) {
             value = data.asMap().get(key);
             if (value != null) {
                 return value;
             }
 
-            long generation = invalidateGeneration.get();
-            V loaded = loadAndTrack(key, loadFunction);
-            if (generation != invalidateGeneration.get()) {
-                return loaded;
+            long generation;
+            // Snapshot generation only after re-checking the cache under the 
publication lock so
+            // public mutations cannot slip between the miss observation and 
the captured version.
+            synchronized (publishLock(key)) {
+                value = data.asMap().get(key);
+                if (value != null) {
+                    return value;
+                }
+                generation = generationOf(key);
             }
-
-            // Keep null results uncached so manual miss load matches 
LoadingCache null-return behavior.
+            V loaded = loadAndTrack(key, loadFunction);
             if (loaded == null) {
                 return null;
             }
 
-            // Leave a narrow hook for tests to pause exactly before the cache 
put race window.
-            beforeManualCachePutForTest(key, loaded);
-            data.put(key, loaded);
-            if (generation != invalidateGeneration.get()) {
-                removeLoadedValue(key, loaded);
+            synchronized (publishLock(key)) {
+                if (generation != generationOf(key)) {
+                    return loaded;
+                }
+                // Leave a narrow hook for tests to pause exactly before the 
cache put race window.
+                beforeManualCachePutForTest(key, loaded);
+                putLoadedValueWithoutGenerationBump(key, loaded);
+                // Re-check after the put because 
invalidateAll()/invalidateIf() may bump stripe generations
+                // outside publishLock while this thread is publishing a 
loaded value.
+                if (generation != generationOf(key)) {
+                    removeLoadedValueWithoutGenerationBump(key, loaded);
+                }
             }
             return loaded;
         }
     }
 
+    // Keep internal load write-back separate from public mutation so it does 
not advance generation.
+    private void putLoadedValueWithoutGenerationBump(K key, V loaded) {
+        data.put(key, loaded);
+    }
+
     // Remove only the value loaded by the current request and keep newer 
replacements intact.
-    private void removeLoadedValue(K key, V loaded) {
+    private void removeLoadedValueWithoutGenerationBump(K key, V loaded) {
         data.asMap().computeIfPresent(key, (ignored, currentValue) -> 
currentValue == loaded ? null : currentValue);
     }
 
+    private CacheLoader<K, V> newCacheLoader() {
+        return new CacheLoader<K, V>() {
+            @Override
+            public V load(K key) {
+                return loadFromDefaultLoader(key);
+            }
+
+            @Override
+            public CompletableFuture<V> asyncReload(K key, V oldValue, 
Executor executor) {
+                long generation = generationOf(key);
+                CompletableFuture<V> result = new CompletableFuture<>();
+                CompletableFuture.supplyAsync(() -> 
loadFromDefaultLoader(key), executor)
+                        .whenComplete((loaded, error) -> {
+                            if (error != null) {
+                                result.completeExceptionally(error);
+                                return;
+                            }
+                            synchronized (publishLock(key)) {
+                                if (generation == generationOf(key)) {
+                                    result.complete(loaded);

Review Comment:
   Thanks for identifying this race. We agree that it is theoretically possible 
when an asynchronous refresh, capacity eviction, and bulk invalidation overlap 
in a narrow publication window.
   
   We do not plan to address it in this PR. This behavior already exists in the 
baseline Caffeine refresh semantics and is not introduced or worsened by this 
refactor. The generation-aware refresh added here prevents stale publication in 
the ordinary invalidation paths, while this remaining case requires the key to 
be evicted during refresh and bulk invalidation to overlap with refresh 
completion.
   
   The impact is limited to a temporarily stale local cache entry. It does not 
affect persisted metadata or data correctness, and the entry is eventually 
corrected by TTL, a subsequent refresh, or another explicit invalidation. Fully 
linearizing this case would require broader publication coordination and 
introduce additional locking complexity into the cache path.



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