924060929 commented on code in PR #66633:
URL: https://github.com/apache/doris/pull/66633#discussion_r3869505857


##########
fe/fe-core/src/main/java/org/apache/doris/datasource/metacache/FeMetaCacheEntry.java:
##########
@@ -0,0 +1,486 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.datasource.metacache;
+
+import org.apache.doris.common.Config;
+import org.apache.doris.connector.cache.CacheSpec;
+import org.apache.doris.connector.cache.CatalogMetaCache;
+import org.apache.doris.connector.cache.MetaCache;
+import org.apache.doris.connector.cache.MetaCacheDefinition;
+import org.apache.doris.connector.cache.MetaCacheRemovalReason;
+import org.apache.doris.connector.cache.ScopePath;
+import org.apache.doris.connector.cache.ScopedMetaCache.CacheMetrics;
+
+import com.github.benmanes.caffeine.cache.RemovalListener;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReferenceArray;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import javax.annotation.Nullable;
+
+/**
+ * FE naming-cache adapter over the shared connector-cache runtime.
+ *
+ * <p>The common runtime owns value storage, load deduplication, generations, 
eviction and leak-free indexes. This
+ * adapter only retains the short per-key publication window needed to update 
FE's auxiliary ID/name indexes together
+ * with a database or table object.
+ */
+public class FeMetaCacheEntry<K, V> {
+    private static final int SINGLE_KEY_STRIPES = 1;
+
+    private static final class StripeState<K> {
+        @Nullable
+        private Map<K, ActionState> activeActions;
+    }
+
+    private static final class ActionState {
+        private long generation;
+        private int references;
+    }
+
+    private static final class ActionToken<K> {
+        private final K key;
+        private final ActionState state;
+        private final long generation;
+
+        private ActionToken(K key, ActionState state) {
+            this.key = key;
+            this.state = state;
+            this.generation = state.generation;
+        }
+    }
+
+    private final String name;
+    @Nullable
+    private final Function<K, V> loader;
+    private final CacheSpec cacheSpec;
+    private final boolean effectiveEnabled;
+    private final boolean autoRefresh;
+    private final int stripeCount;
+    private final AtomicReferenceArray<StripeState<K>> stripeStates;
+    private final CatalogMetaCache owner = new CatalogMetaCache();
+    private final MetaCache<K, V> data;
+
+    public FeMetaCacheEntry(String name, Function<K, V> loader, CacheSpec 
cacheSpec, ExecutorService refreshExecutor) {
+        this(name, loader, cacheSpec, refreshExecutor, true, false, 
defaultObjectStripeCount(), null);
+    }
+
+    public FeMetaCacheEntry(String name, Function<K, V> loader, CacheSpec 
cacheSpec, ExecutorService refreshExecutor,
+            boolean autoRefresh) {
+        this(name, loader, cacheSpec, refreshExecutor, autoRefresh, false, 
defaultObjectStripeCount(), null);
+    }
+
+    public FeMetaCacheEntry(String name, @Nullable Function<K, V> loader, 
CacheSpec cacheSpec,
+            ExecutorService refreshExecutor, boolean autoRefresh, boolean 
contextualOnly) {
+        this(name, loader, cacheSpec, refreshExecutor, autoRefresh, 
contextualOnly,
+                defaultObjectStripeCount(), null);
+    }
+
+    public FeMetaCacheEntry(String name, Function<K, V> loader, CacheSpec 
cacheSpec, ExecutorService refreshExecutor,
+            boolean autoRefresh, int stripeCount) {
+        this(name, loader, cacheSpec, refreshExecutor, autoRefresh, false, 
stripeCount, null);
+    }
+
+    public FeMetaCacheEntry(String name, @Nullable Function<K, V> loader, 
CacheSpec cacheSpec,
+            ExecutorService refreshExecutor, boolean autoRefresh, boolean 
contextualOnly, int stripeCount) {
+        this(name, loader, cacheSpec, refreshExecutor, autoRefresh, 
contextualOnly, stripeCount, null);
+    }
+
+    public static <K, V> FeMetaCacheEntry<K, V> withSyncRemovalListener(String 
name, Function<K, V> loader,
+            CacheSpec cacheSpec, ExecutorService refreshExecutor, 
RemovalListener<K, V> removalListener) {
+        return withSyncRemovalListener(name, loader, cacheSpec, 
refreshExecutor,
+                defaultObjectStripeCount(), removalListener);
+    }
+
+    public static <K, V> FeMetaCacheEntry<K, V> withSyncRemovalListener(String 
name, Function<K, V> loader,
+            CacheSpec cacheSpec, ExecutorService refreshExecutor, int 
stripeCount,
+            RemovalListener<K, V> removalListener) {
+        return new FeMetaCacheEntry<>(name, loader, cacheSpec, 
refreshExecutor, false, false, stripeCount,
+                Objects.requireNonNull(removalListener, "removalListener can 
not be null"));
+    }
+
+    FeMetaCacheEntry(String name, @Nullable Function<K, V> loader, CacheSpec 
cacheSpec,
+            ExecutorService refreshExecutor, boolean autoRefresh, boolean 
contextualOnly, int stripeCount,
+            @Nullable RemovalListener<K, V> removalListener) {
+        this.name = Objects.requireNonNull(name, "name can not be null");
+        this.loader = loader;
+        this.cacheSpec = Objects.requireNonNull(cacheSpec, "cacheSpec can not 
be null");
+        this.autoRefresh = autoRefresh;
+        Objects.requireNonNull(refreshExecutor, "refreshExecutor can not be 
null");
+        if (contextualOnly && loader != null) {
+            throw new IllegalArgumentException("contextual-only entry loader 
must be null");
+        }
+        if (contextualOnly && autoRefresh) {
+            throw new IllegalArgumentException("contextual-only entry can not 
enable auto refresh");
+        }
+        if (!contextualOnly) {
+            Objects.requireNonNull(loader, "loader can not be null");
+        }
+        if (removalListener != null && autoRefresh) {
+            throw new IllegalArgumentException("sync removal listener cache 
can not enable refreshAfterWrite");
+        }
+        if (stripeCount < 1) {
+            throw new IllegalArgumentException("stripeCount must be positive");
+        }
+        this.stripeCount = stripeCount;
+        stripeStates = new AtomicReferenceArray<>(stripeCount);
+        if (stripeCount == SINGLE_KEY_STRIPES) {
+            stripeStates.set(0, new StripeState<>());
+        }
+        effectiveEnabled = CacheSpec.isCacheEnabled(
+                cacheSpec.isEnable(), cacheSpec.getTtlSecond(), 
cacheSpec.getCapacity());
+        MetaCacheDefinition.Builder<K, V> builder = 
MetaCacheDefinition.builder(
+                name, cacheSpec, ignored -> ScopePath.catalog());
+        if (loader != null) {
+            builder.loader(key -> loadAndPause(key, loader));
+        }
+        if (removalListener != null) {
+            builder.removalListener((key, value, reason) ->
+                    removalListener.onRemoval(key, value, 
toCaffeineRemovalCause(reason)));
+        }
+        if (autoRefresh && Config.external_cache_refresh_time_minutes > 0) {
+            builder.refreshAfterWrite(
+                    
Duration.ofMinutes(Config.external_cache_refresh_time_minutes), 
refreshExecutor);
+        }
+        data = owner.create(builder.build());
+    }
+
+    public String name() {
+        return name;
+    }
+
+    public V get(K key) {
+        if (loader == null) {
+            throw new UnsupportedOperationException(String.format(
+                    "Entry '%s' requires a contextual miss loader.", name));
+        }
+        return data.get(key);
+    }
+
+    public V get(K key, Function<K, V> missLoader) {
+        Function<K, V> nonNullLoader = Objects.requireNonNull(missLoader, 
"missLoader can not be null");
+        return data.get(key, loadKey -> loadAndPause(loadKey, nonNullLoader));
+    }
+
+    public V getAndRunIfCurrent(K key, BiConsumer<K, V> currentValueAction) {
+        return getAndRunIfCurrent(key, (ignored, value) -> true, 
currentValueAction);
+    }
+
+    public V getAndRunIfCurrent(K key, BiPredicate<K, V> actionRequired,
+            BiConsumer<K, V> currentValueAction) {
+        BiPredicate<K, V> required = Objects.requireNonNull(actionRequired, 
"actionRequired can not be null");
+        BiConsumer<K, V> action = Objects.requireNonNull(currentValueAction, 
"currentValueAction can not be null");
+        Function<K, V> loadFunction = Objects.requireNonNull(loader, "loader 
can not be null");
+        V cached = data.getIfPresent(key);
+        if (cached != null && !required.test(key, cached)) {
+            return cached;
+        }
+        StripeState<K> stripe = stripeState(key);
+        ActionToken<K> token;
+        synchronized (stripe) {
+            token = beginAction(stripe, key);
+        }
+        try {
+            AtomicBoolean actionPublished = new AtomicBoolean(false);
+            V value = cached == null
+                    ? data.getWithPublicationAction(key, loadKey -> 
loadAndPause(loadKey, loadFunction),
+                            (loaded, commit) -> {
+                                beforeCurrentValueActionForTest(key, loaded);
+                                synchronized (stripe) {
+                                    if (isCurrent(stripe, token) && 
required.test(key, loaded)) {
+                                        commit.accept(() -> action.accept(key, 
loaded));
+                                        actionPublished.set(true);
+                                    } else {
+                                        commit.accept(() -> {
+                                        });
+                                    }
+                                }
+                            })
+                    : cached;
+            if (value == null) {
+                return null;
+            }
+            if (actionPublished.get()) {
+                return value;
+            }
+            if (!required.test(key, value)) {
+                return value;
+            }
+            beforeCurrentValueActionForTest(key, value);
+            synchronized (stripe) {
+                boolean current = isCurrent(stripe, token)
+                        && (!effectiveEnabled || data.getIfPresent(key) == 
value);
+                if (current && required.test(key, value) && isCurrent(stripe, 
token)) {
+                    try {
+                        action.accept(key, value);
+                    } catch (RuntimeException | Error throwable) {
+                        data.invalidateKey(key);
+                        throw throwable;
+                    }
+                }
+            }
+            return value;
+        } finally {
+            synchronized (stripe) {
+                endAction(stripe, token);
+            }
+        }
+    }
+
+    public V getIfPresent(K key) {
+        return data.getIfPresent(key);
+    }
+
+    @Nullable
+    public V findIfPresent(Predicate<K> keyPredicate) {
+        Objects.requireNonNull(keyPredicate, "keyPredicate can not be null");
+        List<V> result = new ArrayList<>(1);
+        data.forEach((key, value) -> {
+            if (result.isEmpty() && keyPredicate.test(key)) {
+                result.add(value);
+            }
+        });
+        return result.isEmpty() ? null : result.get(0);
+    }
+
+    public void put(K key, V value) {
+        Objects.requireNonNull(value, "value can not be null");
+        StripeState<K> stripe = stripeState(key);
+        synchronized (stripe) {
+            bumpAction(stripe, key);
+            beforePublicMutationWriteForTest(key);
+            data.put(key, value);
+        }
+    }
+
+    public V compute(K key, BiFunction<K, V, V> remappingFunction) {
+        return computeAndRun(key, remappingFunction, () -> {
+        });
+    }
+
+    public V computeAndRun(K key, BiFunction<K, V, V> remappingFunction, 
Runnable afterMutation) {
+        BiFunction<K, V, V> remapper = 
Objects.requireNonNull(remappingFunction, "remappingFunction can not be null");
+        Runnable action = Objects.requireNonNull(afterMutation, "afterMutation 
can not be null");
+        StripeState<K> stripe = stripeState(key);
+        synchronized (stripe) {
+            bumpAction(stripe, key);
+            while (true) {
+                V current = data.getIfPresent(key);
+                V updated = effectiveEnabled ? remapper.apply(key, current) : 
null;
+                beforePublicMutationWriteForTest(key);
+                if (data.compareAndSet(key, current, updated)) {
+                    action.run();
+                    return updated;
+                }
+            }
+        }
+    }
+
+    public V computeAfterValidation(K key, BiFunction<K, V, V> 
remappingFunction, Runnable validationAction) {
+        BiFunction<K, V, V> remapper = 
Objects.requireNonNull(remappingFunction, "remappingFunction can not be null");
+        Runnable validation = Objects.requireNonNull(validationAction, 
"validationAction can not be null");
+        StripeState<K> stripe = stripeState(key);
+        synchronized (stripe) {
+            while (true) {
+                V current = data.getIfPresent(key);
+                V updated = effectiveEnabled ? remapper.apply(key, current) : 
null;
+                validation.run();

Review Comment:
   已修复,提交是 529ed2d3801。
   
   之前注册数据库时,ID 映射会先对查询可见,但对应的数据库对象还没有提交完成。并发的按 ID 
查询可能趁这个空档放入另一份对象;事件更新随后替换它时,又会把查询已经拿到的对象重置掉。
   
   现在 ID 映射和数据库对象放在公共 cache 的同一次提交保护里:只有确认旧对象没有变化后才更新 ID,并且在 ID 
更新和对象更新全部完成前,不允许并发加载插入另一份对象。表缓存也用了同样的处理。
   
   我补了可重复的 latch 并发测试,专门覆盖“ID 已可见、对象还没更新”这个时序。验证通过:公共 cache 111 
个测试、FeMetaCacheEntryTest 14 个测试、ExternalCatalogTest 和 ExternalDatabaseTest 共 81 
个测试,以及完整 ./build.sh --fe。



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