This is an automated email from the ASF dual-hosted git repository.
Gabriel39 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 fd807abdd49 [fix](iceberg) Cleanup when drop DB (#64269)
fd807abdd49 is described below
commit fd807abdd4926795ae20a6a526be4511ce96b934
Author: Gabriel <[email protected]>
AuthorDate: Thu Jun 11 09:47:33 2026 +0800
[fix](iceberg) Cleanup when drop DB (#64269)
---
.../datasource/iceberg/IcebergMetadataOps.java | 113 +++++++++-
.../datasource/iceberg/IcebergMetadataOpTest.java | 229 +++++++++++++++++++++
2 files changed, 340 insertions(+), 2 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
index e6612f03680..bb2d1debd8b 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
@@ -44,6 +44,11 @@ import org.apache.doris.datasource.ExternalTable;
import org.apache.doris.datasource.operations.ExternalMetadataOps;
import org.apache.doris.datasource.property.metastore.IcebergRestProperties;
import org.apache.doris.datasource.property.metastore.MetastoreProperties;
+import org.apache.doris.filesystem.FileEntry;
+import org.apache.doris.filesystem.FileIterator;
+import org.apache.doris.filesystem.FileSystem;
+import org.apache.doris.filesystem.Location;
+import org.apache.doris.fs.SpiSwitchingFileSystem;
import org.apache.doris.nereids.trees.plans.commands.info.AddPartitionFieldOp;
import org.apache.doris.nereids.trees.plans.commands.info.CreateTableInfo;
import org.apache.doris.nereids.trees.plans.commands.info.DropPartitionFieldOp;
@@ -52,6 +57,7 @@ import
org.apache.doris.nereids.trees.plans.commands.info.SortFieldInfo;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.iceberg.ManageSnapshots;
import org.apache.iceberg.PartitionSpec;
@@ -79,6 +85,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -93,6 +100,8 @@ import java.util.stream.Stream;
public class IcebergMetadataOps implements ExternalMetadataOps {
private static final Logger LOG =
LogManager.getLogger(IcebergMetadataOps.class);
+ private static final String NAMESPACE_LOCATION_PROP = "location";
+ private static final List<String> ICEBERG_TABLE_LOCATION_CHILD_DIRS =
Arrays.asList("data", "metadata");
protected Catalog catalog;
protected ExternalCatalog dorisCatalog;
protected SupportsNamespaces nsCatalog;
@@ -299,7 +308,11 @@ public class IcebergMetadataOps implements
ExternalMetadataOps {
return;
}
}
- nsCatalog.dropNamespace(getNamespace(dorisDb.getRemoteName()));
+ Namespace namespace = getNamespace(dorisDb.getRemoteName());
+ Optional<String> namespaceLocation = shouldCleanupManagedLocation()
+ ? loadNamespaceLocation(namespace) : Optional.empty();
+ nsCatalog.dropNamespace(namespace);
+ namespaceLocation.ifPresent(location -> cleanupEmptyLocation(location,
"database", dbName, false));
}
@Override
@@ -427,7 +440,103 @@ public class IcebergMetadataOps implements
ExternalMetadataOps {
ErrorReport.reportDdlException(ErrorCode.ERR_UNKNOWN_TABLE,
remoteTblName, remoteDbName);
}
}
- catalog.dropTable(getTableIdentifier(remoteDbName, remoteTblName),
true);
+ TableIdentifier tableIdentifier = getTableIdentifier(remoteDbName,
remoteTblName);
+ Optional<String> tableLocation = shouldCleanupManagedLocation()
+ ? loadTableLocation(tableIdentifier) : Optional.empty();
+ catalog.dropTable(tableIdentifier, true);
+ tableLocation.ifPresent(location ->
+ cleanupEmptyLocation(location, "table", remoteDbName + "." +
remoteTblName, true));
+ }
+
+ private Optional<String> loadNamespaceLocation(Namespace namespace) {
+ Map<String, String> namespaceMetadata =
nsCatalog.loadNamespaceMetadata(namespace);
+ String location = namespaceMetadata.get(NAMESPACE_LOCATION_PROP);
+ return StringUtils.isBlank(location) ? Optional.empty() :
Optional.of(location);
+ }
+
+ private Optional<String> loadTableLocation(TableIdentifier
tableIdentifier) {
+ String location = catalog.loadTable(tableIdentifier).location();
+ return StringUtils.isBlank(location) ? Optional.empty() :
Optional.of(location);
+ }
+
+ private void cleanupEmptyLocation(
+ String location, String objectType, String objectName, boolean
cleanupIcebergTableChildren) {
+ if (!shouldCleanupManagedLocation() || StringUtils.isBlank(location)) {
+ return;
+ }
+ try (FileSystem fs = createCleanupFileSystem()) {
+ boolean deleted = cleanupIcebergTableChildren
+ ? deleteEmptyTableLocation(fs, Location.of(location))
+ : deleteEmptyDirectory(fs, Location.of(location));
+ if (deleted) {
+ LOG.info("Cleaned empty Iceberg {} location {} for {}",
objectType, location, objectName);
+ } else {
+ LOG.info("Skip cleaning Iceberg {} location {} for {}, because
it still contains files",
+ objectType, location, objectName);
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to clean Iceberg {} location {} for {} after
drop",
+ objectType, location, objectName, e);
+ }
+ }
+
+ private boolean shouldCleanupManagedLocation() {
+ // Only cleanup HMS-Iceberg location
+ return dorisCatalog instanceof IcebergExternalCatalog
+ && IcebergExternalCatalog.ICEBERG_HMS.equals(
+ ((IcebergExternalCatalog)
dorisCatalog).getIcebergCatalogType());
+ }
+
+ @VisibleForTesting
+ protected FileSystem createCleanupFileSystem() {
+ return new
SpiSwitchingFileSystem(dorisCatalog.getCatalogProperty().getStoragePropertiesMap());
+ }
+
+ @VisibleForTesting
+ static boolean deleteEmptyDirectory(FileSystem fs, Location location)
throws IOException {
+ if (!fs.exists(location)) {
+ return true;
+ }
+ List<Location> childDirectories = new ArrayList<>();
+ try (FileIterator iterator = fs.list(location)) {
+ while (iterator.hasNext()) {
+ FileEntry entry = iterator.next();
+ if (!entry.isDirectory()) {
+ return false;
+ }
+ childDirectories.add(entry.location());
+ }
+ }
+ for (Location childDirectory : childDirectories) {
+ if (!deleteEmptyDirectory(fs, childDirectory)) {
+ return false;
+ }
+ }
+ return deleteEmptyDirectoryMarker(fs, location);
+ }
+
+ @VisibleForTesting
+ static boolean deleteEmptyTableLocation(FileSystem fs, Location location)
throws IOException {
+ for (String childDir : ICEBERG_TABLE_LOCATION_CHILD_DIRS) {
+ if (!deleteEmptyDirectory(fs, location.resolve(childDir))) {
+ return false;
+ }
+ }
+ return deleteEmptyDirectory(fs, location);
+ }
+
+ private static boolean deleteEmptyDirectoryMarker(FileSystem fs, Location
location) throws IOException {
+ Location directoryMarker =
Location.of(withTrailingSlash(location.uri()));
+ try {
+ fs.delete(directoryMarker, false);
+ } catch (IOException e) {
+ return !fs.exists(location);
+ }
+ return !fs.exists(location);
+ }
+
+ private static String withTrailingSlash(String uri) {
+ return uri.endsWith("/") ? uri : uri + "/";
}
public void renameTableImpl(String dbName, String tblName, String
newTblName) throws DdlException {
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergMetadataOpTest.java
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergMetadataOpTest.java
index 2bf1ef3deb4..079a0c9c312 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergMetadataOpTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergMetadataOpTest.java
@@ -19,6 +19,15 @@ package org.apache.doris.datasource.iceberg;
import org.apache.doris.common.security.authentication.ExecutionAuthenticator;
import org.apache.doris.datasource.CatalogProperty;
+import org.apache.doris.datasource.ExternalDatabase;
+import org.apache.doris.datasource.ExternalTable;
+import org.apache.doris.filesystem.DorisInputFile;
+import org.apache.doris.filesystem.DorisOutputFile;
+import org.apache.doris.filesystem.FileEntry;
+import org.apache.doris.filesystem.FileIterator;
+import org.apache.doris.filesystem.FileSystem;
+import org.apache.doris.filesystem.Location;
+import org.apache.doris.fs.MemoryFileSystem;
import org.apache.iceberg.catalog.Catalog;
import org.apache.iceberg.catalog.Namespace;
@@ -29,12 +38,17 @@ import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
+import java.io.IOException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.NoSuchElementException;
import java.util.Optional;
+import java.util.Set;
public class IcebergMetadataOpTest {
@@ -114,4 +128,219 @@ public class IcebergMetadataOpTest {
Assert.assertEquals(Collections.singletonList("DORIS_HORIZON_T"),
tableNames);
}
+
+ @Test
+ public void testDropTableCleansEmptyTableLocation() throws Exception {
+ MemoryFileSystem fs = new MemoryFileSystem();
+ Location tableLocation = Location.of("hdfs://nn/warehouse/db/t1");
+ fs.mkdirs(tableLocation);
+ fs.mkdirs(tableLocation.resolve("data"));
+ fs.mkdirs(tableLocation.resolve("metadata"));
+
+ IcebergExternalCatalog dorisCatalog = mockHmsCatalog();
+ Catalog icebergCatalog = Mockito.mock(Catalog.class,
+
Mockito.withSettings().extraInterfaces(SupportsNamespaces.class));
+ IcebergMetadataOps ops = newOpsWithCleanupFileSystem(dorisCatalog,
icebergCatalog, fs);
+
+ TableIdentifier tableIdentifier =
TableIdentifier.of(Namespace.of("db"), "t1");
+ org.apache.iceberg.Table icebergTable =
Mockito.mock(org.apache.iceberg.Table.class);
+ Mockito.when(icebergTable.location()).thenReturn(tableLocation.uri());
+
Mockito.when(icebergCatalog.tableExists(tableIdentifier)).thenReturn(true);
+
Mockito.when(icebergCatalog.loadTable(tableIdentifier)).thenReturn(icebergTable);
+ Mockito.when(icebergCatalog.dropTable(tableIdentifier,
true)).thenReturn(true);
+
+ ExternalTable dorisTable = Mockito.mock(ExternalTable.class);
+ Mockito.when(dorisTable.getRemoteDbName()).thenReturn("db");
+ Mockito.when(dorisTable.getRemoteName()).thenReturn("t1");
+ Mockito.when(dorisTable.getName()).thenReturn("t1");
+ ops.dropTableImpl(dorisTable, false);
+
+ Assert.assertFalse(fs.exists(tableLocation));
+ Mockito.verify(icebergCatalog).dropTable(tableIdentifier, true);
+ }
+
+ @Test
+ public void testDropDbCleansEmptyNamespaceLocation() throws Exception {
+ MemoryFileSystem fs = new MemoryFileSystem();
+ Location namespaceLocation = Location.of("hdfs://nn/warehouse/db.db");
+ fs.mkdirs(namespaceLocation);
+
+ IcebergExternalCatalog dorisCatalog = mockHmsCatalog();
+ Catalog icebergCatalog = Mockito.mock(Catalog.class,
+
Mockito.withSettings().extraInterfaces(SupportsNamespaces.class));
+ IcebergMetadataOps ops = newOpsWithCleanupFileSystem(dorisCatalog,
icebergCatalog, fs);
+
+ ExternalDatabase<?> dorisDb = Mockito.mock(ExternalDatabase.class);
+ Mockito.when(dorisDb.getRemoteName()).thenReturn("db");
+ Mockito.doReturn(dorisDb).when(dorisCatalog).getDbNullable("db");
+
+ SupportsNamespaces nsCatalog = (SupportsNamespaces) icebergCatalog;
+ Namespace namespace = Namespace.of("db");
+ Mockito.when(nsCatalog.loadNamespaceMetadata(namespace))
+ .thenReturn(Collections.singletonMap("location",
namespaceLocation.uri()));
+ Mockito.when(nsCatalog.dropNamespace(namespace)).thenReturn(true);
+ ops.dropDbImpl("db", false, false);
+
+ Assert.assertFalse(fs.exists(namespaceLocation));
+ Mockito.verify(nsCatalog).dropNamespace(namespace);
+ }
+
+ @Test
+ public void testDeleteEmptyDirectoryKeepsDirectoryWithExternalFile()
throws Exception {
+ MemoryFileSystem fs = new MemoryFileSystem();
+ Location tableLocation = Location.of("hdfs://nn/warehouse/db/t2");
+ fs.mkdirs(tableLocation);
+ fs.mkdirs(tableLocation.resolve("data"));
+ Location externalFile = tableLocation.resolve("external-file");
+ fs.put(externalFile, new byte[] {1});
+
+ Assert.assertFalse(IcebergMetadataOps.deleteEmptyDirectory(fs,
tableLocation));
+ Assert.assertTrue(fs.exists(tableLocation));
+ Assert.assertTrue(fs.exists(externalFile));
+ Assert.assertTrue(fs.exists(tableLocation.resolve("data")));
+ }
+
+ @Test
+ public void testDeleteEmptyTableLocationCleansFlatObjectStoreMarkers()
throws Exception {
+ FlatMarkerFileSystem fs = new FlatMarkerFileSystem();
+ Location tableLocation = Location.of("s3://bucket/warehouse/db/t3");
+ fs.mkdirs(tableLocation);
+ fs.mkdirs(tableLocation.resolve("data"));
+ fs.mkdirs(tableLocation.resolve("metadata"));
+
+ Assert.assertTrue(fs.exists(tableLocation));
+ Assert.assertTrue(IcebergMetadataOps.deleteEmptyTableLocation(fs,
tableLocation));
+ Assert.assertFalse(fs.exists(tableLocation));
+ }
+
+ private IcebergExternalCatalog mockHmsCatalog() {
+ IcebergExternalCatalog dorisCatalog =
Mockito.mock(IcebergExternalCatalog.class);
+ Mockito.when(dorisCatalog.getExecutionAuthenticator()).thenReturn(new
ExecutionAuthenticator() {
+ });
+
Mockito.when(dorisCatalog.getProperties()).thenReturn(Collections.emptyMap());
+
Mockito.when(dorisCatalog.getIcebergCatalogType()).thenReturn(IcebergExternalCatalog.ICEBERG_HMS);
+ Mockito.when(dorisCatalog.getCatalogProperty()).thenReturn(new
CatalogProperty(null, Collections.emptyMap()));
+ return dorisCatalog;
+ }
+
+ private IcebergMetadataOps newOpsWithCleanupFileSystem(
+ IcebergExternalCatalog dorisCatalog, Catalog icebergCatalog,
FileSystem fs) {
+ IcebergMetadataOps ops = new IcebergMetadataOps(dorisCatalog,
icebergCatalog) {
+ @Override
+ protected FileSystem createCleanupFileSystem() {
+ return fs;
+ }
+ };
+ Mockito.when(dorisCatalog.getMetadataOps()).thenReturn(ops);
+ return ops;
+ }
+
+ private static class FlatMarkerFileSystem implements FileSystem {
+ private final Set<String> markers = new HashSet<>();
+ private final Set<String> files = new HashSet<>();
+
+ @Override
+ public boolean exists(Location location) {
+ String uri = location.uri();
+ String marker = withTrailingSlash(uri);
+ if (markers.contains(uri) || markers.contains(marker) ||
files.contains(uri)) {
+ return true;
+ }
+ String prefix = withTrailingSlash(uri);
+ for (String file : files) {
+ if (file.startsWith(prefix)) {
+ return true;
+ }
+ }
+ for (String directoryMarker : markers) {
+ if (directoryMarker.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void mkdirs(Location location) {
+ markers.add(withTrailingSlash(location.uri()));
+ }
+
+ @Override
+ public void delete(Location location, boolean recursive) throws
IOException {
+ if (recursive) {
+ throw new IOException("recursive delete is not fail-safe");
+ }
+ String marker = withTrailingSlash(location.uri());
+ for (String file : files) {
+ if (file.startsWith(marker) && !file.equals(marker)) {
+ throw new IOException("Directory not empty: " +
location.uri());
+ }
+ }
+ for (String directoryMarker : markers) {
+ if (directoryMarker.startsWith(marker) &&
!directoryMarker.equals(marker)) {
+ throw new IOException("Directory not empty: " +
location.uri());
+ }
+ }
+ markers.remove(marker);
+ files.remove(location.uri());
+ }
+
+ @Override
+ public void rename(Location src, Location dst) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public FileIterator list(Location location) {
+ String prefix = withTrailingSlash(location.uri());
+ List<FileEntry> entries = new ArrayList<>();
+ for (String file : files) {
+ if (file.startsWith(prefix)) {
+ entries.add(new FileEntry(Location.of(file), 1L, false,
0L, null));
+ }
+ }
+ return iteratorOf(entries);
+ }
+
+ @Override
+ public DorisInputFile newInputFile(Location location) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DorisOutputFile newOutputFile(Location location) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void close() {
+ }
+
+ private static FileIterator iteratorOf(List<FileEntry> entries) {
+ return new FileIterator() {
+ private int index = 0;
+
+ @Override
+ public boolean hasNext() {
+ return index < entries.size();
+ }
+
+ @Override
+ public FileEntry next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ return entries.get(index++);
+ }
+
+ @Override
+ public void close() {
+ }
+ };
+ }
+
+ private static String withTrailingSlash(String uri) {
+ return uri.endsWith("/") ? uri : uri + "/";
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]