Skip to content

[fix](fe) Fix deadlock in ExternalDatabase.refreshMetaToUninitialized caused by lock inversion with Caffeine cache#65409

Closed
starocean999 wants to merge 3 commits into
apache:masterfrom
starocean999:master_0708
Closed

[fix](fe) Fix deadlock in ExternalDatabase.refreshMetaToUninitialized caused by lock inversion with Caffeine cache#65409
starocean999 wants to merge 3 commits into
apache:masterfrom
starocean999:master_0708

Conversation

@starocean999

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

RefreshDatabaseCommand.run() [line 43]
-> RefreshManager.handleRefreshDb() [line 80]
-> refreshDbInternal() [line 120]
-> ExternalDatabase.resetMetaToUninitialized() [line 119]
-> synchronized(this) { [line 126]
metaCache.invalidateAll() [line 132]
-> namesCache.invalidateAll() [MetaCache:148]
-> metaObjCache.invalidateAll() [MetaCache:151]

RefreshTableCommand.run() [line 43]
-> RefreshManager.handleRefreshTable() [line 124]
-> ExternalDatabase.getTableNullable() [line 499]
-> makeSureInitialized() [line 500]
-> MetaCache.getMetaObj() [line 505]
-> synchronized(metaObjCache) { [MetaCache:93]
metaObjCache.get(name) // Caffeine node lock [MetaCache:99]
-> [cache loader from buildMetaCache()] [line 174]
-> buildTableForInit() [line 260]
-> getTableNamesWithLock() [line 271]
-> makeSureInitialized() [line 497]
-> synchronized(this) // DEADLOCK [line 141]

Problem Summary:

ExternalDatabase.resetMetaToUninitialized() calls
metaCache.invalidateAll() while holding synchronized(this). This is
unnecessary — MetaCache.invalidateAll() only touches thread-safe Caffeine
caches and a ConcurrentHashMap, none of which require the ExternalDatabase
monitor for correctness. The reference pattern ExternalCatalog.resetToUninitialized()
already calls onRefreshCache() (which calls invalidateAll()) outside
synchronized(this).

None

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • 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:

    • No.
    • Yes.
  • 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

… caused by lock inversion with Caffeine cache
@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?

@starocean999

Copy link
Copy Markdown
Contributor Author

/review

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

Automated review result: request changes.

I found three blocking issues in this PR:

  1. resetMetaToUninitialized() now publishes metaCache = null, but concurrent readers still dereference metaCache outside the database monitor after makeSureInitialized() returns. This introduces a new NPE race in external table/database metadata resolution.
  2. The added FE test calls Thread.threadId(), which does not exist under the configured Java 17 compiler target.
  3. The real-path deadlock test coordinates the loader and refresh threads so progress can depend on the 10-second latch timeout rather than deterministic lock-order validation.

Critical checkpoint conclusions:

  • Goal/test proof: the intended deadlock fix is not yet proven safely because the production change introduces a new concurrency failure and the new test does not compile as written.
  • Scope/focus: the code change is small, but setting metaCache to null expands the cache lifecycle state seen by unlocked readers.
  • Concurrency/lifecycle: this PR is specifically concurrency-sensitive; the old Caffeine lock inversion is addressed in one place, but the replacement cache lifecycle is unsafe for parallel metadata readers.
  • Configuration/session/compatibility: no new config was added; session-cache bypass was checked and not raised as a separate issue; Java 17 compatibility is broken by the test API usage.
  • Parallel paths: getTableNullable() and getTableNamesWithLock() both remain relevant dereference paths after initialization.
  • Test coverage/results: coverage intent is good, but current tests are blocked by compilation and one concurrency test is timing-dependent.
  • Persistence/transactions/data writes/observability: not applicable to this PR beyond external metadata cache invalidation.

User focus: no additional user-provided focus was supplied.

Subagent conclusions: optimizer-rewrite proposed OPT-001 and OPT-002; both were accepted as inline comments MAIN-001 and MAIN-002. tests-session-config marked TEST-001 as a duplicate of OPT-002 and later proposed TEST-002, accepted as MAIN-003. Convergence round 1 found TEST-002, so the ledger/comment set was updated. Convergence round 2 ended with both live subagents returning NO_NEW_VALUABLE_FINDINGS for the final three-comment set.

Validation notes: I could not run the full FE build/test flow because this checkout lacks .worktree_initialized and thirdparty/installed / thirdparty/installed/bin/protoc. I did verify PR head/base metadata via GitHub, existing review context via API, and the Java 17 Thread.threadId() failure with a local javac --release 17 probe.

Comment thread fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java Outdated
@starocean999

Copy link
Copy Markdown
Contributor Author

/review

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

Automated follow-up review result for current head 96a5f3da6377133982d9f9dfe735ed503c0887d0: no new inline comments.

I re-reviewed the two changed files and the relevant ExternalDatabase / MetaCache / session-aware catalog paths after the follow-up commit. The three earlier inline comments are not resubmitted: the current head no longer publishes metaCache = null, replaces Thread.threadId() with Thread.getId(), and releases/asserts the real-path test latch before calling resetMetaToUninitialized().

Critical checkpoint conclusions:

  • Goal/test proof: the PR still targets the ExternalDatabase.resetMetaToUninitialized() lock inversion with Caffeine cache loading, and the added test covers the intended deadlock path. Current GitHub CheckStyle is passing.
  • Scope/focus: the production change is narrow: move invalidateAll() outside the database monitor and avoid re-entering makeSureInitialized() from the cache-loader existence check.
  • Concurrency/lifecycle: the main cache-field lifecycle concern from the first review is addressed by keeping metaCache visible. I checked parallel getTableNullable(), getTableNamesWithLock(), refresh, and delegated session catalog paths and did not find a new PR-introduced blocker.
  • Configuration/session/compatibility: no new config or persistence format is added. Delegated Iceberg REST session lookups still bypass the shared table-name cache before reaching the new helper.
  • Parallel paths: table-name listing, table lookup by name/id, replay-only cache lookup, and refresh invalidation paths were checked at the relevant call sites.
  • Test coverage/results: the new FE unit test is focused on the deadlock regression. I did not run FE tests locally because this checkout lacks thirdparty/installed and thirdparty/installed/bin/protoc, which the FE instructions treat as a stop-build condition.
  • Persistence/transactions/data writes/observability: not applicable beyond external metadata cache invalidation; no new logging/metrics requirement found.

User focus: no additional user-provided review focus was supplied.

Subagent conclusions: optimizer-rewrite proposed optimizer-rewrite-001; the main review dismissed it with evidence because the internal namesCache invalidation window was already present before this PR and is not introduced by the changed helper. tests-session-config reported no new valuable findings. Convergence round 1 ended with both live subagents returning NO_NEW_VALUABLE_FINDINGS for the same ledger and empty final inline comment set.

Validation notes: local git diff --check on the changed files passed. GitHub PR metadata confirms the reviewed head SHA. Remote PR checks currently show CheckStyle passing; the code-review check is this run.

@starocean999

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 30.43% (7/23) 🎉
Increment coverage report
Complete coverage report

@starocean999

Copy link
Copy Markdown
Contributor Author

/review

@starocean999

Copy link
Copy Markdown
Contributor Author

run buildall

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

Automated review result for current head 9677c93eecc73db8b1e146d3ab6e478de1ed35bb: request changes.

I found one remaining blocking concurrency issue. The latest head addresses the earlier metaCache = null NPE race and the Java 17/test-latch comments, so I did not resubmit those. The remaining issue is that resetMetaToUninitialized() now releases the database monitor before invalidating the old MetaCache, which lets a concurrent reader re-mark the database initialized and read the old cache during the refresh window.

Critical checkpoint conclusions:

  • Goal/test proof: the PR targets the ExternalDatabase.resetMetaToUninitialized() lock inversion with Caffeine cache loading and adds a focused FE unit test, but the production fix still has a concurrency correctness gap.
  • Scope/focus: the change is narrow, but the cache lifecycle ordering around initialized and metaCache is the key correctness boundary.
  • Concurrency/lifecycle: this PR is explicitly concurrency-sensitive. Moving invalidateAll() outside the monitor breaks the deadlock, but it also publishes a reset state before the old cache is invalidated.
  • Configuration/session/compatibility: no new config or persistence format is added. The previous Java 17 Thread.threadId() issue is fixed on this head, and I did not find a distinct session-cache propagation issue.
  • Parallel paths: getTableNullable() and getTableNamesWithLock() are both affected because they call makeSureInitialized() and then dereference metaCache outside the monitor.
  • Test coverage/results: static checks on the changed files passed (git diff --check and line-length scan). I did not run FE tests locally because this checkout lacks thirdparty/installed/bin/protoc, which the FE instructions treat as a stop-build condition. GitHub currently shows COMPILE success and FE UT pending for this head.
  • Persistence/transactions/data writes/observability: not applicable beyond external metadata cache invalidation.

User focus: no additional user-provided review focus was supplied.

Subagent conclusions: optimizer-rewrite reported no new optimizer/planner finding after checking the diff, existing threads, and table-binding paths. tests-session-config reported no new test/session/config finding and noted the same missing local protoc validation limit. After main accepted the single M-1 inline candidate, convergence round 1 ended with both live subagents replying NO_NEW_VALUABLE_FINDINGS for the same one-comment final set.

// Do NOT null metaCache here — callers dereference it outside the monitor
// after makeSureInitialized() returns (e.g. getTableNullable, getTableNamesWithLock).
if (cacheToInvalidate != null) {
cacheToInvalidate.invalidateAll();

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.

Moving the invalidation outside the monitor fixes the Caffeine lock inversion, but this ordering now publishes initialized = false while the old metaCache is still live and not yet invalidated. A query thread can enter makeSureInitialized() in this window, see initialized == false, call buildMetaCache() as a no-op because metaCache is non-null, set initialized = true, and then getTableNullable()/getTableNamesWithLock() can read the old cache before this line invalidates it. That means REFRESH DATABASE can still allow a new reader to plan from stale table metadata during the refresh. Please publish a fresh cache/snapshot under the monitor before invalidating the old one, or otherwise keep readers from reinitializing until the old cache has been invalidated outside the monitor.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 70.00% (7/10) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17744	4078	4009	4009
q2	2023	331	204	204
q3	10337	1413	846	846
q4	4677	469	344	344
q5	7498	856	554	554
q6	184	179	137	137
q7	786	844	618	618
q8	9311	1620	1573	1573
q9	5647	4433	4407	4407
q10	6761	1765	1523	1523
q11	512	345	319	319
q12	721	543	450	450
q13	18137	3448	2746	2746
q14	269	263	254	254
q15	q16	779	783	725	725
q17	994	927	991	927
q18	7181	5756	5493	5493
q19	1291	1250	984	984
q20	765	672	581	581
q21	5872	2620	2430	2430
q22	440	363	301	301
Total cold run time: 101929 ms
Total hot run time: 29425 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4312	4246	4270	4246
q2	296	321	219	219
q3	4536	5012	4414	4414
q4	2043	2143	1378	1378
q5	4417	4288	4305	4288
q6	223	176	128	128
q7	1710	1810	1880	1810
q8	2585	2174	2203	2174
q9	8066	8093	7847	7847
q10	4757	4699	4288	4288
q11	563	408	398	398
q12	764	801	586	586
q13	3294	3681	3010	3010
q14	311	300	266	266
q15	q16	704	726	659	659
q17	1361	1299	1313	1299
q18	8132	7329	7250	7250
q19	1150	1127	1092	1092
q20	2227	2207	2001	2001
q21	5225	4612	4344	4344
q22	517	454	401	401
Total cold run time: 57193 ms
Total hot run time: 52098 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 180529 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 9677c93eecc73db8b1e146d3ab6e478de1ed35bb, data reload: false

query5	4353	652	512	512
query6	515	225	214	214
query7	4879	617	344	344
query8	348	193	180	180
query9	8792	4169	4115	4115
query10	483	375	301	301
query11	5854	2357	2126	2126
query12	171	104	101	101
query13	1256	623	441	441
query14	6300	5348	4972	4972
query14_1	4338	4381	4327	4327
query15	211	210	182	182
query16	1026	469	453	453
query17	995	726	591	591
query18	2463	486	365	365
query19	217	197	166	166
query20	116	110	108	108
query21	248	164	135	135
query22	13658	13537	13383	13383
query23	17388	16606	16114	16114
query23_1	16242	16366	16279	16279
query24	7719	1800	1308	1308
query24_1	1331	1307	1285	1285
query25	573	483	401	401
query26	1336	366	216	216
query27	2573	614	375	375
query28	4523	2036	2036	2036
query29	1100	656	493	493
query30	344	257	239	239
query31	1126	1094	990	990
query32	161	68	63	63
query33	538	314	255	255
query34	1176	1177	663	663
query35	767	788	696	696
query36	1410	1410	1201	1201
query37	156	102	92	92
query38	1874	1697	1670	1670
query39	919	908	892	892
query39_1	870	858	870	858
query40	243	160	142	142
query41	67	63	64	63
query42	98	92	90	90
query43	327	324	284	284
query44	1461	811	785	785
query45	209	183	178	178
query46	1162	1256	777	777
query47	2356	2328	2232	2232
query48	406	420	316	316
query49	583	447	325	325
query50	1054	420	342	342
query51	10847	10844	10781	10781
query52	85	87	75	75
query53	257	291	206	206
query54	277	237	237	237
query55	73	70	69	69
query56	314	300	301	300
query57	1420	1391	1326	1326
query58	283	251	250	250
query59	1567	1664	1437	1437
query60	317	272	254	254
query61	154	147	150	147
query62	694	643	552	552
query63	252	207	209	207
query64	2872	1054	864	864
query65	4887	4768	4713	4713
query66	1849	508	393	393
query67	29563	29509	29371	29371
query68	3163	1572	1020	1020
query69	402	305	278	278
query70	1052	956	936	936
query71	365	313	303	303
query72	3302	2657	2409	2409
query73	839	778	424	424
query74	5128	4959	4771	4771
query75	2617	2587	2216	2216
query76	2339	1205	806	806
query77	369	369	294	294
query78	12368	12355	11757	11757
query79	1418	1183	790	790
query80	667	555	464	464
query81	458	328	287	287
query82	595	159	128	128
query83	400	323	295	295
query84	332	163	131	131
query85	930	623	516	516
query86	376	297	283	283
query87	1840	1825	1763	1763
query88	3787	2836	2858	2836
query89	455	417	353	353
query90	1950	199	190	190
query91	196	196	164	164
query92	67	59	59	59
query93	1551	1609	1049	1049
query94	563	342	314	314
query95	777	613	473	473
query96	1048	778	350	350
query97	2700	2694	2546	2546
query98	219	207	203	203
query99	1149	1184	1039	1039
Total cold run time: 265886 ms
Total hot run time: 180529 ms

@hello-stephen

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

query1	0.00	0.00	0.00
query2	0.10	0.05	0.04
query3	0.26	0.13	0.13
query4	1.60	0.14	0.14
query5	0.24	0.23	0.26
query6	1.26	1.05	1.06
query7	0.04	0.00	0.01
query8	0.06	0.04	0.04
query9	0.38	0.31	0.31
query10	0.55	0.57	0.55
query11	0.20	0.15	0.15
query12	0.18	0.15	0.15
query13	0.48	0.47	0.48
query14	1.00	1.02	1.03
query15	0.64	0.59	0.61
query16	0.34	0.32	0.32
query17	1.15	1.10	1.10
query18	0.22	0.21	0.21
query19	2.04	1.91	1.99
query20	0.02	0.02	0.01
query21	15.46	0.21	0.13
query22	4.97	0.06	0.06
query23	16.11	0.32	0.12
query24	2.95	0.45	0.34
query25	0.11	0.07	0.04
query26	0.72	0.20	0.14
query27	0.06	0.03	0.03
query28	3.55	0.98	0.54
query29	12.47	4.04	3.25
query30	0.27	0.14	0.14
query31	2.77	0.64	0.30
query32	3.22	0.60	0.49
query33	3.15	3.20	3.28
query34	15.56	4.28	3.52
query35	3.53	3.54	3.53
query36	0.58	0.44	0.43
query37	0.09	0.06	0.07
query38	0.06	0.04	0.04
query39	0.04	0.03	0.03
query40	0.17	0.16	0.16
query41	0.09	0.03	0.03
query42	0.04	0.03	0.02
query43	0.04	0.03	0.04
Total cold run time: 96.77 s
Total hot run time: 25.05 s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants