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

CalvinKirs 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 0edac26d33f [fix](paimon) Stop clearing the Paimon table cache on 
every table lookup (#68860)
0edac26d33f is described below

commit 0edac26d33f62bdbe9c6e3671481deec36e9bcd8
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Sat Oct 10 16:32:59 2026 +0800

    [fix](paimon) Stop clearing the Paimon table cache on every table lookup 
(#68860)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #67023, #67395, #67996
    
    Problem Summary:
    
    **In short.** Since #67395, every Paimon table lookup on master clears
    that table's cached metadata before reloading it, so the Paimon metadata
    cache never hits. Every statement reloads the table and its latest
    snapshot from the remote catalog, and a catalog with caching enabled no
    longer serves its cached snapshot. This PR removes the invalidation and
    restores the regression expectation that #67395 changed.
    
    **Background.** #67395 forward-ported branch-4.1 #67023, which keeps the
    Paimon write cache coherent. In branch-4.x, the Doris table cache sits
    above Paimon's SDK `CachingCatalog`. On a Doris cache miss, #67023
    invalidates the SDK entry before reloading, so the reload cannot hand
    back a stale SDK table. #67395 placed that invalidation in
    `CatalogBackedPaimonCatalogOps.getTable`. On master, however, #67996 had
    already replaced the SDK `CachingCatalog` with the Doris-owned
    `PaimonMetaCacheCatalog`, and `PaimonCatalogFactory` always disables the
    SDK wrapper. The catalog that `getTable` wraps is therefore the Doris
    cache itself.
    
    | | branch-4.x | master since #67395 |
    |---|---|---|
    | Table cache | Doris table cache above Paimon's SDK `CachingCatalog` |
    `PaimonMetaCacheCatalog` only (SDK wrapper disabled) |
    | When the reload path runs | only on a Doris cache miss | on every
    `getTableHandle`, several times per statement |
    | What the invalidation clears | the SDK entry about to be reloaded |
    the Doris entries for the table: table, latest snapshot, schema-at memo,
    partition view |
    
    **The problem, and what it cost.**
    
    - **The cache never hits.** On a local filesystem catalog, 7 SELECT
    statements made 86 table-cache requests, with 0 hits and 38
    invalidations. The latest-snapshot cache had 0 hits in 24 requests. On
    HMS, REST or object-store catalogs, each of those reloads is a remote
    call, and it happens in every statement.
    - **The cache semantics changed.** A catalog with caching enabled used
    to keep reading its cached snapshot until REFRESH or the cache TTL. Now
    it reads data committed by other engines at once. #67395 changed
    `test_paimon_table_meta_cache` to expect the rows Spark wrote without a
    REFRESH, and attributed this to Paimon 1.4. branch-4.2 runs the same
    Paimon 1.4.2, and the same test there still expects the cached rows.
    
    **How this PR fixes it.**
    
    - `CatalogBackedPaimonCatalogOps.getTable` no longer invalidates the
    table. Nothing is lost by this: REFRESH TABLE / CATALOG, Doris writes
    (`refreshTableAfterExternalMutation`) and DDL all invalidate through
    `CatalogMetaCache`, which clears the `PaimonMetaCacheCatalog` entries,
    and no other layer caches the table.
    - `test_paimon_table_meta_cache` expects the cached rows again before
    the REFRESH.
    - `PaimonTableOptionsTest#catalogOpsInvalidatesPaimonCacheBeforeReload`
    pinned the invalidation. It is replaced by
    
`PaimonMetaCacheCatalogTest#connectorLookupsHitTheTableCacheUntilTheTableIsInvalidated`:
    repeated lookups through `CatalogBackedPaimonCatalogOps` load the table
    once, and a lookup after the table is invalidated loads it again.
    
    **Results.** Measured on a local FE + BE with a filesystem Paimon
    catalog, with a second catalog on the same warehouse playing the
    external writer:
    
    | | before | after |
    |---|---|---|
    | Cached catalog, after the other catalog wrote a row | sees the new row
    | sees the cached rows until REFRESH TABLE |
    | Cached catalog, after its own INSERT | sees it | sees it |
    | Table-cache hits | 0 of 86 requests (7 SELECTs) | 20 of 20 requests
    for 5 repeated SELECTs |
    
    Putting the invalidation back makes the new unit test fail. All 675
    tests of `fe-connector-paimon` pass.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [x] Regression test
    - `test_paimon_table_meta_cache` needs the Paimon docker fixture and was
    not run locally.
        - [x] Unit Test
    - `PaimonMetaCacheCatalogTest`, `PaimonTableOptionsTest` and the rest of
    `fe-connector-paimon` (675 tests) pass locally.
        - [x] Manual test (add detailed scripts or steps below)
    - Local FE + BE with two filesystem Paimon catalogs on one warehouse
    (one with `meta.cache.paimon.table.ttl-second` = 0); results in the
    table above.
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
    - [x] Yes. A Paimon catalog with metadata caching enabled serves
    repeated queries from its cache again. Data committed by other engines
    becomes visible after REFRESH or the cache TTL, as in branch-4.x and on
    master before #67395.
---
 .../doris/connector/paimon/PaimonCatalogOps.java   |  8 +++---
 .../paimon/PaimonMetaCacheCatalogTest.java         | 21 +++++++++++++++
 .../connector/paimon/PaimonTableOptionsTest.java   | 30 ----------------------
 .../paimon/test_paimon_table_meta_cache.groovy     |  5 ++--
 4 files changed, 26 insertions(+), 38 deletions(-)

diff --git 
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonCatalogOps.java
 
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonCatalogOps.java
index a895f711951..06193e96ccf 100644
--- 
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonCatalogOps.java
+++ 
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonCatalogOps.java
@@ -308,11 +308,9 @@ public interface PaimonCatalogOps {
          */
         @Override
         public Table getTable(Identifier identifier) throws 
Catalog.TableNotExistException {
-            // Doris owns the outer table-metadata cache and only reaches this 
method on an outer-cache
-            // load. Paimon's CachingCatalog owns a second cache for the Table 
and its lower-level readers.
-            // Invalidate that inner entry before reloading, otherwise REFRESH 
or outer-cache expiry can
-            // immediately repopulate Doris with the same stale bucket, path, 
or table options.
-            catalog.invalidateTable(identifier);
+            // Every metadata, scan and write lookup lands here, several times 
per statement. The wrapped
+            // catalog is the Doris table cache (PaimonMetaCacheCatalog; the 
SDK CachingCatalog is disabled),
+            // so this must not invalidate: REFRESH, Doris writes and DDL 
invalidate through CatalogMetaCache.
             Table table = catalog.getTable(identifier);
             Map<String, String> optionsForCopy = 
PaimonTableOptions.forCopy(tableOptions);
             return optionsForCopy.isEmpty() ? table : 
table.copy(optionsForCopy);
diff --git 
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonMetaCacheCatalogTest.java
 
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonMetaCacheCatalogTest.java
index 073535fbd0c..67abc59782d 100644
--- 
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonMetaCacheCatalogTest.java
+++ 
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonMetaCacheCatalogTest.java
@@ -294,6 +294,27 @@ class PaimonMetaCacheCatalogTest {
         }
     }
 
+    @Test
+    void connectorLookupsHitTheTableCacheUntilTheTableIsInvalidated() throws 
Exception {
+        // Each statement resolves its table handle several times through 
CatalogBackedPaimonCatalogOps.
+        // Those lookups must be served from the Doris table cache; only an 
invalidation of the table
+        // (REFRESH TABLE, a Doris write or DDL all end in CatalogMetaCache) 
makes the next one reload.
+        RecordingCatalog recording = new RecordingCatalog();
+        try (CatalogMetaCache owner = CatalogMetaCache.unmanaged()) {
+            PaimonCatalogOps ops = new 
PaimonCatalogOps.CatalogBackedPaimonCatalogOps(
+                    PaimonMetaCacheCatalog.tryToCreate(recording.catalog(), 
owner, 100, 100,
+                            cacheOptions(Duration.ofDays(1), 
Duration.ofDays(1)), true, false));
+
+            Table first = ops.getTable(TABLE);
+            Assertions.assertSame(first, ops.getTable(TABLE));
+            Assertions.assertEquals(1, recording.tableLoads.get(), "a repeated 
lookup must not reload the table");
+
+            owner.invalidateTable("db", "t");
+            Assertions.assertNotSame(first, ops.getTable(TABLE));
+            Assertions.assertEquals(2, recording.tableLoads.get());
+        }
+    }
+
     @Test
     void systemTableIsRebuiltFromTheCachedOriginTable() throws Exception {
         AtomicLong clock = new AtomicLong();
diff --git 
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonTableOptionsTest.java
 
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonTableOptionsTest.java
index c759df4e41c..f291ea74fc6 100644
--- 
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonTableOptionsTest.java
+++ 
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonTableOptionsTest.java
@@ -29,12 +29,9 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
-import java.lang.reflect.Proxy;
 import java.nio.file.Path;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 /**
@@ -276,33 +273,6 @@ public class PaimonTableOptionsTest {
         }
     }
 
-    @Test
-    public void catalogOpsInvalidatesPaimonCacheBeforeReload() throws 
Exception {
-        List<String> calls = new ArrayList<>();
-        Table loaded = (Table) Proxy.newProxyInstance(
-                Table.class.getClassLoader(), new Class<?>[] {Table.class},
-                (proxy, method, args) -> null);
-        Catalog catalog = (Catalog) Proxy.newProxyInstance(
-                Catalog.class.getClassLoader(), new Class<?>[] {Catalog.class},
-                (proxy, method, args) -> {
-                    if ("invalidateTable".equals(method.getName())) {
-                        calls.add("invalidate");
-                        return null;
-                    }
-                    if ("getTable".equals(method.getName())) {
-                        calls.add("get");
-                        return loaded;
-                    }
-                    return null;
-                });
-
-        Table actual = new 
PaimonCatalogOps.CatalogBackedPaimonCatalogOps(catalog)
-                .getTable(Identifier.create("db", "t"));
-
-        Assertions.assertSame(loaded, actual);
-        Assertions.assertEquals(java.util.Arrays.asList("invalidate", "get"), 
calls);
-    }
-
     @Test
     public void createCatalogRejectsBadTableOption() {
         Map<String, String> props = new HashMap<>();
diff --git 
a/regression-test/suites/external_table_p0/paimon/test_paimon_table_meta_cache.groovy
 
b/regression-test/suites/external_table_p0/paimon/test_paimon_table_meta_cache.groovy
index 199f4bdce93..e4c806bf26d 100644
--- 
a/regression-test/suites/external_table_p0/paimon/test_paimon_table_meta_cache.groovy
+++ 
b/regression-test/suites/external_table_p0/paimon/test_paimon_table_meta_cache.groovy
@@ -99,11 +99,10 @@ suite("test_paimon_table_meta_cache", "p0,external") {
 
         spark_paimon "INSERT INTO paimon.${testDb}.test_insert VALUES (2, 
'external_insert')"
 
-        // The cache retains the Paimon table object and its schema, not a 
data snapshot. Paimon 1.4
-        // resolves the latest snapshot when a read is planned from that 
cached table object.
+        // The cached catalog keeps reading the latest snapshot it cached 
until REFRESH (or its TTL).
         sql """switch ${catalogWithCache}"""
         def result2 = sql """select * from ${testDb}.test_insert order by id"""
-        assertEquals(2, result2.size())
+        assertEquals(1, result2.size())
 
         sql """switch ${catalogNoCache}"""
         def result2NoCache = sql """select * from ${testDb}.test_insert order 
by id"""


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

Reply via email to