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

morningman 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 af847e7a4d4 [improvement](fe) Validate submitted meta.cache keys of 
the catalog's own engines (#69556)
af847e7a4d4 is described below

commit af847e7a4d4e6c94017e734a17fe3b7e0676ff01
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Sun Oct 25 08:04:05 2026 +0000

    [improvement](fe) Validate submitted meta.cache keys of the catalog's own 
engines (#69556)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #66717, #67726
    
    Problem Summary:
    
    **In short.** This brings the DDL-time checks of `meta.cache.*` catalog
    properties from branch-4.x #66717 to master. CREATE / ALTER CATALOG now
    rejects a `meta.cache.<engine>.<entry>.<option>` key that the statement
    submits for an engine the catalog owns when the option is not `enable`,
    `ttl-second`, `capacity` or `max-weight`, or when its value has the
    wrong type, and it rejects a catalog quota (or an entry quota of a
    catalog without one) above the FE global quota
    `external_meta_cache_max_weight`. Unknown entries and keys stored by
    earlier statements stay accepted, as master intends.
    
    **Background.** External catalogs size their metadata caches with keys
    such as `meta.cache.hive.file.ttl-second` or
    `meta.cache.iceberg.table.max-weight`, plus a catalog quota
    `meta.cache.max-weight` that is capped by the FE-wide
    `external_meta_cache_max_weight`. Master (#67726) rewrote the cache
    governance of branch-4.x (#66717) on the connector SPI. Branch-4.x
    validates every `meta.cache.*` key at CREATE / ALTER against a
    whitelist. Master deliberately tolerates unknown entries, because a
    newer connector version may write an entry an older FE does not know,
    and it never re-validates stored keys, because ALTER can overwrite but
    not remove a key.
    
    **The problem, and what it cost.** Master only rejects an unknown
    `.max-weight` entry that the statement submits and checks the values of
    a few keys, so these DDLs succeed:
    
    - a misspelled option such as `meta.cache.hive.file.max-weigth = 64MB`:
    the limit the operator asked for is silently not applied;
    - `meta.cache.hive.file.enable = 1`: `Boolean.parseBoolean` reads it as
    false, so the cache is silently turned off;
    - `meta.cache.hive.file.ttl-second = -2`;
    - `meta.cache.max-weight = 1PB` on an FE whose global quota is 1GB:
    admission clamps the caches to the global quota at run time, so SHOW
    CREATE CATALOG promises far more memory than the caches can get.
    
    **How this PR fixes it.**
    
    - `CacheSpec.checkSubmittedEngineProperties` checks the keys a statement
    submits under `meta.cache.<engine>.`: the option after the last dot must
    be `enable` (true / false), `ttl-second` (>= -1), `capacity` (>= 0) or
    `max-weight` (a positive size); anything else fails with `Unknown
    external meta cache property: <key>`, the branch-4.x message. The value
    messages are the existing ones (`The parameter <key> is wrong, value is
    <value>`, `Invalid cache weight for '<key>': <value>`). Entry names are
    not checked.
    - `CacheSpec.checkSubmittedWeightsAgainstGlobal` rejects a submitted
    `meta.cache.max-weight`, and a submitted entry `max-weight` of a catalog
    without a catalog quota, above the global quota of the FE running the
    statement (nothing is checked when the global quota is unset).
    - Both run inside `CacheSpec.checkWeightProperties(properties,
    submittedProperties, engine, ...)`, which the hive connector (for `hive`
    and its `iceberg` sibling), iceberg, paimon, max_compute and adbc
    already call with the statement's properties, so no connector changes.
    For the namespace that fe-core owns (`default`, or `doris` for a remote
    Doris catalog), a new
    `ExternalCatalog.checkSubmittedMetaCacheProperties` hook runs them;
    `CatalogMgr` calls it on CREATE and ALTER CATALOG only, never on replay.
    - Only submitted keys are checked, so a key stored by an earlier
    statement, or a stored quota above a global quota lowered after a
    restart, never blocks an unrelated ALTER.
    
    Differences from branch-4.x, kept on purpose:
    
    - An unknown entry (for example `meta.cache.iceberg.schema.ttl-second`)
    and a key of an engine the catalog does not route stay accepted and are
    ignored.
    - `meta.cache.hive.partition_values.*`, `meta.cache.iceberg.snapshot.*`
    and `meta.cache.paimon.{table,snapshot}.max-weight` are not supported on
    master: a submitted `max-weight` of them is rejected as an unknown
    weight entry, their other options are accepted and ignored, and stored
    values are ignored. A catalog that only had entry quotas on them has no
    weight limit after upgrading.
    - An entry `max-weight = 0` is rejected; a stored 0 means no entry limit
    rather than a disabled entry.
    
    **Results.**
    
    | DDL | Master today | After this PR |
    |---|---|---|
    | `meta.cache.hive.file.max-weigth = 64MB` | accepted, ignored |
    `Unknown external meta cache property: meta.cache.hive.file.max-weigth`
    |
    | `meta.cache.hive.file.enable = 1` | accepted, cache off | `The
    parameter meta.cache.hive.file.enable is wrong, value is 1` |
    | `meta.cache.iceberg.table.ttl-second = -2` on an hms catalog |
    accepted | `The parameter meta.cache.iceberg.table.ttl-second is wrong,
    value is -2` |
    | `ALTER ... meta.cache.default.schema.enable = 1` | accepted | rejected
    the same way |
    | `meta.cache.max-weight = 1PB` with a 1GB global quota | accepted,
    clamped at run time | `meta.cache.max-weight (1125899906842624 bytes)
    can not exceed the FE global max weight external_meta_cache_max_weight
    (1073741824 bytes)` |
    | `meta.cache.default.future_entry.max-weight = 64MB`, then an unrelated
    ALTER | accepted | accepted (unchanged) |
    
    Unit tests: `CacheSpecTest` (3 new), one new test each in
    `HiveCatalogPropertiesTest`, `IcebergConnectorValidatePropertiesTest`,
    `PaimonConnectorValidatePropertiesTest`, `MaxComputePartitionCacheTest`
    and `AdbcCatalogPropertiesTest`, and 3 new tests in
    `ExternalCatalogMetaCacheWeightTest` (the fe-core hook, the global quota
    passed in explicitly, and the remote Doris engine). Without the
    submitted-key check the new hive, iceberg and paimon tests fail; without
    the global-quota check, or without the remote Doris engine, the
    corresponding tests fail. All tests of fe-connector-cache, -hive,
    -iceberg, -paimon, -maxcompute and -adbc pass, and so do 280 fe-core
    tests around catalog DDL and the metadata caches.
    `external_table_p0/test_catalog_ddl` gains the misspelled option,
    `enable=1`, `ttl-second=-2` and the ALTER case; its global-quota case
    runs only when `external_meta_cache_max_weight` is set, since CI
    frontends run without one. The suite passed on a local cluster both
    without a global quota and with `external_meta_cache_max_weight = 1GB`.
---
 .../connector/adbc/AdbcCatalogPropertiesTest.java  | 13 +++
 .../apache/doris/connector/cache/CacheSpec.java    | 97 ++++++++++++++++++++--
 .../doris/connector/cache/CacheSpecTest.java       | 81 ++++++++++++++++++
 .../connector/hive/HiveCatalogPropertiesTest.java  | 18 ++++
 .../IcebergConnectorValidatePropertiesTest.java    | 11 +++
 .../maxcompute/MaxComputePartitionCacheTest.java   | 15 ++++
 .../PaimonConnectorValidatePropertiesTest.java     | 15 ++++
 .../org/apache/doris/datasource/CatalogMgr.java    |  8 ++
 .../apache/doris/datasource/ExternalCatalog.java   | 31 ++++++-
 .../doris/RemoteDorisExternalCatalog.java          |  5 ++
 .../ExternalCatalogMetaCacheWeightTest.java        | 56 +++++++++++++
 .../external_table_p0/test_catalog_ddl.groovy      | 54 ++++++++++++
 12 files changed, 398 insertions(+), 6 deletions(-)

diff --git 
a/fe/fe-connector/fe-connector-adbc/src/test/java/org/apache/doris/connector/adbc/AdbcCatalogPropertiesTest.java
 
b/fe/fe-connector/fe-connector-adbc/src/test/java/org/apache/doris/connector/adbc/AdbcCatalogPropertiesTest.java
index c3ecbfd8ab8..764499aeb00 100644
--- 
a/fe/fe-connector/fe-connector-adbc/src/test/java/org/apache/doris/connector/adbc/AdbcCatalogPropertiesTest.java
+++ 
b/fe/fe-connector/fe-connector-adbc/src/test/java/org/apache/doris/connector/adbc/AdbcCatalogPropertiesTest.java
@@ -249,6 +249,19 @@ class AdbcCatalogPropertiesTest {
                 current, Map.of("meta.cache.max-weight", "1MB", 
"meta.cache.adbc.metadata.max-weight", "2MB")));
     }
 
+    @Test
+    void submittedMetaCacheKeysMustNameAKnownOption() {
+        AdbcConnectorProvider provider = new AdbcConnectorProvider();
+        Map<String, String> current = minimal();
+        IllegalArgumentException misspelled = 
Assertions.assertThrows(IllegalArgumentException.class,
+                () -> provider.validatePropertiesForUpdate(
+                        current, Map.of("meta.cache.adbc.metadata.ttl-secnod", 
"60")));
+        Assertions.assertEquals("Unknown external meta cache property: 
meta.cache.adbc.metadata.ttl-secnod",
+                misspelled.getMessage());
+        current.put("meta.cache.adbc.metadata.ttl-secnod", "60");
+        Assertions.assertDoesNotThrow(() -> 
provider.validatePropertiesForUpdate(current, Map.of("comment", "new")));
+    }
+
     /**
      * A cache knob is read through {@code CacheSpec}, which answers an 
unparseable value with the default
      * rather than an error. Silently caching for ten minutes when the 
operator wrote {@code ttl-second=6O0}
diff --git 
a/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/CacheSpec.java
 
b/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/CacheSpec.java
index c720139716e..f3f2150159a 100644
--- 
a/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/CacheSpec.java
+++ 
b/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/CacheSpec.java
@@ -220,13 +220,26 @@ public final class CacheSpec {
         checkWeightProperties(properties, Collections.emptyMap(), engine, 
knownEntries);
     }
 
-    /** DDL validation: reject unknown entries only when explicitly submitted 
by this statement. */
+    /**
+     * DDL validation: the keys this statement submits for {@code engine} must 
be well formed (see
+     * {@link #checkSubmittedEngineProperties}), its submitted weights must 
not exceed the FE-global limit, and an
+     * unknown weight entry is rejected only when this statement submits it.
+     */
     public static void checkWeightProperties(Map<String, String> properties,
             Map<String, String> submittedProperties, String engine, String... 
knownEntries) {
+        checkWeightProperties(properties, submittedProperties, 
MetaCacheGovernance.globalMaxWeight(), engine,
+                knownEntries);
+    }
+
+    /** As {@link #checkWeightProperties(Map, Map, String, String...)}, 
against the given FE-global limit. */
+    public static void checkWeightProperties(Map<String, String> properties, 
Map<String, String> submittedProperties,
+            OptionalLong globalMaxWeight, String engine, String... 
knownEntries) {
+        checkSubmittedEngineProperties(submittedProperties, engine);
         OptionalLong catalogMax = checkCatalogWeightProperty(properties);
         if (properties == null) {
             return;
         }
+        checkSubmittedWeightsAgainstGlobal(properties, submittedProperties, 
globalMaxWeight, engine);
         String prefix = metaCacheKeyPrefix(engine);
         Set<String> weightKeys = new HashSet<>();
         for (String entry : knownEntries) {
@@ -244,10 +257,7 @@ public final class CacheSpec {
                 // Like other connector properties, validate consumed values 
but tolerate unknown keys.
                 continue;
             }
-            long parsed = parseWeight(property.getValue(), key, false, 0L);
-            if (parsed <= 0L) {
-                throw new IllegalArgumentException(key + " must be positive");
-            }
+            long parsed = parsePositiveWeight(property.getValue(), key);
             if (catalogMax.isPresent() && parsed > catalogMax.getAsLong()) {
                 throw new IllegalArgumentException(key + " can not exceed "
                         + MetaCacheBudgetManager.CATALOG_MAX_WEIGHT_PROPERTY);
@@ -255,6 +265,71 @@ public final class CacheSpec {
         }
     }
 
+    /**
+     * DDL validation of the {@code meta.cache.<engine>.<entry>.<option>} keys 
this statement submits: the option must
+     * be enable, ttl-second, capacity or max-weight, and the value one of its 
type. Entry names are not checked, so a
+     * key of an entry that a newer connector reads stays accepted, and keys 
an earlier statement persisted are not
+     * inspected, so a stored key never blocks an unrelated ALTER.
+     */
+    public static void checkSubmittedEngineProperties(Map<String, String> 
submittedProperties, String engine) {
+        String prefix = metaCacheKeyPrefix(engine);
+        for (Map.Entry<String, String> property : 
submittedProperties.entrySet()) {
+            String key = property.getKey();
+            if (!key.startsWith(prefix)) {
+                continue;
+            }
+            int optionStart = key.lastIndexOf('.');
+            if (optionStart <= prefix.length()) {
+                throw unknownProperty(key);
+            }
+            String value = property.getValue();
+            switch (key.substring(optionStart)) {
+                case KEY_ENABLE:
+                    checkBooleanProperty(value, key);
+                    break;
+                case KEY_TTL_SECOND:
+                    checkLongProperty(value, CACHE_NO_TTL, key);
+                    break;
+                case KEY_CAPACITY:
+                    checkLongProperty(value, 0L, key);
+                    break;
+                case KEY_MAX_WEIGHT:
+                    parsePositiveWeight(value, key);
+                    break;
+                default:
+                    throw unknownProperty(key);
+            }
+        }
+    }
+
+    /**
+     * DDL validation against the FE-global limit, the one of the FE running 
the statement: a submitted catalog
+     * weight, and a submitted {@code engine} entry weight of a catalog 
without a catalog weight, must not exceed it.
+     * Admission clamps every cache to the limit anyway; this keeps SHOW 
CREATE CATALOG from promising more. Only
+     * submitted keys are checked, so a limit lowered after a restart never 
blocks an unrelated ALTER.
+     */
+    public static void checkSubmittedWeightsAgainstGlobal(Map<String, String> 
effectiveProperties,
+            Map<String, String> submittedProperties, OptionalLong 
globalMaxWeight, String engine) {
+        if (!globalMaxWeight.isPresent()) {
+            return;
+        }
+        boolean catalogLimited = 
effectiveProperties.containsKey(MetaCacheBudgetManager.CATALOG_MAX_WEIGHT_PROPERTY);
+        String prefix = metaCacheKeyPrefix(engine);
+        for (Map.Entry<String, String> property : 
submittedProperties.entrySet()) {
+            String key = property.getKey();
+            boolean limitedByGlobal = 
key.equals(MetaCacheBudgetManager.CATALOG_MAX_WEIGHT_PROPERTY)
+                    || !catalogLimited && key.startsWith(prefix) && 
key.endsWith(KEY_MAX_WEIGHT);
+            if (!limitedByGlobal) {
+                continue;
+            }
+            long weight = parsePositiveWeight(property.getValue(), key);
+            if (weight > globalMaxWeight.getAsLong()) {
+                throw new IllegalArgumentException(key + " (" + weight + " 
bytes) can not exceed the FE global max"
+                        + " weight external_meta_cache_max_weight (" + 
globalMaxWeight.getAsLong() + " bytes)");
+            }
+        }
+    }
+
     public static boolean isCacheEnabled(boolean enable, long ttlSecond, long 
capacity) {
         return enable && ttlSecond != 0 && capacity != 0;
     }
@@ -400,6 +475,18 @@ public final class CacheSpec {
         return value.longValue();
     }
 
+    private static long parsePositiveWeight(String value, String key) {
+        long parsed = parseWeight(value, key, false, 0L);
+        if (parsed <= 0L) {
+            throw new IllegalArgumentException(key + " must be positive");
+        }
+        return parsed;
+    }
+
+    private static IllegalArgumentException unknownProperty(String key) {
+        return new IllegalArgumentException("Unknown external meta cache 
property: " + key);
+    }
+
     private static IllegalArgumentException invalidWeight(String key, String 
value) {
         return new IllegalArgumentException("Invalid cache weight for '" + key 
+ "': " + value);
     }
diff --git 
a/fe/fe-connector/fe-connector-cache/src/test/java/org/apache/doris/connector/cache/CacheSpecTest.java
 
b/fe/fe-connector/fe-connector-cache/src/test/java/org/apache/doris/connector/cache/CacheSpecTest.java
index 1169a4ef505..4d5080a2516 100644
--- 
a/fe/fe-connector/fe-connector-cache/src/test/java/org/apache/doris/connector/cache/CacheSpecTest.java
+++ 
b/fe/fe-connector/fe-connector-cache/src/test/java/org/apache/doris/connector/cache/CacheSpecTest.java
@@ -285,6 +285,87 @@ public class CacheSpecTest {
                 () -> CacheSpec.checkCatalogWeightProperty(properties));
     }
 
+    @Test
+    public void submittedEngineKeysMustNameAKnownOptionWithAValueOfItsType() {
+        IllegalArgumentException misspelled = 
Assertions.assertThrows(IllegalArgumentException.class,
+                () -> CacheSpec.checkSubmittedEngineProperties(
+                        
Collections.singletonMap("meta.cache.iceberg.table.max-weigth", "16MB"), 
"iceberg"));
+        Assertions.assertEquals("Unknown external meta cache property: 
meta.cache.iceberg.table.max-weigth",
+                misspelled.getMessage());
+        for (String key : new String[] {"meta.cache.iceberg.max-weight", 
"meta.cache.iceberg.enable",
+                "meta.cache.iceberg..enable", "meta.cache.iceberg.table."}) {
+            Assertions.assertThrows(IllegalArgumentException.class, () -> 
CacheSpec.checkSubmittedEngineProperties(
+                    Collections.singletonMap(key, "1"), "iceberg"), key);
+        }
+        IllegalArgumentException enable = 
Assertions.assertThrows(IllegalArgumentException.class,
+                () -> CacheSpec.checkSubmittedEngineProperties(
+                        
Collections.singletonMap("meta.cache.iceberg.table.enable", "1"), "iceberg"));
+        Assertions.assertEquals("The parameter meta.cache.iceberg.table.enable 
is wrong, value is 1",
+                enable.getMessage());
+        IllegalArgumentException ttl = 
Assertions.assertThrows(IllegalArgumentException.class,
+                () -> CacheSpec.checkSubmittedEngineProperties(
+                        
Collections.singletonMap("meta.cache.iceberg.table.ttl-second", "-2"), 
"iceberg"));
+        Assertions.assertEquals("The parameter 
meta.cache.iceberg.table.ttl-second is wrong, value is -2",
+                ttl.getMessage());
+        Assertions.assertThrows(IllegalArgumentException.class, () -> 
CacheSpec.checkSubmittedEngineProperties(
+                
Collections.singletonMap("meta.cache.iceberg.manifest.capacity", "-1"), 
"iceberg"));
+        Assertions.assertThrows(IllegalArgumentException.class, () -> 
CacheSpec.checkSubmittedEngineProperties(
+                
Collections.singletonMap("meta.cache.iceberg.manifest.max-weight", "0"), 
"iceberg"));
+
+        // Entries are not checked: a newer connector may read one this 
version does not know. Other engines'
+        // keys, the catalog weight and plain properties belong to other 
validators.
+        Map<String, String> accepted = new HashMap<>();
+        accepted.put("meta.cache.iceberg.schema.ttl-second", "0");
+        accepted.put("meta.cache.iceberg.future_entry.enable", "TRUE");
+        accepted.put("meta.cache.iceberg.table.ttl-second", "-1");
+        accepted.put("meta.cache.iceberg.table.capacity", "0");
+        accepted.put("meta.cache.iceberg.table.max-weight", "64MB");
+        accepted.put("meta.cache.paimon.table.enable", "1");
+        accepted.put("meta.cache.max-weight", "1GB");
+        accepted.put("comment", "x");
+        CacheSpec.checkSubmittedEngineProperties(accepted, "iceberg");
+    }
+
+    @Test
+    public void checkWeightPropertiesValidatesOnlyTheSubmittedEngineKeys() {
+        Map<String, String> stored = new HashMap<>();
+        stored.put("meta.cache.iceberg.table.max-weigth", "16MB");
+        stored.put("meta.cache.iceberg.table.enable", "1");
+        Assertions.assertDoesNotThrow(() -> CacheSpec.checkWeightProperties(
+                stored, Collections.singletonMap("comment", "updated"), 
"iceberg", "table"));
+        Assertions.assertThrows(IllegalArgumentException.class, () -> 
CacheSpec.checkWeightProperties(
+                stored, 
Collections.singletonMap("meta.cache.iceberg.table.enable", "yes"), "iceberg", 
"table"));
+    }
+
+    @Test
+    public void submittedWeightsMayNotExceedTheGlobalLimit() {
+        OptionalLong global = OptionalLong.of(1024L * 1024 * 1024);
+        Map<String, String> catalogAbove = 
Collections.singletonMap("meta.cache.max-weight", "2GB");
+        IllegalArgumentException e = 
Assertions.assertThrows(IllegalArgumentException.class,
+                () -> CacheSpec.checkWeightProperties(catalogAbove, 
catalogAbove, global, "iceberg", "table"));
+        Assertions.assertEquals("meta.cache.max-weight (2147483648 bytes) can 
not exceed the FE global max weight"
+                + " external_meta_cache_max_weight (1073741824 bytes)", 
e.getMessage());
+        Map<String, String> catalogAtLimit = 
Collections.singletonMap("meta.cache.max-weight", "1GB");
+        CacheSpec.checkWeightProperties(catalogAtLimit, catalogAtLimit, 
global, "iceberg", "table");
+
+        // Without a catalog weight the global limit bounds an entry weight; 
with one the catalog weight does.
+        Map<String, String> entryAbove = 
Collections.singletonMap("meta.cache.iceberg.table.max-weight", "2GB");
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> CacheSpec.checkWeightProperties(entryAbove, entryAbove, 
global, "iceberg", "table"));
+        Map<String, String> limitedCatalog = new HashMap<>(entryAbove);
+        limitedCatalog.put("meta.cache.max-weight", "1GB");
+        IllegalArgumentException parent = 
Assertions.assertThrows(IllegalArgumentException.class,
+                () -> CacheSpec.checkWeightProperties(limitedCatalog, 
entryAbove, global, "iceberg", "table"));
+        Assertions.assertEquals("meta.cache.iceberg.table.max-weight can not 
exceed meta.cache.max-weight",
+                parent.getMessage());
+
+        // A stored weight is not compared again, e.g. after the limit was 
lowered, and no limit bounds nothing.
+        CacheSpec.checkWeightProperties(catalogAbove, 
Collections.singletonMap("comment", "updated"), global,
+                "iceberg", "table");
+        CacheSpec.checkWeightProperties(catalogAbove, catalogAbove, 
OptionalLong.empty(), "iceberg", "table");
+        CacheSpec.checkSubmittedWeightsAgainstGlobal(entryAbove, entryAbove, 
global, "paimon");
+    }
+
     @Test
     public void invalidPersistedEntryWeightFallsBackToUnbounded() {
         Map<String, String> properties = new HashMap<>();
diff --git 
a/fe/fe-connector/fe-connector-hive/src/test/java/org/apache/doris/connector/hive/HiveCatalogPropertiesTest.java
 
b/fe/fe-connector/fe-connector-hive/src/test/java/org/apache/doris/connector/hive/HiveCatalogPropertiesTest.java
index ef81f1b6e1e..73df0bb4966 100644
--- 
a/fe/fe-connector/fe-connector-hive/src/test/java/org/apache/doris/connector/hive/HiveCatalogPropertiesTest.java
+++ 
b/fe/fe-connector/fe-connector-hive/src/test/java/org/apache/doris/connector/hive/HiveCatalogPropertiesTest.java
@@ -245,6 +245,24 @@ class HiveCatalogPropertiesTest {
                 HiveTestProperties.mapWith(), unknownEntry));
     }
 
