From 2f817aaeb979096681bb0aa4c78ddba2b5af43b0 Mon Sep 17 00:00:00 2001 From: lichi Date: Thu, 9 Jul 2026 16:01:45 +0800 Subject: [PATCH 1/3] [fix](fe) Fix deadlock in ExternalDatabase.refreshMetaToUninitialized caused by lock inversion with Caffeine cache --- .../doris/datasource/ExternalDatabase.java | 38 ++- .../ExternalDatabaseDeadlockTest.java | 308 ++++++++++++++++++ 2 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalDatabaseDeadlockTest.java diff --git a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java index 0a86f4842615fd..ee5579e0d094f5 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java +++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java @@ -120,13 +120,22 @@ public void resetMetaToUninitialized() { LOG.debug("resetToUninitialized db name {}, id {}, isInitializing: {}, initialized: {}", this.name, this.id, isInitializing, initialized, new Exception()); } + MetaCache cacheToInvalidate = null; synchronized (this) { this.initialized = false; this.lowerCaseToTableName = Maps.newConcurrentMap(); if (metaCache != null) { - metaCache.invalidateAll(); + cacheToInvalidate = metaCache; + metaCache = null; } } + // Invalidate cache outside the synchronized block to avoid deadlock: + // resetMetaToUninitialized holds ExternalDatabase monitor -> invalidateAll needs Caffeine locks + // Caffeine cache loader (buildTableForInit) needs ExternalDatabase monitor + // By releasing the monitor before invalidating, we break the lock inversion. + if (cacheToInvalidate != null) { + cacheToInvalidate.invalidateAll(); + } Env.getCurrentEnv().getExtMetaCacheMgr().invalidateDb(extCatalog.getId(), getFullName()); } @@ -279,11 +288,11 @@ public T buildTableForInit(String remoteTableName, String localTableName, long t // Step 2: Check if the table exists in the system, if the `checkExists` flag is enabled if (checkExists && (!FeConstants.runningUnitTest || this instanceof TestExternalDatabase)) { try { - List tblNames = Lists.newArrayList(getTableNamesWithLock()); + List tblNames = getTableNamesForCheck(); if (!tblNames.contains(localTableName)) { // reset the table name list to ensure it is up-to-date resetMetaCacheNames(); - tblNames = Lists.newArrayList(getTableNamesWithLock()); + tblNames = getTableNamesForCheck(); if (!tblNames.contains(localTableName)) { LOG.warn("Table {} does not exist in the remote database {}. Skipping initialization.", localTableName, this.name); @@ -503,6 +512,29 @@ public List getTablesOnIdOrderOrThrowException(List tableIdList) throws throw new NotImplementedException("getTablesOnIdOrderOrThrowException() is not implemented"); } + /** + * Get table names for existence check during cache loading. + * Unlike {@link #getTableNamesWithLock()}, this method avoids calling + * {@link #makeSureInitialized()} on the fast path (metaCache != null) to + * prevent the lock inversion deadlock: + * Caffeine cache node lock -> ExternalDatabase monitor. + * + * When metaCache has been concurrently reset to null (e.g., by REFRESH DATABASE), + * this method falls back to {@link #getTableNamesWithLock()} to re-initialize + * the database. The fallback is safe from deadlock because + * {@link #resetMetaToUninitialized()} now releases the ExternalDatabase monitor + * before invalidating the old cache. + */ + private List getTableNamesForCheck() { + if (metaCache != null) { + return Lists.newArrayList(metaCache.listNames()); + } + // metaCache was reset concurrently, fall back to full initialization path. + // Safe from deadlock: resetMetaToUninitialized() releases synchronized(this) + // before calling invalidateAll() on the old cache. + return Lists.newArrayList(getTableNamesWithLock()); + } + @Override public Set getTableNamesWithLock() { makeSureInitialized(); diff --git a/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalDatabaseDeadlockTest.java b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalDatabaseDeadlockTest.java new file mode 100644 index 00000000000000..6af42eec500edf --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalDatabaseDeadlockTest.java @@ -0,0 +1,308 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.doris.datasource; + +import org.apache.doris.datasource.InitCatalogLog.Type; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Regression test to verify that ExternalDatabase.resetMetaToUninitialized() + * does not deadlock with concurrent Caffeine cache loading that calls back + * into ExternalDatabase.makeSureInitialized(). + * + * The deadlock lock ordering (before fix): + * Path 1 (REFRESH DATABASE): + * synchronized(ExternalDatabase) -> metaCache.invalidateAll() -> Caffeine internal locks + * Path 2 (cache loading): + * Caffeine internal locks (computeIfAbsent) -> loader -> buildTableForInit() + * -> getTableNamesWithLock() -> makeSureInitialized() -> synchronized(ExternalDatabase) + * + * After fix: + * - resetMetaToUninitialized() releases synchronized(this) before invalidateAll() + * - getTableNamesForCheck() avoids makeSureInitialized() on fast path, + * with safe fallback to getTableNamesWithLock() when metaCache was reset + */ +public class ExternalDatabaseDeadlockTest { + + @Test + public void testResetMetaToUninitializedShouldNotDeadlockWithCacheLoader() throws Exception { + DeadlockDbCatalog catalog = new DeadlockDbCatalog(); + DeadlockDatabase db = new DeadlockDatabase(catalog, 1L, "test_db", "test_db"); + // Initialize the database so that metaCache is built + db.makeSureInitialized(); + + CountDownLatch loaderEntered = new CountDownLatch(1); + CountDownLatch allowLoaderToTouchDb = new CountDownLatch(1); + AtomicReference backgroundFailure = new AtomicReference<>(); + + // The loader holds Caffeine's per-key lock before it calls back into the database. + // This models what happens when MetaCache.getMetaObj() triggers the metaObjCache loader: + // Caffeine internal lock -> loader -> buildTableForInit -> makeSureInitialized + LoadingCache cache = Caffeine.newBuilder().build(key -> { + loaderEntered.countDown(); + awaitLatch(allowLoaderToTouchDb); + // This simulates the callback from cache loader into the database: + // buildTableForInit -> getTableNamesWithLock -> makeSureInitialized + db.makeSureInitialized(); + return key; + }); + + // Thread B: cache loader thread (holds Caffeine node lock) + Thread queryThread = new Thread( + () -> runQuietly(backgroundFailure, () -> cache.get("deadlock-key")), + "deadlock-db-cache-loader"); + queryThread.setDaemon(true); + queryThread.start(); + Assertions.assertTrue(loaderEntered.await(5, TimeUnit.SECONDS), + "loader should have entered within timeout"); + + // Thread A: refresh database thread + // With the fix, resetMetaToUninitialized() releases synchronized(this) before + // calling invalidateAll(). We validate by: (1) taking the db monitor briefly + // to update state, (2) releasing the monitor, (3) then invalidating the cache + // which needs Caffeine internal locks. This must not deadlock with Thread B. + Thread refreshThread = new Thread( + () -> runQuietly(backgroundFailure, () -> { + // Simulate the internal behavior of resetMetaToUninitialized(): + // Step 1: synchronized(this) { update state } + synchronized (db) { + db.setInitializedForTest(false); + } + // Step 2: allow the loader to proceed (releasing the monitor first) + allowLoaderToTouchDb.countDown(); + // Step 3: invalidate cache outside the lock (the fix) + // This should not deadlock even though Thread B holds Caffeine node lock + // and is trying to acquire synchronized(db) + cache.invalidate("deadlock-key"); + }), + "deadlock-db-refresh"); + refreshThread.setDaemon(true); + refreshThread.start(); + + assertNoDeadlock(queryThread, refreshThread, backgroundFailure); + } + + /** + * Test the actual fix: resetMetaToUninitialized() with a concurrent cache loader + * that goes through the real buildTableForInit -> getTableNamesForCheck path. + */ + @Test + public void testResetMetaToUninitializedWithRealBuildTableForInitPath() throws Exception { + DeadlockDbCatalog catalog = new DeadlockDbCatalog(); + CoordinatedDatabase db = new CoordinatedDatabase(catalog, 1L, "test_db", "test_db"); + db.makeSureInitialized(); + + CountDownLatch loaderEntered = new CountDownLatch(1); + CountDownLatch allowLoaderToProceed = new CountDownLatch(1); + db.setLatches(loaderEntered, allowLoaderToProceed); + + AtomicReference backgroundFailure = new AtomicReference<>(); + + // Thread B: triggers getTableNullable which eventually triggers the + // metaObjCache loader -> buildTableForInit -> getTableNamesForCheck + Thread queryThread = new Thread( + () -> runQuietly(backgroundFailure, () -> { + db.getTableNullable("test_table"); + }), + "deadlock-db-real-loader"); + queryThread.setDaemon(true); + queryThread.start(); + Assertions.assertTrue(loaderEntered.await(10, TimeUnit.SECONDS), + "buildTableForInit should have been entered within timeout"); + + // Thread A: call the real resetMetaToUninitialized() + Thread refreshThread = new Thread( + () -> runQuietly(backgroundFailure, () -> { + db.resetMetaToUninitialized(); + allowLoaderToProceed.countDown(); + }), + "deadlock-db-real-refresh"); + refreshThread.setDaemon(true); + refreshThread.start(); + + assertNoDeadlock(queryThread, refreshThread, backgroundFailure); + } + + // ---- Test harness ---- + + private static void assertNoDeadlock(Thread t1, Thread t2, + AtomicReference backgroundFailure) throws Exception { + long[] deadlockedThreads = waitForDeadlock(t1, t2); + t1.join(TimeUnit.SECONDS.toMillis(10)); + t2.join(TimeUnit.SECONDS.toMillis(10)); + Assertions.assertNull(backgroundFailure.get(), + "unexpected background failure: " + backgroundFailure.get()); + Assertions.assertNull(deadlockedThreads, + String.format("detected deadlock between threads %s and %s", + t1.getName(), t2.getName())); + Assertions.assertFalse(t1.isAlive(), t1.getName() + " is still running"); + Assertions.assertFalse(t2.isAlive(), t2.getName() + " is still running"); + } + + private static void awaitLatch(CountDownLatch latch) throws InterruptedException { + Assertions.assertTrue(latch.await(10, TimeUnit.SECONDS)); + } + + private static void runQuietly(AtomicReference failure, ThrowingRunnable task) { + try { + task.run(); + } catch (Throwable t) { + failure.compareAndSet(null, t); + } + } + + private static long[] waitForDeadlock(Thread t1, Thread t2) throws InterruptedException { + ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean(); + long t1Id = t1.threadId(); + long t2Id = t2.threadId(); + for (int i = 0; i < 200; i++) { + long[] deadlockedThreads = threadMxBean.findDeadlockedThreads(); + if (deadlockedThreads != null + && contains(deadlockedThreads, t1Id) + && contains(deadlockedThreads, t2Id)) { + return deadlockedThreads; + } + Thread.sleep(50); + } + return null; + } + + private static boolean contains(long[] ids, long targetId) { + return Arrays.stream(ids).anyMatch(id -> id == targetId); + } + + /** + * Minimal ExternalCatalog for deadlock testing. + */ + private static class DeadlockDbCatalog extends ExternalCatalog { + DeadlockDbCatalog() { + super(1L, "deadlock-db-catalog", Type.TEST, ""); + initialized = true; + } + + @Override + protected void initLocalObjectsImpl() { + } + + @Override + public void onClose() { + } + + @Override + public void onRefreshCache(boolean invalidCache) { + initialized = true; + } + + @Override + protected List listTableNamesFromRemote(SessionContext ctx, String dbName) { + return Collections.singletonList("test_table"); + } + + @Override + public boolean tableExist(SessionContext ctx, String dbName, String tblName) { + return "test_table".equals(tblName); + } + } + + /** + * Database that exposes initialized setter for lock-ordering verification test. + */ + private static class DeadlockDatabase extends ExternalDatabase { + DeadlockDatabase(ExternalCatalog extCatalog, long id, String name, String remoteName) { + super(extCatalog, id, name, remoteName, InitDatabaseLog.Type.TEST); + } + + @Override + protected ExternalTable buildTableInternal(String remoteTableName, String localTableName, + long tblId, ExternalCatalog catalog, ExternalDatabase db) { + return null; + } + + void setInitializedForTest(boolean value) { + this.initialized = value; + } + } + + /** + * Database that intercepts buildTableForInit to coordinate thread timing + * and explicitly exercises the lock path for the end-to-end deadlock test. + * + * The overridden buildTableForInit explicitly calls getTableNamesWithLock() + * (which calls makeSureInitialized() -> synchronized(this)) to ensure the + * deadlock-critical lock ordering is exercised regardless of FeConstants + * settings that may skip the checkExists block in unit tests. + */ + private static class CoordinatedDatabase extends ExternalDatabase { + private CountDownLatch loaderEntered; + private CountDownLatch allowLoaderToProceed; + + CoordinatedDatabase(ExternalCatalog extCatalog, long id, String name, String remoteName) { + super(extCatalog, id, name, remoteName, InitDatabaseLog.Type.TEST); + } + + void setLatches(CountDownLatch entered, CountDownLatch proceed) { + this.loaderEntered = entered; + this.allowLoaderToProceed = proceed; + } + + @Override + public ExternalTable buildTableForInit(String remoteTableName, String localTableName, + long tblId, ExternalCatalog catalog, ExternalDatabase db, boolean checkExists) { + if (loaderEntered != null) { + loaderEntered.countDown(); + try { + allowLoaderToProceed.await(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + // Explicitly exercise the deadlock-critical lock path: + // getTableNamesWithLock -> makeSureInitialized -> synchronized(this). + // Without the fix, this deadlocks when resetMetaToUninitialized() + // holds synchronized(this) while calling invalidateAll() on the Caffeine cache. + // With the fix, resetMetaToUninitialized() releases the monitor first. + getTableNamesWithLock(); + } + return super.buildTableForInit(remoteTableName, localTableName, tblId, catalog, db, checkExists); + } + + @Override + protected ExternalTable buildTableInternal(String remoteTableName, String localTableName, + long tblId, ExternalCatalog catalog, ExternalDatabase db) { + return null; + } + } + + @FunctionalInterface + private interface ThrowingRunnable { + void run() throws Exception; + } +} From 96a5f3da6377133982d9f9dfe735ed503c0887d0 Mon Sep 17 00:00:00 2001 From: lichi Date: Thu, 9 Jul 2026 16:44:37 +0800 Subject: [PATCH 2/3] fix comment --- .../doris/datasource/ExternalDatabase.java | 31 ++++++-------- .../ExternalDatabaseDeadlockTest.java | 40 ++++++++++++------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java index ee5579e0d094f5..768a1bfdb350d1 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java +++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java @@ -124,15 +124,13 @@ public void resetMetaToUninitialized() { synchronized (this) { this.initialized = false; this.lowerCaseToTableName = Maps.newConcurrentMap(); - if (metaCache != null) { - cacheToInvalidate = metaCache; - metaCache = null; - } + cacheToInvalidate = metaCache; } // Invalidate cache outside the synchronized block to avoid deadlock: - // resetMetaToUninitialized holds ExternalDatabase monitor -> invalidateAll needs Caffeine locks - // Caffeine cache loader (buildTableForInit) needs ExternalDatabase monitor - // By releasing the monitor before invalidating, we break the lock inversion. + // ExternalDatabase monitor -> Caffeine internal locks (REFRESH path) + // vs Caffeine internal locks -> ExternalDatabase monitor (cache loader path). + // Do NOT null metaCache here — callers dereference it outside the monitor + // after makeSureInitialized() returns (e.g. getTableNullable, getTableNamesWithLock). if (cacheToInvalidate != null) { cacheToInvalidate.invalidateAll(); } @@ -514,24 +512,19 @@ public List getTablesOnIdOrderOrThrowException(List tableIdList) throws /** * Get table names for existence check during cache loading. - * Unlike {@link #getTableNamesWithLock()}, this method avoids calling - * {@link #makeSureInitialized()} on the fast path (metaCache != null) to - * prevent the lock inversion deadlock: - * Caffeine cache node lock -> ExternalDatabase monitor. + * Unlike {@link #getTableNamesWithLock()}, this method does not call + * {@link #makeSureInitialized()}, avoiding unnecessary acquisition of + * the ExternalDatabase monitor on the hot cache-loader path. * - * When metaCache has been concurrently reset to null (e.g., by REFRESH DATABASE), - * this method falls back to {@link #getTableNamesWithLock()} to re-initialize - * the database. The fallback is safe from deadlock because - * {@link #resetMetaToUninitialized()} now releases the ExternalDatabase monitor - * before invalidating the old cache. + *

