Skip to content

[fix](paimon) Stop clearing the Paimon table cache on every table lookup - #68860

Merged
CalvinKirs merged 1 commit into
apache:masterfrom
morningman:b2m-t14a-paimon-cache-hits
Oct 10, 2026
Merged

CalvinKirs merged 1 commit into
apache:masterfrom
morningman:b2m-t14a-paimon-cache-hits

Conversation

@morningman

Copy link
Copy Markdown
Contributor

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. [feature](paimon) Add Paimon table write support #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

    • Regression test
      • test_paimon_table_meta_cache needs the Paimon docker fixture and was not run locally.
    • Unit Test
      • PaimonMetaCacheCatalogTest, PaimonTableOptionsTest and the rest of fe-connector-paimon (675 tests) pass locally.
    • 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
  • Behavior changed:

  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

### What problem does this PR solve?

Issue Number: None

Related PR: apache#67023, apache#67395, apache#67996

Problem Summary: master apache#67395 forward-ported branch-4.1 apache#67023, which keeps the Paimon write
cache coherent. In branch-4.x the Doris table cache sits above Paimon's SDK CachingCatalog, and on a
Doris cache miss apache#67023 invalidates the SDK entry before reloading, so the reload cannot return a
stale SDK table. apache#67395 put that invalidation into `CatalogBackedPaimonCatalogOps.getTable`. On master,
however, apache#67996 had already replaced the SDK CachingCatalog with the Doris-owned
`PaimonMetaCacheCatalog` (the SDK wrapper is always disabled), so the catalog this method wraps is the
Doris cache itself. `getTable` runs on every `getTableHandle`, several times per statement, and each
call invalidated the table's cached metadata (the table, its latest snapshot, the schema-at memo and
the partition view) and reloaded it from the remote catalog.

As a result the Paimon metadata cache never hit. On a local filesystem catalog, 7 SELECT statements
made 86 table-cache requests with 0 hits and 38 invalidations, and every statement reloaded the table
and its latest snapshot; on HMS, REST or object-store catalogs each reload is a remote call. A catalog
with caching enabled also stopped serving its cached snapshot. apache#67395 changed
`test_paimon_table_meta_cache` to expect rows written by Spark without a REFRESH and attributed that to
Paimon 1.4, but branch-4.2, on the same Paimon 1.4.2, still expects the cached rows.

This removes the invalidation. REFRESH TABLE / CATALOG, Doris writes
(`refreshTableAfterExternalMutation`) and DDL invalidate through `CatalogMetaCache`, which clears the
`PaimonMetaCacheCatalog` entries, and no other layer caches the table. The regression expectation is
restored, and the unit test that pinned the invalidation is replaced by one that pins cache hits until
the table is invalidated.

### Release note

None

### Check List (For Author)

- Test: Unit Test, Manual test; Regression test (not run locally)
    - New `PaimonMetaCacheCatalogTest#connectorLookupsHitTheTableCacheUntilTheTableIsInvalidated`
      fails when the invalidation is put back; all 675 tests of `fe-connector-paimon` pass.
    - Local FE + BE with a filesystem Paimon catalog: after another catalog writes the table, the
      cached catalog reads the cached rows until REFRESH TABLE, and reads its own writes at once; five
      repeated SELECTs add 20 table-cache requests, all hits (before: 0 hits).
    - `test_paimon_table_meta_cache` needs the Paimon docker fixture and was not run locally.
- Behavior changed: 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 apache#67395.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complete static review of PR #68860 at e864467. Two review rounds converged with no new valuable findings in round 2. There were no existing inline threads or additional user focus points.