+    @Test
+    void submittedMetaCacheKeysOfBothEnginesMustNameAKnownOption() {
+        HiveConnectorProvider provider = new HiveConnectorProvider();
+        IllegalArgumentException misspelled = 
Assertions.assertThrows(IllegalArgumentException.class,
+                () -> provider.validateProperties(
+                        
HiveTestProperties.mapWith("meta.cache.hive.file.max-weigth", "64MB")));
+        Assertions.assertEquals("Unknown external meta cache property: 
meta.cache.hive.file.max-weigth",
+                misspelled.getMessage());
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> 
provider.validateProperties(HiveTestProperties.mapWith("meta.cache.hive.file.enable",
 "1")));
+        Assertions.assertThrows(IllegalArgumentException.class, () -> 
provider.validateProperties(
+                
HiveTestProperties.mapWith("meta.cache.iceberg.table.ttl-second", "-2")));
+        // A key stored by an earlier statement is not validated again by an 
unrelated ALTER.
+        Assertions.assertDoesNotThrow(() -> 
provider.validatePropertiesForUpdate(
+                HiveTestProperties.mapWith("meta.cache.hive.file.max-weigth", 
"64MB"),
+                java.util.Collections.singletonMap("comment", "updated")));
+    }
+
     @Test
     void validCatalogPassesBothDoors() {
         Assertions.assertDoesNotThrow(() -> 
HiveTestProperties.minimal().checkCreateTimeOnlyRules());
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorValidatePropertiesTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorValidatePropertiesTest.java
index 7e2d2268c44..7241b620a39 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorValidatePropertiesTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorValidatePropertiesTest.java
@@ -130,6 +130,17 @@ public class IcebergConnectorValidatePropertiesTest {
                 "meta.cache.iceberg.table.enable", "maybe")).contains("is 
wrong"));
     }
 
