This is an automated email from the ASF dual-hosted git repository.
Gabriel39 pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new e0115cac0cd [fix](catalog) safely publish Hadoop properties (#66392)
e0115cac0cd is described below
commit e0115cac0cd7fcef867801469a05ceb2b3ca1616
Author: Gabriel <[email protected]>
AuthorDate: Tue Aug 4 12:04:16 2026 +0800
[fix](catalog) safely publish Hadoop properties (#66392)
### What problem does this PR solve?
Concurrent first consumers of a catalog's Hadoop properties could
observe the cache before its initialization completed. One thread could
copy the `HashMap` while the initializer was still populating it,
causing a `ConcurrentModificationException` during external table sink
binding.
### What is changed?
- Build the Hadoop property map locally and publish it through the
volatile cache only after all entries and the filesystem cache key are
ready.
- Add a deterministic concurrency regression test that verifies readers
cannot observe a partially initialized cache.
### Validation
- `CatalogPropertyTest`
- `PaimonWriteBindingTest`
- `StoragePropertiesFsCacheFingerprintTest`
- FE Checkstyle
---
.../apache/doris/datasource/CatalogProperty.java | 20 ++-
.../tablefunction/PaimonTableValuedFunction.java | 4 +-
.../doris/datasource/CatalogPropertyTest.java | 137 +++++++++++++++++++++
3 files changed, 154 insertions(+), 7 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogProperty.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogProperty.java
index 1ca2cad83c2..b3081296eff 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogProperty.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogProperty.java
@@ -33,6 +33,7 @@ import org.apache.hadoop.conf.Configuration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -281,10 +282,13 @@ public class CatalogProperty {
* Get Hadoop properties with lazy loading, using double-check locking to
ensure thread safety
*/
public Map<String, String> getHadoopProperties() {
- if (hadoopProperties == null) {
+ // Retain the observed snapshot because invalidation may clear the
volatile cache concurrently.
+ Map<String, String> cachedProperties = hadoopProperties;
+ if (cachedProperties == null) {
synchronized (this) {
- if (hadoopProperties == null) {
- hadoopProperties = new HashMap<>();
+ cachedProperties = hadoopProperties;
+ if (cachedProperties == null) {
+ Map<String, String> result = new HashMap<>();
Map<StorageProperties.Type, StorageProperties> storageMap
= getStoragePropertiesMap();
for (StorageProperties sp : storageMap.values()) {
@@ -294,15 +298,19 @@ public class CatalogProperty {
String key = entry.getKey();
String value = entry.getValue();
if (value != null) {
- hadoopProperties.put(key, value);
+ result.put(key, value);
}
});
}
}
- StorageProperties.setCombinedFsCacheKey(hadoopProperties,
storageMap.values());
+ StorageProperties.setCombinedFsCacheKey(result,
storageMap.values());
+ // Readers share this snapshot without locking, so publish
it only when complete
+ // and keep caller-specific mutations out of the catalog
cache.
+ cachedProperties = Collections.unmodifiableMap(result);
+ hadoopProperties = cachedProperties;
}
}
}
- return hadoopProperties;
+ return cachedProperties;
}
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/PaimonTableValuedFunction.java
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/PaimonTableValuedFunction.java
index 3e7e247e865..bd7da501ccd 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/PaimonTableValuedFunction.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/PaimonTableValuedFunction.java
@@ -48,6 +48,7 @@ import org.apache.paimon.table.Table;
import org.apache.paimon.table.source.Split;
import org.apache.paimon.table.system.SystemTableLoader;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -86,7 +87,8 @@ public class PaimonTableValuedFunction extends
MetadataTableValuedFunction {
}
PaimonExternalCatalog paimonExternalCatalog = (PaimonExternalCatalog)
dorisCatalog;
- this.hadoopProps =
paimonExternalCatalog.getCatalogProperty().getHadoopProperties();
+ // Keep TVF-specific Kerberos entries isolated from the catalog's
shared immutable snapshot.
+ this.hadoopProps = new
HashMap<>(paimonExternalCatalog.getCatalogProperty().getHadoopProperties());
appendHMSKerberosProps(hadoopProps, paimonExternalCatalog);
this.hadoopAuthenticator =
paimonExternalCatalog.getExecutionAuthenticator();
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/datasource/CatalogPropertyTest.java
b/fe/fe-core/src/test/java/org/apache/doris/datasource/CatalogPropertyTest.java
new file mode 100644
index 00000000000..071505565f8
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/datasource/CatalogPropertyTest.java
@@ -0,0 +1,137 @@
+// 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.property.storage.StorageProperties;
+
+import org.apache.hadoop.conf.Configuration;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class CatalogPropertyTest {
+
+ @Test
+ public void testHadoopPropertiesArePublishedAfterInitialization() throws
Exception {
+ CountDownLatch iterationStarted = new CountDownLatch(1);
+ CountDownLatch allowIteration = new CountDownLatch(1);
+ Configuration configuration = new
BlockingConfiguration(iterationStarted, allowIteration);
+ configuration.set("fs.test.property", "complete");
+
+ StorageProperties storageProperties =
Mockito.mock(StorageProperties.class);
+
Mockito.when(storageProperties.getHadoopStorageConfig()).thenReturn(configuration);
+
Mockito.when(storageProperties.getFsCacheFingerprint()).thenReturn("test-fingerprint");
+
+ CatalogProperty catalogProperty = new CatalogProperty(null,
Collections.emptyMap()) {
+ @Override
+ public Map<StorageProperties.Type, StorageProperties>
getStoragePropertiesMap() {
+ return Collections.singletonMap(StorageProperties.Type.HDFS,
storageProperties);
+ }
+ };
+
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ AtomicReference<Map<String, String>> readerResult = new
AtomicReference<>();
+ Thread concurrentReader = new Thread(
+ () -> readerResult.set(new
HashMap<>(catalogProperty.getHadoopProperties())));
+ try {
+ Future<Map<String, String>> initializer =
executor.submit(catalogProperty::getHadoopProperties);
+ Assert.assertTrue(iterationStarted.await(5, TimeUnit.SECONDS));
+
+ concurrentReader.start();
+ Assert.assertTrue(waitUntilBlockedOrTerminated(concurrentReader,
5, TimeUnit.SECONDS));
+ Assert.assertEquals("The reader must block until initialization
publishes the completed map",
+ Thread.State.BLOCKED, concurrentReader.getState());
+
+ allowIteration.countDown();
+ Assert.assertEquals("complete", initializer.get(5,
TimeUnit.SECONDS).get("fs.test.property"));
+ concurrentReader.join(TimeUnit.SECONDS.toMillis(5));
+ Assert.assertFalse(concurrentReader.isAlive());
+ Assert.assertEquals("complete",
readerResult.get().get("fs.test.property"));
+ } finally {
+ allowIteration.countDown();
+ concurrentReader.interrupt();
+ executor.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testHadoopPropertiesCacheIsImmutable() {
+ Configuration configuration = new Configuration(false);
+ configuration.set("fs.test.property", "complete");
+
+ StorageProperties storageProperties =
Mockito.mock(StorageProperties.class);
+
Mockito.when(storageProperties.getHadoopStorageConfig()).thenReturn(configuration);
+
Mockito.when(storageProperties.getFsCacheFingerprint()).thenReturn("test-fingerprint");
+
+ CatalogProperty catalogProperty = new CatalogProperty(null,
Collections.emptyMap()) {
+ @Override
+ public Map<StorageProperties.Type, StorageProperties>
getStoragePropertiesMap() {
+ return Collections.singletonMap(StorageProperties.Type.HDFS,
storageProperties);
+ }
+ };
+
+ Map<String, String> hadoopProperties =
catalogProperty.getHadoopProperties();
+ Assert.assertThrows(UnsupportedOperationException.class,
+ () -> hadoopProperties.put("fs.test.property", "modified"));
+ }
+
+ private static boolean waitUntilBlockedOrTerminated(Thread thread, long
timeout, TimeUnit timeUnit) {
+ long deadline = System.nanoTime() + timeUnit.toNanos(timeout);
+ while (thread.isAlive() && thread.getState() != Thread.State.BLOCKED
+ && System.nanoTime() < deadline) {
+ Thread.yield();
+ }
+ return !thread.isAlive() || thread.getState() == Thread.State.BLOCKED;
+ }
+
+ private static class BlockingConfiguration extends Configuration {
+ private final CountDownLatch iterationStarted;
+ private final CountDownLatch allowIteration;
+
+ BlockingConfiguration(CountDownLatch iterationStarted, CountDownLatch
allowIteration) {
+ super(false);
+ this.iterationStarted = iterationStarted;
+ this.allowIteration = allowIteration;
+ }
+
+ @Override
+ public Iterator<Map.Entry<String, String>> iterator() {
+ iterationStarted.countDown();
+ try {
+ if (!allowIteration.await(5, TimeUnit.SECONDS)) {
+ throw new AssertionError("Timed out waiting to continue
Hadoop configuration initialization");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new AssertionError("Interrupted while initializing
Hadoop configuration", e);
+ }
+ return super.iterator();
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]