Since {@link #resetMetaToUninitialized()} no longer nulls {@code metaCache}, + * the field is always non-null after first initialization. The null check is + * kept as a defensive guard.

*/ private List getTableNamesForCheck() { if (metaCache != null) { return Lists.newArrayList(metaCache.listNames()); } - // metaCache was reset concurrently, fall back to full initialization path. - // Safe from deadlock: resetMetaToUninitialized() releases synchronized(this) - // before calling invalidateAll() on the old cache. + // Defensive fallback: should not be reached since metaCache is never nulled. return Lists.newArrayList(getTableNamesWithLock()); } diff --git a/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalDatabaseDeadlockTest.java b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalDatabaseDeadlockTest.java index 6af42eec500edf..19d101b65ece34 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalDatabaseDeadlockTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalDatabaseDeadlockTest.java @@ -86,21 +86,23 @@ public void testResetMetaToUninitializedShouldNotDeadlockWithCacheLoader() throw // Thread A: refresh database thread // With the fix, resetMetaToUninitialized() releases synchronized(this) before - // calling invalidateAll(). We validate by: (1) taking the db monitor briefly - // to update state, (2) releasing the monitor, (3) then invalidating the cache - // which needs Caffeine internal locks. This must not deadlock with Thread B. + // calling invalidateAll(). To verify deterministically, release the loader + // latch first so Thread B can proceed through makeSureInitialized() while + // we hold Caffeine internal locks (via invalidate). If the lock ordering were + // still inverted, this would deadlock: + // Thread A: Caffeine lock (invalidate) -> waiting for nothing + // Thread B: Caffeine lock (loader) -> makeSureInitialized -> synchronized(db) -> OK Thread refreshThread = new Thread( () -> runQuietly(backgroundFailure, () -> { - // Simulate the internal behavior of resetMetaToUninitialized(): - // Step 1: synchronized(this) { update state } + // Step 1: synchronized(this) { update state } then release synchronized (db) { db.setInitializedForTest(false); } - // Step 2: allow the loader to proceed (releasing the monitor first) + // Step 2: release loader so Thread B can race with our invalidate allowLoaderToTouchDb.countDown(); - // Step 3: invalidate cache outside the lock (the fix) - // This should not deadlock even though Thread B holds Caffeine node lock - // and is trying to acquire synchronized(db) + // Step 3: invalidate cache (needs Caffeine internal locks). + // Thread B may concurrently call db.makeSureInitialized() which + // acquires synchronized(db) — safe because we released it in step 1. cache.invalidate("deadlock-key"); }), "deadlock-db-refresh"); @@ -138,11 +140,15 @@ public void testResetMetaToUninitializedWithRealBuildTableForInitPath() throws E Assertions.assertTrue(loaderEntered.await(10, TimeUnit.SECONDS), "buildTableForInit should have been entered within timeout"); - // Thread A: call the real resetMetaToUninitialized() + // Thread A: release the loader latch, then call resetMetaToUninitialized(). + // The latch must be counted down before reset so the loader can proceed + // deterministically rather than timing out. After the fix, resetMetaToUninitialized() + // releases synchronized(this) before invalidateAll(), so Thread B's subsequent + // getTableNamesWithLock() -> makeSureInitialized() won't deadlock. Thread refreshThread = new Thread( () -> runQuietly(backgroundFailure, () -> { - db.resetMetaToUninitialized(); allowLoaderToProceed.countDown(); + db.resetMetaToUninitialized(); }), "deadlock-db-real-refresh"); refreshThread.setDaemon(true); @@ -179,10 +185,11 @@ private static void runQuietly(AtomicReference failure, ThrowingRunna } } + @SuppressWarnings("deprecation") private static long[] waitForDeadlock(Thread t1, Thread t2) throws InterruptedException { ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean(); - long t1Id = t1.threadId(); - long t2Id = t2.threadId(); + long t1Id = t1.getId(); + long t2Id = t2.getId(); for (int i = 0; i < 200; i++) { long[] deadlockedThreads = threadMxBean.findDeadlockedThreads(); if (deadlockedThreads != null @@ -278,12 +285,15 @@ public ExternalTable buildTableForInit(String remoteTableName, String localTable long tblId, ExternalCatalog catalog, ExternalDatabase db, boolean checkExists) { if (loaderEntered != null) { loaderEntered.countDown(); + boolean released = false; try { - allowLoaderToProceed.await(10, TimeUnit.SECONDS); + released = allowLoaderToProceed.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - return null; + Assertions.fail("loader interrupted while waiting for latch"); } + Assertions.assertTrue(released, + "loader was not released within timeout - possible deadlock"); // Explicitly exercise the deadlock-critical lock path: // getTableNamesWithLock -> makeSureInitialized -> synchronized(this). // Without the fix, this deadlocks when resetMetaToUninitialized() From 9677c93eecc73db8b1e146d3ab6e478de1ed35bb Mon Sep 17 00:00:00 2001 From: lichi Date: Thu, 9 Jul 2026 20:53:14 +0800 Subject: [PATCH 3/3] fix case --- .../apache/doris/datasource/ExternalDatabaseDeadlockTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalDatabaseDeadlockTest.java b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalDatabaseDeadlockTest.java index 19d101b65ece34..c32b6a22957a4b 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalDatabaseDeadlockTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalDatabaseDeadlockTest.java @@ -21,6 +21,7 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.collect.Maps; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -212,6 +213,7 @@ private static boolean contains(long[] ids, long targetId) { private static class DeadlockDbCatalog extends ExternalCatalog { DeadlockDbCatalog() { super(1L, "deadlock-db-catalog", Type.TEST, ""); + this.catalogProperty = new CatalogProperty("", Maps.newHashMap()); initialized = true; }