+    @Test
+    public void misspelledMetaCacheOptionRejectedThroughProvider() {
+        // The option must be known; an unknown entry stays accepted because a 
newer connector may read it.
+        Assertions.assertEquals(
+                "Unknown external meta cache property: 
meta.cache.iceberg.table.max-weigth",
+                rejectMessage(props("iceberg.catalog.type", "hms", 
"hive.metastore.uris", "thrift://h",
+                        "meta.cache.iceberg.table.max-weigth", "16MB")));
+        PROVIDER.validateProperties(props("iceberg.catalog.type", "hms", 
"hive.metastore.uris", "thrift://h",
+                "meta.cache.iceberg.schema.ttl-second", "0"));
+    }
+
     @Test
     public void validMetaCacheKnobsAcceptedThroughProvider() {
         // ttl-second=-1 (no-expiration sentinel, min is -1), 0 (disable), 
capacity=0, boolean enable all pass.
diff --git 
a/fe/fe-connector/fe-connector-maxcompute/src/test/java/org/apache/doris/connector/maxcompute/MaxComputePartitionCacheTest.java
 
b/fe/fe-connector/fe-connector-maxcompute/src/test/java/org/apache/doris/connector/maxcompute/MaxComputePartitionCacheTest.java
index d1245b6a12c..61b8d2acaf7 100644
--- 
a/fe/fe-connector/fe-connector-maxcompute/src/test/java/org/apache/doris/connector/maxcompute/MaxComputePartitionCacheTest.java
+++ 
b/fe/fe-connector/fe-connector-maxcompute/src/test/java/org/apache/doris/connector/maxcompute/MaxComputePartitionCacheTest.java
@@ -91,6 +91,21 @@ public class MaxComputePartitionCacheTest {
                 current, Map.of("meta.cache.max-weight", "1MB", 
"meta.cache.max_compute.partition.max-weight", "2MB")));
     }
 