Critical checkpoints:

  • Goal and proof: Removing the per-lookup invalidation permits repeated Paimon table lookups to hit the Doris cache, as the new unit test checks. The changed external-INSERT regression asserts stale-until-REFRESH behavior for its TTL setting. These tests do not cover the cache-disable/expiry mismatch or an external schema change followed by Doris INSERT (inline findings).
  • Scope: The production edit is confined to one lookup method, with corresponding unit and regression changes. The behavior affects downstream snapshot pinning and writer schema binding.
  • Concurrency and lifecycle: The connector's table, latest-snapshot, and schema caches have distinct lifetimes. Shared table-scope invalidation fences in-flight publication; statement handles can retain their pin for that statement. No new locks, threads, circular ownership, or static initialization dependencies were introduced.
  • Configuration: No new option was added. Existing paimon.cache-enabled and paimon.cache.expire-after-write settings expose an uncoordinated snapshot-cache lifetime (P1). Explicit schema.cache.ttl-second=0 can expose the writer-schema mismatch (P2). No dynamic configuration update path changed.
  • Compatibility and parallel paths: No wire fields, function symbols, storage formats, or gensrc definitions changed. I traced ordinary scans, explicit time travel, system/branch handles, table-option copies, REST partition planning, Doris DDL, writes, and REFRESH; no further independent issue was substantiated.
  • Conditions and tests: The edited method adds no runtime condition. The new unit assertion covers table-cache hits and direct invalidation; the removed test covered the previous invalidation behavior. The regression's no-cache catalog sets the snapshot TTL to zero, so it misses the distinct disabled-table-cache setting. Test expectations match the cases written, but the two findings remain untested. No builds or tests were run because this review is static only.
  • Observability: No new execution path or log/metric instrumentation was added; the identified failures manifest as old rows or an Unknown column error. No separate observability defect was substantiated.
  • Transactions, persistence, and writes: No EditLog format or replay behavior changed. Successful Doris mutation and refresh paths invalidate connector scope. A proposed post-commit edit-log failure was dismissed: the examined journal failure paths exit or throw rather than returning a healthy FE that serves a stale next query. The retained target Table can still bind an obsolete INSERT schema after an external ALTER (P2).
  • FE/BE variables and performance: No FE-to-BE variable was added. Avoiding invalidation on each repeated lookup reduces unnecessary table reloads, but snapshot pins must be coordinated with the table cache (P1). No other substantiated CPU or memory regression was found.