+    @Test
+    public void submittedMetaCacheKeysMustNameAKnownOption() {
+        MaxComputeConnectorProvider provider = new 
MaxComputeConnectorProvider();
+        Map<String, String> current = MCTestProperties.minimalMap();
+        IllegalArgumentException misspelled = 
Assertions.assertThrows(IllegalArgumentException.class,
+                () -> provider.validatePropertiesForUpdate(
+                        current, 
Map.of("meta.cache.max_compute.partition.max-weigth", "1MB")));
+        Assertions.assertEquals("Unknown external meta cache property: 
meta.cache.max_compute.partition.max-weigth",
+                misspelled.getMessage());
+        Assertions.assertThrows(IllegalArgumentException.class, () -> 
provider.validatePropertiesForUpdate(
+                current, Map.of("meta.cache.max_compute.partition.enable", 
"1")));
+        current.put("meta.cache.max_compute.partition.max-weigth", "1MB");
+        Assertions.assertDoesNotThrow(() -> 
provider.validatePropertiesForUpdate(current, Map.of("comment", "new")));
+    }
+
     @Test
     public void managedCacheRetainsOnlySpecsAndHonorsBothLimits() throws 
Exception {
         Odps odps = new Odps(new AliyunAccount("ak", "sk"));
diff --git 
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonConnectorValidatePropertiesTest.java
 
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonConnectorValidatePropertiesTest.java
index 991cdffd535..96c28cf3019 100644
--- 
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonConnectorValidatePropertiesTest.java
+++ 
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonConnectorValidatePropertiesTest.java
@@ -98,6 +98,21 @@ public class PaimonConnectorValidatePropertiesTest {
                         "meta.cache.paimon.table.enable", "maybe")));
     }
 
+    @Test
+    public void rejectsMisspelledMetaCacheOption() {
+        IllegalArgumentException misspelled = 
Assertions.assertThrows(IllegalArgumentException.class,
+                () -> validate(props(
+                        "paimon.catalog.type", "filesystem", "warehouse", 
"/wh",
+                        "meta.cache.paimon.partition_view.max-weigth", 
"16MB")));
+        Assertions.assertEquals(
+                "Unknown external meta cache property: 
meta.cache.paimon.partition_view.max-weigth",
+                misspelled.getMessage());
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> validate(props(
+                        "paimon.catalog.type", "filesystem", "warehouse", 
"/wh",
+                        "meta.cache.paimon.partition_view.enable", "1")));
+    }
+
     @Test
     public void acceptsValidMetaCacheKnobs() {
         // Valid values must pass: ttl-second=-1 is the "no expiration" 
sentinel (min is -1), 0 disables,
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogMgr.java 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogMgr.java
index 00300420aa1..385b42b9bc9 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogMgr.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogMgr.java
@@ -291,6 +291,8 @@ public class CatalogMgr implements Writable, 
GsonPostProcessable {
         if (catalog instanceof ExternalCatalog) {
             try {
                 ((ExternalCatalog) catalog).checkProperties();
+                ((ExternalCatalog) catalog).checkSubmittedMetaCacheProperties(
+                        catalog.getProperties(), cmd.getProperties());
             } catch (DdlException | RuntimeException e) {
                 catalog.onCreateFailure();
                 throw e;
@@ -447,6 +449,12 @@ public class CatalogMgr implements Writable, 
GsonPostProcessable {
                 }
             }
 
+            if (catalog instanceof ExternalCatalog) {
+                Map<String, String> effectiveProperties = 
Maps.newHashMap(oldProperties);
+                effectiveProperties.putAll(updates);
+                ((ExternalCatalog) 
catalog).checkSubmittedMetaCacheProperties(effectiveProperties, updates);
+            }
+
             // Filesystem binding may read Hadoop XML files. Validate the 
detached snapshot before
             // taking the global catalog write lock, then retry if another 
ALTER changed the snapshot.
             boolean validatedWithoutMutation = catalog instanceof 
ExternalCatalog
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
index 3934483b97b..2ee07578704 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
@@ -41,6 +41,7 @@ import org.apache.doris.common.util.ExceptionDiagnosticUtils;
 import org.apache.doris.common.util.Util;
 import org.apache.doris.connector.cache.CacheSpec;
 import org.apache.doris.connector.cache.MetaCacheBudgetManager;
+import org.apache.doris.connector.cache.MetaCacheGovernance;
 import org.apache.doris.datasource.doris.RemoteDorisExternalDatabase;
 import org.apache.doris.datasource.infoschema.ExternalInfoSchemaDatabase;
 import org.apache.doris.datasource.infoschema.ExternalMysqlDatabase;
@@ -79,6 +80,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
+import java.util.OptionalLong;
 import java.util.Set;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -479,7 +481,34 @@ public abstract class ExternalCatalog
 
     /** Strict CREATE/ALTER validation for the core metadata-cache namespace 
owned by this catalog. */
     protected void checkMetaCacheWeightProperties(Map<String, String> 
properties) {
-        CacheSpec.checkWeightProperties(properties, "default", "schema");
+        CacheSpec.checkWeightProperties(properties, coreMetaCacheEngine(), 
"schema");
+    }
+
+    /** The engine of the core metadata-cache namespace that fe-core owns for 
this catalog. */
+    protected String coreMetaCacheEngine() {
+        return "default";
+    }
+
+    /**
+     * CREATE / ALTER CATALOG only, never on replay: the meta cache keys this 
statement submits for the engine of the
+     * core metadata-cache namespace must be well formed, and the weights it 
submits must not exceed the FE-global
+     * limit. A connector checks the keys of its own engines when it validates 
the statement's properties.
+     */
+    public void checkSubmittedMetaCacheProperties(Map<String, String> 
effectiveProperties,
+            Map<String, String> submittedProperties) throws DdlException {
+        checkSubmittedMetaCacheProperties(effectiveProperties, 
submittedProperties,
+                MetaCacheGovernance.globalMaxWeight());
+    }
+
+    void checkSubmittedMetaCacheProperties(Map<String, String> 
effectiveProperties,
+            Map<String, String> submittedProperties, OptionalLong 
globalMaxWeight) throws DdlException {
+        try {
+            CacheSpec.checkSubmittedEngineProperties(submittedProperties, 
coreMetaCacheEngine());
+            CacheSpec.checkSubmittedWeightsAgainstGlobal(
+                    effectiveProperties, submittedProperties, globalMaxWeight, 
coreMetaCacheEngine());
+        } catch (IllegalArgumentException e) {
+            throw new DdlException(e.getMessage());
+        }
     }
 
     /**
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/doris/RemoteDorisExternalCatalog.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/doris/RemoteDorisExternalCatalog.java
index 0c2397d40f8..2f4bb0c99e6 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/doris/RemoteDorisExternalCatalog.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/doris/RemoteDorisExternalCatalog.java
@@ -81,6 +81,11 @@ public class RemoteDorisExternalCatalog extends 
ExternalCatalog {
                 DorisExternalMetaCache.ENTRY_SCHEMA, 
DorisExternalMetaCache.ENTRY_BACKENDS);
     }
 
+    @Override
+    protected String coreMetaCacheEngine() {
+        return DorisExternalMetaCache.ENGINE;
+    }
+
     public List<String> getFeNodes() {
         return 
parseHttpHosts(catalogProperty.getOrDefault(RemoteDorisProperties.FE_HTTP_HOSTS,
 ""));
     }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalCatalogMetaCacheWeightTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalCatalogMetaCacheWeightTest.java
index 3d8561ab2db..711a73a769d 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalCatalogMetaCacheWeightTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalCatalogMetaCacheWeightTest.java
@@ -25,8 +25,10 @@ import org.apache.doris.datasource.test.TestExternalCatalog;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.OptionalLong;
 
 public class ExternalCatalogMetaCacheWeightTest {
 
@@ -62,6 +64,60 @@ public class ExternalCatalogMetaCacheWeightTest {
         });
     }
 
+    @Test
+    public void submittedCoreKeysMustNameAKnownOptionWithAValueOfItsType() 
throws DdlException {
+        ExternalCatalog catalog = new TestExternalCatalog(3L, "test", "", 
testCatalogProperties(), "");
+        DdlException misspelled = Assertions.assertThrows(DdlException.class,
+                () -> 
catalog.checkSubmittedMetaCacheProperties(testCatalogProperties(),
+                        
Collections.singletonMap("meta.cache.default.schema.max-weigth", "64MB"),
+                        OptionalLong.empty()));
+        Assertions.assertTrue(misspelled.getMessage().contains(
+                "Unknown external meta cache property: 
meta.cache.default.schema.max-weigth"), misspelled.getMessage());
+        for (String[] bad : new String[][] 
{{"meta.cache.default.schema.enable", "1"},
+                {"meta.cache.default.schema.ttl-second", "-2"}, 
{"meta.cache.default.schema.capacity", "-1"}}) {
+            Assertions.assertThrows(DdlException.class, () -> 
catalog.checkSubmittedMetaCacheProperties(
+                    testCatalogProperties(), Collections.singletonMap(bad[0], 
bad[1]), OptionalLong.empty()), bad[0]);
+        }
+
+        // An entry this version does not know stays accepted; a connector 
validates the keys of its own engine.
+        Map<String, String> accepted = new HashMap<>();
+        accepted.put("meta.cache.default.future_entry.max-weight", "64MB");
+        accepted.put("meta.cache.hive.file.enable", "1");
+        catalog.checkSubmittedMetaCacheProperties(testCatalogProperties(), 
accepted, OptionalLong.empty());
+        // A key an earlier statement stored is not inspected again.
+        Map<String, String> stored = testCatalogProperties();
+        stored.put("meta.cache.default.schema.max-weigth", "64MB");
+        catalog.checkSubmittedMetaCacheProperties(stored, 
Collections.singletonMap("comment", "updated"),
+                OptionalLong.empty());
+    }
+
+    @Test
+    public void submittedWeightsMayNotExceedTheGlobalLimitOfThisFrontend() 
throws DdlException {
+        ExternalCatalog catalog = new TestExternalCatalog(4L, "test", "", 
testCatalogProperties(), "");
+        OptionalLong global = OptionalLong.of(64L * 1024 * 1024);
+        Map<String, String> catalogAbove = 
Collections.singletonMap("meta.cache.max-weight", "128MB");
+        DdlException e = Assertions.assertThrows(DdlException.class,
+                () -> catalog.checkSubmittedMetaCacheProperties(catalogAbove, 
catalogAbove, global));
+        Assertions.assertTrue(e.getMessage().contains("meta.cache.max-weight 
(134217728 bytes) can not exceed the FE"
+                + " global max weight external_meta_cache_max_weight (67108864 
bytes)"), e.getMessage());
+        Map<String, String> entryAbove = 
Collections.singletonMap("meta.cache.default.schema.max-weight", "128MB");
+        Assertions.assertThrows(DdlException.class,
+                () -> catalog.checkSubmittedMetaCacheProperties(entryAbove, 
entryAbove, global));
+
+        catalog.checkSubmittedMetaCacheProperties(catalogAbove, 
Collections.singletonMap("comment", "updated"), global);
+        catalog.checkSubmittedMetaCacheProperties(catalogAbove, catalogAbove, 
OptionalLong.empty());
+    }
+
+    @Test
+    public void remoteDorisChecksTheKeysOfItsOwnEngine() throws DdlException {
+        ExternalCatalog catalog = new RemoteDorisExternalCatalog(3L, "remote", 
"", remoteDorisProperties(), "");
+        Assertions.assertThrows(DdlException.class, () -> 
catalog.checkSubmittedMetaCacheProperties(
+                remoteDorisProperties(), 
Collections.singletonMap("meta.cache.doris.backends.enable", "1"),
+                OptionalLong.empty()));
+        catalog.checkSubmittedMetaCacheProperties(remoteDorisProperties(),
+                
Collections.singletonMap("meta.cache.doris.future_entry.ttl-second", "0"), 
OptionalLong.empty());
+    }
+
     private static Map<String, String> testCatalogProperties() {
         Map<String, String> properties = new HashMap<>();
         properties.put("catalog_provider.class", 
RefreshCatalogTest.RefreshCatalogProvider.class.getName());
diff --git a/regression-test/suites/external_table_p0/test_catalog_ddl.groovy 
b/regression-test/suites/external_table_p0/test_catalog_ddl.groovy
index a75159a00f1..07b9a84ba49 100644
--- a/regression-test/suites/external_table_p0/test_catalog_ddl.groovy
+++ b/regression-test/suites/external_table_p0/test_catalog_ddl.groovy
@@ -153,4 +153,58 @@ suite("test_catalog_ddl", "p0,external") {
             )
         """
         sql """alter catalog test_ddl_future_core_cache set 
properties("comment" = "updated")"""
+
+        // A submitted meta cache key must name a known option with a value of 
its type. An unknown entry is still
+        // accepted (above): a newer connector may read an entry this version 
does not know.
+        sql """drop catalog if exists test_ddl_strict_meta_cache"""
+        test {
+            sql """
+                create catalog test_ddl_strict_meta_cache properties(
+                    "type" = "hms",
+                    "hive.metastore.uris" = "thrift://127.0.0.1:9083",
+                    "meta.cache.hive.file.max-weigth" = "64MB"
+                )
+            """
+            exception "Unknown external meta cache property: 
meta.cache.hive.file.max-weigth"
+        }
+        test {
+            sql """
+                create catalog test_ddl_strict_meta_cache properties(
+                    "type" = "hms",
+                    "hive.metastore.uris" = "thrift://127.0.0.1:9083",
+                    "meta.cache.hive.file.enable" = "1"
+                )
+            """
+            exception "The parameter meta.cache.hive.file.enable is wrong, 
value is 1"
+        }
+        test {
+            sql """
+                create catalog test_ddl_strict_meta_cache properties(
+                    "type" = "hms",
+                    "hive.metastore.uris" = "thrift://127.0.0.1:9083",
+                    "meta.cache.iceberg.table.ttl-second" = "-2"
+                )
+            """
+            exception "The parameter meta.cache.iceberg.table.ttl-second is 
wrong, value is -2"
+        }
+        // The core namespace that fe-core owns is checked the same way, on 
ALTER too.
+        test {
+            sql """alter catalog test_ddl_future_core_cache set 
properties("meta.cache.default.schema.enable" = "1")"""
+            exception "The parameter meta.cache.default.schema.enable is 
wrong, value is 1"
+        }
+
+        // A submitted weight may not exceed the FE global limit. Frontends 
run without one by default.
+        def globalWeight = sql """ADMIN SHOW FRONTEND CONFIG LIKE 
'external_meta_cache_max_weight'"""
+        if (globalWeight.size() == 1 && globalWeight[0][1] != "0") {
+            test {
+                sql """
+                    create catalog test_ddl_strict_meta_cache properties(
+                        "type" = "hms",
+                        "hive.metastore.uris" = "thrift://127.0.0.1:9083",
+                        "meta.cache.max-weight" = "1PB"
+                    )
+                """
+                exception "can not exceed the FE global max weight 
external_meta_cache_max_weight"
+            }
+        }
 }


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

Reply via email to