The final changed-file and candidate sweep is complete. The accepted inline set is P1 and P2; overlapping cache candidates were merged, and no existing blocking comment IDs apply.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Keep the snapshot pin in step with the table cache. With paimon.cache-enabled=false, the table is reloaded on every lookup but PaimonLatestSnapshotCache still holds the prior snapshot id for its default 24-hour access TTL. Likewise, when a configured paimon.cache.expire-after-write reloads the table, the old snapshot id survives; beginQuerySnapshot keeps pinning scans to it, and frequent reads can extend that staleness indefinitely after an external INSERT. The removed invalidation previously cleared both entries. Disable or expire the snapshot cache whenever the table cache is disabled or expires, and cover both settings with external-write reads.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two lifetimes were already separate before this PR, and branch-4.x has the same staleness. Since the connector cutover (#64304) the latest-snapshot pin has followed meta.cache.paimon.table.ttl-second (an access TTL, 24 h by default). Setting it to 0 turns off both the table cache and the pin, which is what the no-cache catalog in test_paimon_table_meta_cache does. paimon.cache-enabled and paimon.cache.expire-after-* are Paimon's own catalog-cache options. #67996 applied them to the Doris table cache when it replaced the SDK CachingCatalog, but they never governed the pin. On branch-4.x they configure only the SDK CachingCatalog (4.x even turns it off under a weight limit). Doris keeps serving its cached snapshot there until the meta.cache.paimon.table TTL or REFRESH, and that TTL is access-based too, so frequent reads keep it alive there as well.

The per-lookup invalidation added by #67395 hid this, but it also kept the cache from ever hitting (0 hits in 86 lookups for 7 SELECTs), which is the bug this PR fixes. Making paimon.cache-enabled also turn off the snapshot pin would give the option a meaning it does not have on branch-4.x, so I'd keep that out of this PR.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Resolve the current schema for INSERT targets. After an external ALTER TABLE ... ADD COLUMN, this retained Table can still have the old schema().fields(). PaimonWritePlanProvider.getWriteColumns uses those fields for the statement's writer schema, so an INSERT naming the new column fails as Unknown column until table expiry or REFRESH, even when schema.cache.ttl-second=0 lets DESC show the new column. The previous per-lookup invalidation reloaded the target Table. Read the live target schema at the writer boundary, and cover external ALTER followed by Doris INSERT.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches branch-4.x, where #67023 made the Doris metadata cache the owner of the table instances that writes use, so a column another engine adds becomes writable only after REFRESH TABLE or cache expiry there too. What differs on master is that getTableSchema reads the latest schema file rather than the cached table's rowType() (a stale rowType() kept old names after a RENAME), so with schema.cache.ttl-second=0 DESC can show the column before INSERT accepts it. That window ends at REFRESH TABLE or when the cached table expires, at most Paimon's cache.expire-after-write (30 minutes by default).

Reloading the table for each write would have to give every handle in the statement the same table version, since the write plan and the bound write columns both read the table carried by their handle. That is a change to the write path's metadata contract rather than to this PR's cache hits, so I'd rather not fold it in here.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28636 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit e8644677fdaaa053b6c119b0cf1b42c32da55613, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17614	5630	5526	5526
q2	2125	322	279	279
q3	10209	1403	826	826
q4	4674	473	344	344
q5	7439	783	537	537
q6	190	189	153	153
q7	748	802	606	606
q8	9245	1343	1043	1043
q9	5702	4439	4405	4405
q10	6825	1289	1031	1031
q11	439	252	231	231
q12	623	397	283	283
q13	18075	3037	2384	2384
q14	266	260	249	249
q15	q16	730	722	670	670
q17	1156	733	571	571
q18	7031	6225	6162	6162
q19	1117	959	611	611
q20	379	331	220	220
q21	4963	2239	2390	2239
q22	355	307	266	266
Total cold run time: 99905 ms
Total hot run time: 28636 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6142	6111	6109	6109
q2	679	584	563	563
q3	5191	5378	4928	4928
q4	1980	2146	1540	1540
q5	5251	5183	5841	5183
q6	249	197	149	149
q7	2308	2089	1836	1836
q8	3165	2834	2730	2730
q9	8231	8170	8203	8170
q10	3837	3761	3326	3326
q11	615	431	435	431
q12	698	773	531	531
q13	2889	3237	2579	2579
q14	304	305	294	294
q15	q16	725	707	663	663
q17	8423	7508	7450	7450
q18	13197	12199	13036	12199
q19	912	784	780	780
q20	2179	2158	1916	1916
q21	5714	4640	4758	4640
q22	499	442	397	397
Total cold run time: 73188 ms
Total hot run time: 66414 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150720 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit e8644677fdaaa053b6c119b0cf1b42c32da55613, data reload: false

query5	4299	577	442	442
query6	428	205	189	189
query7	4794	462	217	217
query8	314	179	159	159
query9	8776	3924	3915	3915
query10	478	307	251	251
query11	5975	3404	3210	3210
query12	139	93	87	87
query13	1248	451	323	323
query14	6518	4708	4428	4428
query14_1	4220	4243	4152	4152
query15	206	195	185	185
query16	958	440	408	408
query17	866	658	533	533
query18	2422	423	304	304
query19	200	164	136	136
query20	82	81	79	79
query21	212	131	110	110
query22	12930	13066	12881	12881
query23	13107	12671	12309	12309
query23_1	12303	12106	12084	12084
query24	7073	814	456	456
query24_1	465	450	449	449
query25	497	381	338	338
query26	1265	255	133	133
query27	2775	428	264	264
query28	4618	1916	1913	1913
query29	1519	571	425	425
query30	288	219	182	182
query31	888	756	635	635
query32	139	90	90	90
query33	492	286	228	228
query34	914	842	497	497
query35	739	772	627	627
query36	818	810	748	748
query37	122	96	88	88
query38	1789	1770	1678	1678
query39	737	720	672	672
query39_1	680	647	645	645
query40	210	109	89	89
query41	64	65	61	61
query42	88	86	87	86
query43	363	373	322	322
query44	1281	682	673	673
query45	180	182	164	164
query46	832	945	569	569
query47	2999	3026	2937	2937
query48	296	307	208	208
query49	554	374	275	275
query50	680	273	207	207
query51	10410	10144	9994	9994
query52	78	82	66	66
query53	182	205	150	150
query54	216	183	183	183
query55	71	69	64	64
query56	240	214	190	190
query57	1644	1577	1596	1577
query58	274	250	244	244
query59	2331	2362	2130	2130
query60	272	227	217	217
query61	142	145	141	141
query62	389	342	282	282
query63	192	161	160	160
query64	2716	941	753	753
query65	3468	3361	3380	3361
query66	1758	402	286	286
query67	20564	20132	19955	19955
query68	2964	987	597	597
query69	395	276	241	241
query70	903	834	821	821
query71	303	227	206	206
query72	2688	2676	2283	2283
query73	505	531	292	292
query74	4584	4466	4291	4291
query75	2315	2292	1913	1913
query76	2299	996	580	580
query77	346	388	297	297
query78	8975	8895	8411	8411
query79	879	836	511	511
query80	479	412	319	319
query81	532	318	274	274
query82	206	134	105	105
query83	187	198	175	175
query84	285	131	106	106
query85	752	451	375	375
query86	251	233	231	231
query87	1984	1991	1826	1826
query88	3539	2659	2644	2644
query89	331	284	257	257
query90	2100	184	178	178
query91	160	143	124	124
query92	95	85	87	85
query93	973	977	555	555
query94	446	303	269	269
query95	582	333	387	333
query96	659	519	229	229
query97	2447	2415	2305	2305
query98	153	146	141	141
query99	763	765	651	651
Total cold run time: 232556 ms
Total hot run time: 150720 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.52 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit e8644677fdaaa053b6c119b0cf1b42c32da55613, data reload: false

query1	0.01	0.01	0.00
query2	0.10	0.05	0.05
query3	0.26	0.12	0.11
query4	1.61	0.13	0.12
query5	0.28	0.21	0.25
query6	1.17	0.68	0.70
query7	0.04	0.01	0.01
query8	0.06	0.04	0.04
query9	0.42	0.36	0.36
query10	0.57	0.56	0.56
query11	0.21	0.15	0.14
query12	0.18	0.15	0.16
query13	0.50	0.50	0.51
query14	0.96	0.96	0.96
query15	0.63	0.61	0.60
query16	0.34	0.35	0.30
query17	1.19	1.13	1.16
query18	0.22	0.21	0.20
query19	2.10	1.98	1.94
query20	0.02	0.02	0.01
query21	15.42	0.26	0.15
query22	4.47	0.04	0.05
query23	16.27	0.32	0.12
query24	2.96	0.57	0.50
query25	0.11	0.07	0.07
query26	1.32	0.23	0.15
query27	0.05	0.04	0.04
query28	2.23	0.60	0.41
query29	12.54	4.03	3.26
query30	0.28	0.15	0.14
query31	2.77	0.57	0.34
query32	3.23	0.61	0.52
query33	3.18	3.26	3.16
query34	15.52	3.98	3.40
query35	3.34	3.37	3.33
query36	0.55	0.48	0.47
query37	0.09	0.06	0.06
query38	0.05	0.04	0.04
query39	0.04	0.03	0.03
query40	0.17	0.14	0.14
query41	0.09	0.03	0.03
query42	0.03	0.02	0.02
query43	0.04	0.03	0.03
Total cold run time: 95.62 s
Total hot run time: 24.52 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 0.00% (0/60) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage `` 🎉
Increment coverage report
Complete coverage report

@suxiaogang223 suxiaogang223 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@CalvinKirs
CalvinKirs merged commit 0edac26 into apache:master Oct 10, 2026
41 of 42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants