This is an automated email from the ASF dual-hosted git repository.
yiguolei 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 a8344169095 branch-4.1: [fix](iceberg) Run execute actions in catalog
auth scope (#67695)
a8344169095 is described below
commit a8344169095efbb27f305ab675a40b9f199ac756
Author: Gabriel <[email protected]>
AuthorDate: Thu Sep 10 11:19:09 2026 +0800
branch-4.1: [fix](iceberg) Run execute actions in catalog auth scope
(#67695)
## What problem does this PR solve?
Iceberg maintenance actions can load a table under the catalog
authentication context but execute metadata reads and commits after that
context has ended. On authenticated filesystems, actions such as
`expire_snapshots` and `rollback_to_snapshot` can therefore commit under
the FE process identity or splice a writable table from a new catalog
generation into a stale authentication scope.
## What is changed and how does it work?
- Capture the action's metadata operations and execution authenticator
atomically under the catalog reset monitor.
- Execute writable table acquisition and metadata mutation in that
generation's authentication scope.
- Fence writable acquisition with the captured metadata operations. If
the catalog resets before mutation starts, rebuild the action and retry
once under the new generation; commit failures are never retried.
- Preserve checked `UserException` handling across the Hadoop
authentication boundary.
- Add a deterministic two-authenticator reset test that verifies
generation A cannot load or commit after generation B replaces it.
- Cover both `rollback_to_snapshot` and `expire_snapshots` in the
Kerberos Iceberg/HDFS regression.
## Check List
- [x] Unit tests
- [x] FE Checkstyle
- [x] Regression script syntax validation
## Tests
- `ExecuteActionCommandTest`: 2 passed
- Catalog generation fence tests: 2 passed
- FE Checkstyle: 0 violations
- Kerberos regression Groovy syntax validation: passed
- Full Kerberos E2E was not run locally because it requires the
dedicated external Docker environment.
---
.../iceberg/IcebergExternalMetaCache.java | 11 +-
.../iceberg/action/BaseIcebergAction.java | 14 +-
.../action/IcebergCherrypickSnapshotAction.java | 9 +-
.../action/IcebergExecuteActionFactory.java | 42 +++-
.../action/IcebergExpireSnapshotsAction.java | 8 +-
.../iceberg/action/IcebergFastForwardAction.java | 8 +-
.../action/IcebergPublishChangesAction.java | 9 +-
.../action/IcebergRewriteDataFilesAction.java | 5 +-
.../action/IcebergRewriteManifestsAction.java | 8 +-
.../action/IcebergRollbackToSnapshotAction.java | 8 +-
.../action/IcebergRollbackToTimestampAction.java | 8 +-
.../action/IcebergSetCurrentSnapshotAction.java | 8 +-
.../trees/plans/commands/ExecuteActionCommand.java | 75 ++++++-
.../plans/commands/ExecuteActionCommandTest.java | 245 +++++++++++++++++++++
.../test_iceberg_hadoop_catalog_kerberos.groovy | 68 +++++-
15 files changed, 470 insertions(+), 56 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergExternalMetaCache.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergExternalMetaCache.java
index b58e659abfc..1cd7c5684f6 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergExternalMetaCache.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergExternalMetaCache.java
@@ -422,12 +422,19 @@ public class IcebergExternalMetaCache extends
AbstractExternalMetaCache {
}
}
- private static RuntimeException catalogGenerationMoved(NameMapping
nameMapping) {
- return new RuntimeException(String.format(
+ private static CatalogGenerationChangedException
catalogGenerationMoved(NameMapping nameMapping) {
+ return new CatalogGenerationChangedException(String.format(
"Catalog %d was reset while acquiring iceberg table %s.%s,
please retry.",
nameMapping.getCtlId(), nameMapping.getLocalDbName(),
nameMapping.getLocalTblName()));
}
+ /** Signals that writable acquisition must restart before any metadata
mutation begins. */
+ public static final class CatalogGenerationChangedException extends
RuntimeException {
+ public CatalogGenerationChangedException(String message) {
+ super(message);
+ }
+ }
+
MetaCacheSizeEstimate prepareTableForCachePublication(
NameMapping nameMapping, IcebergTableCacheValue value) {
return value.prepareForCachePublication(nameMapping);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/BaseIcebergAction.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/BaseIcebergAction.java
index b54d7509f00..550c8331ae2 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/BaseIcebergAction.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/BaseIcebergAction.java
@@ -20,11 +20,16 @@ package org.apache.doris.datasource.iceberg.action;
import org.apache.doris.catalog.TableIf;
import org.apache.doris.common.UserException;
import org.apache.doris.datasource.iceberg.IcebergExternalTable;
+import org.apache.doris.datasource.iceberg.IcebergMetadataOps;
+import org.apache.doris.datasource.iceberg.IcebergUtils;
import org.apache.doris.info.PartitionNamesInfo;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.plans.commands.execute.BaseExecuteAction;
+import org.apache.iceberg.Table;
+
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
/**
@@ -33,11 +38,13 @@ import java.util.Optional;
* functionality while inheriting common execution action behavior.
*/
public abstract class BaseIcebergAction extends BaseExecuteAction {
+ private final IcebergMetadataOps metadataOps;
protected BaseIcebergAction(String actionType, Map<String, String>
properties,
Optional<PartitionNamesInfo> partitionNamesInfo,
- Optional<Expression> whereCondition) {
+ Optional<Expression> whereCondition, IcebergMetadataOps
metadataOps) {
super(actionType, properties, partitionNamesInfo, whereCondition);
+ this.metadataOps = Objects.requireNonNull(metadataOps, "metadataOps is
null");
}
@Override
@@ -71,4 +78,9 @@ public abstract class BaseIcebergAction extends
BaseExecuteAction {
// Default implementation does nothing.
}
+ protected final Table getWritableIcebergTable(TableIf table) {
+ // The expected ops fences lazy table acquisition to the authenticator
generation selected at dispatch.
+ return IcebergUtils.getWritableIcebergTable((IcebergExternalTable)
table, metadataOps);
+ }
+
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergCherrypickSnapshotAction.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergCherrypickSnapshotAction.java
index 94924514a58..bf3be0c8559 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergCherrypickSnapshotAction.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergCherrypickSnapshotAction.java
@@ -24,7 +24,7 @@ import org.apache.doris.catalog.Type;
import org.apache.doris.common.ArgumentParsers;
import org.apache.doris.common.UserException;
import org.apache.doris.datasource.ExternalTable;
-import org.apache.doris.datasource.iceberg.IcebergExternalTable;
+import org.apache.doris.datasource.iceberg.IcebergMetadataOps;
import org.apache.doris.info.PartitionNamesInfo;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -48,8 +48,9 @@ public class IcebergCherrypickSnapshotAction extends
BaseIcebergAction {
public static final String SNAPSHOT_ID = "snapshot_id";
public IcebergCherrypickSnapshotAction(Map<String, String> properties,
- Optional<PartitionNamesInfo> partitionNamesInfo,
Optional<Expression> whereCondition) {
- super("cherrypick_snapshot", properties, partitionNamesInfo,
whereCondition);
+ Optional<PartitionNamesInfo> partitionNamesInfo,
Optional<Expression> whereCondition,
+ IcebergMetadataOps metadataOps) {
+ super("cherrypick_snapshot", properties, partitionNamesInfo,
whereCondition, metadataOps);
}
@Override
@@ -70,7 +71,7 @@ public class IcebergCherrypickSnapshotAction extends
BaseIcebergAction {
@Override
protected List<String> executeAction(TableIf table) throws UserException {
- Table icebergTable = ((IcebergExternalTable)
table).getWritableIcebergTable();
+ Table icebergTable = getWritableIcebergTable(table);
Long sourceSnapshotId = namedArguments.getLong(SNAPSHOT_ID);
try {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergExecuteActionFactory.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergExecuteActionFactory.java
index 0d09a9ef35c..db3eb223707 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergExecuteActionFactory.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergExecuteActionFactory.java
@@ -18,7 +18,12 @@
package org.apache.doris.datasource.iceberg.action;
import org.apache.doris.common.DdlException;
+import org.apache.doris.datasource.ExternalCatalog;
+import org.apache.doris.datasource.hive.HMSExternalCatalog;
+import org.apache.doris.datasource.iceberg.IcebergExternalCatalog;
+import
org.apache.doris.datasource.iceberg.IcebergExternalMetaCache.CatalogGenerationChangedException;
import org.apache.doris.datasource.iceberg.IcebergExternalTable;
+import org.apache.doris.datasource.iceberg.IcebergMetadataOps;
import org.apache.doris.info.PartitionNamesInfo;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.plans.commands.execute.ExecuteAction;
@@ -59,41 +64,60 @@ public class IcebergExecuteActionFactory {
Optional<PartitionNamesInfo> partitionNamesInfo,
Optional<Expression> whereCondition,
IcebergExternalTable table) throws DdlException {
+ IcebergMetadataOps metadataOps = getMetadataOps(table);
switch (actionType.toLowerCase()) {
case ROLLBACK_TO_SNAPSHOT:
return new IcebergRollbackToSnapshotAction(properties,
partitionNamesInfo,
- whereCondition);
+ whereCondition, metadataOps);
case ROLLBACK_TO_TIMESTAMP:
return new IcebergRollbackToTimestampAction(properties,
partitionNamesInfo,
- whereCondition);
+ whereCondition, metadataOps);
case SET_CURRENT_SNAPSHOT:
return new IcebergSetCurrentSnapshotAction(properties,
partitionNamesInfo,
- whereCondition);
+ whereCondition, metadataOps);
case CHERRYPICK_SNAPSHOT:
return new IcebergCherrypickSnapshotAction(properties,
partitionNamesInfo,
- whereCondition);
+ whereCondition, metadataOps);
case FAST_FORWARD:
return new IcebergFastForwardAction(properties,
partitionNamesInfo,
- whereCondition);
+ whereCondition, metadataOps);
case EXPIRE_SNAPSHOTS:
return new IcebergExpireSnapshotsAction(properties,
partitionNamesInfo,
- whereCondition);
+ whereCondition, metadataOps);
case REWRITE_DATA_FILES:
return new IcebergRewriteDataFilesAction(properties,
partitionNamesInfo,
- whereCondition);
+ whereCondition, metadataOps);
case PUBLISH_CHANGES:
return new IcebergPublishChangesAction(properties,
partitionNamesInfo,
- whereCondition);
+ whereCondition, metadataOps);
case REWRITE_MANIFESTS:
return new IcebergRewriteManifestsAction(properties,
partitionNamesInfo,
- whereCondition);
+ whereCondition, metadataOps);
default:
throw new DdlException("Unsupported Iceberg procedure: " +
actionType
+ ". Supported procedures: " + String.join(", ",
getSupportedActions()));
}
}
+ private static IcebergMetadataOps getMetadataOps(IcebergExternalTable
table) throws DdlException {
+ ExternalCatalog catalog = table.getCatalog();
+ IcebergMetadataOps metadataOps;
+ if (catalog instanceof HMSExternalCatalog) {
+ metadataOps = ((HMSExternalCatalog)
catalog).getIcebergMetadataOps();
+ } else if (catalog instanceof IcebergExternalCatalog) {
+ metadataOps = (IcebergMetadataOps) catalog.getMetadataOps();
+ } else {
+ throw new DdlException("Unsupported catalog type for Iceberg
execute action: "
+ + catalog.getClass().getSimpleName());
+ }
+ if (metadataOps == null) {
+ throw new CatalogGenerationChangedException(
+ "Catalog was reset while preparing Iceberg execute
action");
+ }
+ return metadataOps;
+ }
+
/**
* Get supported Iceberg procedure names.
*
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergExpireSnapshotsAction.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergExpireSnapshotsAction.java
index 82a93022354..f2653f39216 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergExpireSnapshotsAction.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergExpireSnapshotsAction.java
@@ -25,7 +25,7 @@ import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.ArgumentParsers;
import org.apache.doris.common.UserException;
import org.apache.doris.datasource.ExternalTable;
-import org.apache.doris.datasource.iceberg.IcebergExternalTable;
+import org.apache.doris.datasource.iceberg.IcebergMetadataOps;
import org.apache.doris.info.PartitionNamesInfo;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -69,8 +69,8 @@ public class IcebergExpireSnapshotsAction extends
BaseIcebergAction {
public IcebergExpireSnapshotsAction(Map<String, String> properties,
Optional<PartitionNamesInfo> partitionNamesInfo,
- Optional<Expression> whereCondition) {
- super("expire_snapshots", properties, partitionNamesInfo,
whereCondition);
+ Optional<Expression> whereCondition, IcebergMetadataOps
metadataOps) {
+ super("expire_snapshots", properties, partitionNamesInfo,
whereCondition, metadataOps);
}
@Override
@@ -149,7 +149,7 @@ public class IcebergExpireSnapshotsAction extends
BaseIcebergAction {
@Override
protected List<String> executeAction(TableIf table) throws UserException {
- Table icebergTable = ((IcebergExternalTable)
table).getWritableIcebergTable();
+ Table icebergTable = getWritableIcebergTable(table);
// Parse parameters
String olderThan = namedArguments.getString(OLDER_THAN);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergFastForwardAction.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergFastForwardAction.java
index cd746a7dbe6..82a0d746537 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergFastForwardAction.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergFastForwardAction.java
@@ -24,7 +24,7 @@ import org.apache.doris.catalog.Type;
import org.apache.doris.common.ArgumentParsers;
import org.apache.doris.common.UserException;
import org.apache.doris.datasource.ExternalTable;
-import org.apache.doris.datasource.iceberg.IcebergExternalTable;
+import org.apache.doris.datasource.iceberg.IcebergMetadataOps;
import org.apache.doris.info.PartitionNamesInfo;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -46,8 +46,8 @@ public class IcebergFastForwardAction extends
BaseIcebergAction {
public IcebergFastForwardAction(Map<String, String> properties,
Optional<PartitionNamesInfo> partitionNamesInfo,
- Optional<Expression> whereCondition) {
- super("fast_forward", properties, partitionNamesInfo, whereCondition);
+ Optional<Expression> whereCondition, IcebergMetadataOps
metadataOps) {
+ super("fast_forward", properties, partitionNamesInfo, whereCondition,
metadataOps);
}
@Override
@@ -70,7 +70,7 @@ public class IcebergFastForwardAction extends
BaseIcebergAction {
@Override
protected List<String> executeAction(TableIf table) throws UserException {
- Table icebergTable = ((IcebergExternalTable)
table).getWritableIcebergTable();
+ Table icebergTable = getWritableIcebergTable(table);
String sourceBranch = namedArguments.getString(BRANCH);
String desBranch = namedArguments.getString(TO);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergPublishChangesAction.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergPublishChangesAction.java
index bf3f116d1cb..7fd7fbb710a 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergPublishChangesAction.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergPublishChangesAction.java
@@ -24,7 +24,7 @@ import org.apache.doris.catalog.Type;
import org.apache.doris.common.ArgumentParsers;
import org.apache.doris.common.UserException;
import org.apache.doris.datasource.ExternalTable;
-import org.apache.doris.datasource.iceberg.IcebergExternalTable;
+import org.apache.doris.datasource.iceberg.IcebergMetadataOps;
import org.apache.doris.info.PartitionNamesInfo;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -47,8 +47,9 @@ public class IcebergPublishChangesAction extends
BaseIcebergAction {
private static final String WAP_ID_PROP = "wap.id";
public IcebergPublishChangesAction(Map<String, String> properties,
- Optional<PartitionNamesInfo> partitionNamesInfo,
Optional<Expression> whereCondition) {
- super("publish_changes", properties, partitionNamesInfo,
whereCondition);
+ Optional<PartitionNamesInfo> partitionNamesInfo,
Optional<Expression> whereCondition,
+ IcebergMetadataOps metadataOps) {
+ super("publish_changes", properties, partitionNamesInfo,
whereCondition, metadataOps);
}
@Override
@@ -66,7 +67,7 @@ public class IcebergPublishChangesAction extends
BaseIcebergAction {
@Override
protected List<String> executeAction(TableIf table) throws UserException {
- Table icebergTable = ((IcebergExternalTable)
table).getWritableIcebergTable();
+ Table icebergTable = getWritableIcebergTable(table);
String targetWapId = namedArguments.getString(WAP_ID);
// Find the target WAP snapshot
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRewriteDataFilesAction.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRewriteDataFilesAction.java
index a22397a146b..940431d04c5 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRewriteDataFilesAction.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRewriteDataFilesAction.java
@@ -23,6 +23,7 @@ import org.apache.doris.catalog.Type;
import org.apache.doris.common.ArgumentParsers;
import org.apache.doris.common.UserException;
import org.apache.doris.datasource.iceberg.IcebergExternalTable;
+import org.apache.doris.datasource.iceberg.IcebergMetadataOps;
import org.apache.doris.datasource.iceberg.IcebergUtils;
import org.apache.doris.datasource.iceberg.rewrite.RewriteDataFileExecutor;
import org.apache.doris.datasource.iceberg.rewrite.RewriteDataFilePlanner;
@@ -75,8 +76,8 @@ public class IcebergRewriteDataFilesAction extends
BaseIcebergAction {
public IcebergRewriteDataFilesAction(Map<String, String> properties,
Optional<PartitionNamesInfo> partitionNamesInfo,
- Optional<Expression> whereCondition) {
- super("rewrite_data_files", properties, partitionNamesInfo,
whereCondition);
+ Optional<Expression> whereCondition, IcebergMetadataOps
metadataOps) {
+ super("rewrite_data_files", properties, partitionNamesInfo,
whereCondition, metadataOps);
}
/**
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRewriteManifestsAction.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRewriteManifestsAction.java
index dce45c27296..9af02f29c39 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRewriteManifestsAction.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRewriteManifestsAction.java
@@ -23,7 +23,7 @@ import org.apache.doris.catalog.Type;
import org.apache.doris.common.ArgumentParsers;
import org.apache.doris.common.UserException;
import org.apache.doris.datasource.ExternalTable;
-import org.apache.doris.datasource.iceberg.IcebergExternalTable;
+import org.apache.doris.datasource.iceberg.IcebergMetadataOps;
import org.apache.doris.datasource.iceberg.rewrite.RewriteManifestExecutor;
import org.apache.doris.info.PartitionNamesInfo;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -47,8 +47,8 @@ public class IcebergRewriteManifestsAction extends
BaseIcebergAction {
public IcebergRewriteManifestsAction(Map<String, String> properties,
Optional<PartitionNamesInfo> partitionNamesInfo,
- Optional<Expression> whereCondition) {
- super("rewrite_manifests", properties, partitionNamesInfo,
whereCondition);
+ Optional<Expression> whereCondition, IcebergMetadataOps
metadataOps) {
+ super("rewrite_manifests", properties, partitionNamesInfo,
whereCondition, metadataOps);
}
@Override
@@ -67,8 +67,8 @@ public class IcebergRewriteManifestsAction extends
BaseIcebergAction {
@Override
protected List<String> executeAction(TableIf table) throws UserException {
+ Table icebergTable = getWritableIcebergTable(table);
try {
- Table icebergTable = ((IcebergExternalTable)
table).getWritableIcebergTable();
Snapshot current = icebergTable.currentSnapshot();
if (current == null) {
// No current snapshot means the table is empty, no manifests
to rewrite
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRollbackToSnapshotAction.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRollbackToSnapshotAction.java
index a5609f83439..c8fef0e9529 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRollbackToSnapshotAction.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRollbackToSnapshotAction.java
@@ -24,7 +24,7 @@ import org.apache.doris.catalog.Type;
import org.apache.doris.common.ArgumentParsers;
import org.apache.doris.common.UserException;
import org.apache.doris.datasource.ExternalTable;
-import org.apache.doris.datasource.iceberg.IcebergExternalTable;
+import org.apache.doris.datasource.iceberg.IcebergMetadataOps;
import org.apache.doris.info.PartitionNamesInfo;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -46,8 +46,8 @@ public class IcebergRollbackToSnapshotAction extends
BaseIcebergAction {
public IcebergRollbackToSnapshotAction(Map<String, String> properties,
Optional<PartitionNamesInfo> partitionNamesInfo,
- Optional<Expression> whereCondition) {
- super("rollback_to_snapshot", properties, partitionNamesInfo,
whereCondition);
+ Optional<Expression> whereCondition, IcebergMetadataOps
metadataOps) {
+ super("rollback_to_snapshot", properties, partitionNamesInfo,
whereCondition, metadataOps);
}
@Override
@@ -68,7 +68,7 @@ public class IcebergRollbackToSnapshotAction extends
BaseIcebergAction {
@Override
protected List<String> executeAction(TableIf table) throws UserException {
- Table icebergTable = ((IcebergExternalTable)
table).getWritableIcebergTable();
+ Table icebergTable = getWritableIcebergTable(table);
Long targetSnapshotId = namedArguments.getLong(SNAPSHOT_ID);
Snapshot targetSnapshot = icebergTable.snapshot(targetSnapshotId);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRollbackToTimestampAction.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRollbackToTimestampAction.java
index de7e2a68079..948e27ec245 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRollbackToTimestampAction.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergRollbackToTimestampAction.java
@@ -24,7 +24,7 @@ import org.apache.doris.catalog.Type;
import org.apache.doris.common.UserException;
import org.apache.doris.common.util.TimeUtils;
import org.apache.doris.datasource.ExternalTable;
-import org.apache.doris.datasource.iceberg.IcebergExternalTable;
+import org.apache.doris.datasource.iceberg.IcebergMetadataOps;
import org.apache.doris.info.PartitionNamesInfo;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -49,8 +49,8 @@ public class IcebergRollbackToTimestampAction extends
BaseIcebergAction {
public IcebergRollbackToTimestampAction(Map<String, String> properties,
Optional<PartitionNamesInfo> partitionNamesInfo,
- Optional<Expression> whereCondition) {
- super("rollback_to_timestamp", properties, partitionNamesInfo,
whereCondition);
+ Optional<Expression> whereCondition, IcebergMetadataOps
metadataOps) {
+ super("rollback_to_timestamp", properties, partitionNamesInfo,
whereCondition, metadataOps);
}
@Override
@@ -96,7 +96,7 @@ public class IcebergRollbackToTimestampAction extends
BaseIcebergAction {
@Override
protected List<String> executeAction(TableIf table) throws UserException {
- Table icebergTable = ((IcebergExternalTable)
table).getWritableIcebergTable();
+ Table icebergTable = getWritableIcebergTable(table);
String timestampStr = namedArguments.getString(TIMESTAMP);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergSetCurrentSnapshotAction.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergSetCurrentSnapshotAction.java
index 5b2c5bd220e..c16ef354673 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergSetCurrentSnapshotAction.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/action/IcebergSetCurrentSnapshotAction.java
@@ -25,7 +25,7 @@ import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.ArgumentParsers;
import org.apache.doris.common.UserException;
import org.apache.doris.datasource.ExternalTable;
-import org.apache.doris.datasource.iceberg.IcebergExternalTable;
+import org.apache.doris.datasource.iceberg.IcebergMetadataOps;
import org.apache.doris.info.PartitionNamesInfo;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -48,8 +48,8 @@ public class IcebergSetCurrentSnapshotAction extends
BaseIcebergAction {
public IcebergSetCurrentSnapshotAction(Map<String, String> properties,
Optional<PartitionNamesInfo> partitionNamesInfo,
- Optional<Expression> whereCondition) {
- super("set_current_snapshot", properties, partitionNamesInfo,
whereCondition);
+ Optional<Expression> whereCondition, IcebergMetadataOps
metadataOps) {
+ super("set_current_snapshot", properties, partitionNamesInfo,
whereCondition, metadataOps);
}
@Override
@@ -87,7 +87,7 @@ public class IcebergSetCurrentSnapshotAction extends
BaseIcebergAction {
@Override
protected List<String> executeAction(TableIf table) throws UserException {
- Table icebergTable = ((IcebergExternalTable)
table).getWritableIcebergTable();
+ Table icebergTable = getWritableIcebergTable(table);
Snapshot previousSnapshot = icebergTable.currentSnapshot();
Long previousSnapshotId = previousSnapshot != null ?
previousSnapshot.snapshotId() : null;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteActionCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteActionCommand.java
index edb3a059078..dd7e4b412bf 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteActionCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteActionCommand.java
@@ -24,9 +24,12 @@ import org.apache.doris.catalog.TableIf;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.UserException;
+import org.apache.doris.common.security.authentication.ExecutionAuthenticator;
import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.datasource.ExternalCatalog;
import org.apache.doris.datasource.ExternalObjectLog;
import org.apache.doris.datasource.ExternalTable;
+import
org.apache.doris.datasource.iceberg.IcebergExternalMetaCache.CatalogGenerationChangedException;
import org.apache.doris.info.PartitionNamesInfo;
import org.apache.doris.info.TableNameInfo;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -47,6 +50,7 @@ import java.util.Optional;
* [WHERE condition]
*/
public class ExecuteActionCommand extends Command implements ForwardWithSync {
+ private static final int MAX_CATALOG_GENERATION_RETRIES = 1;
private final TableNameInfo tableNameInfo;
private final String actionName;
private final Map<String, String> properties;
@@ -96,15 +100,7 @@ public class ExecuteActionCommand extends Command
implements ForwardWithSync {
}
try {
- ExecuteAction action = ExecuteActionFactory.createAction(
- actionName, properties, partitionNamesInfo,
whereCondition, table);
-
- if (!action.isSupported(table)) {
- throw new AnalysisException("Action '" + actionName + "' is
not supported for this table engine");
- }
-
- action.validate(tableNameInfo, ctx.getCurrentUserIdentity());
- ResultSet resultSet = action.execute(table);
+ ResultSet resultSet = executeWithCatalogGenerationRetry(ctx,
table);
logRefreshTable(table, System.currentTimeMillis());
if (resultSet != null) {
executor.sendResultSet(resultSet);
@@ -144,6 +140,54 @@ public class ExecuteActionCommand extends Command
implements ForwardWithSync {
return whereCondition;
}
+ private ResultSet executeWithCatalogGenerationRetry(ConnectContext ctx,
TableIf table) throws Exception {
+ ExternalTable externalTable = (ExternalTable) table;
+ ExternalCatalog catalog = externalTable.getCatalog();
+ for (int retry = 0; ; retry++) {
+ try {
+ ExecuteAction action;
+ ExecutionAuthenticator authenticator;
+ synchronized (catalog) {
+ // Reset also holds this monitor, so the action's metadata
ops and
+ // authenticator form one generation.
+ catalog.makeSureInitialized();
+ action = ExecuteActionFactory.createAction(
+ actionName, properties, partitionNamesInfo,
whereCondition, table);
+ authenticator = catalog.getExecutionAuthenticator();
+ }
+ if (!action.isSupported(table)) {
+ throw new AnalysisException("Action '" + actionName + "'
is not supported for this table engine");
+ }
+ action.validate(tableNameInfo, ctx.getCurrentUserIdentity());
+ return executeAuthenticated(action, externalTable,
authenticator);
+ } catch (CatalogGenerationChangedException e) {
+ // A generation fence fails before mutation, so rebuilding the
action is safe;
+ // never retry commit errors.
+ if (retry >= MAX_CATALOG_GENERATION_RETRIES) {
+ throw new UserException(e.getMessage(), e);
+ }
+ }
+ }
+ }
+
+ private ResultSet executeAuthenticated(ExecuteAction action, ExternalTable
table,
+ ExecutionAuthenticator authenticator) throws Exception {
+ try {
+ // Iceberg tables retain filesystem configuration, not the
caller's UGI, so loading the table and
+ // committing its metadata must stay within one catalog
authentication scope.
+ return authenticator.execute(() -> {
+ try {
+ return action.execute(table);
+ } catch (UserException e) {
+ // Hadoop doAs obscures checked exceptions, so carry this
one across as a runtime exception.
+ throw new AuthenticatedActionException(e);
+ }
+ });
+ } catch (AuthenticatedActionException e) {
+ throw e.getUserException();
+ }
+ }
+
/**
* Log refresh table to make follow fe metadata cache refresh.
*
@@ -164,4 +208,17 @@ public class ExecuteActionCommand extends Command
implements ForwardWithSync {
throw new UserException("Unsupported table type: " +
table.getClass().getName() + " for refresh table");
}
}
+
+ private static final class AuthenticatedActionException extends
RuntimeException {
+ private final UserException userException;
+
+ private AuthenticatedActionException(UserException userException) {
+ super(userException);
+ this.userException = userException;
+ }
+
+ private UserException getUserException() {
+ return userException;
+ }
+ }
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ExecuteActionCommandTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ExecuteActionCommandTest.java
new file mode 100644
index 00000000000..2ed4d7a6cdf
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ExecuteActionCommandTest.java
@@ -0,0 +1,245 @@
+// 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.nereids.trees.plans.commands;
+
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.catalog.DatabaseIf;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.common.security.authentication.ExecutionAuthenticator;
+import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.datasource.CatalogMgr;
+import org.apache.doris.datasource.ExternalMetaCacheMgr;
+import org.apache.doris.datasource.NameMapping;
+import org.apache.doris.datasource.iceberg.IcebergExternalCatalog;
+import org.apache.doris.datasource.iceberg.IcebergExternalMetaCache;
+import org.apache.doris.datasource.iceberg.IcebergExternalTable;
+import org.apache.doris.datasource.iceberg.IcebergMetadataOps;
+import org.apache.doris.info.TableNameInfo;
+import org.apache.doris.mysql.privilege.AccessControllerManager;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.trees.plans.commands.execute.ExecuteAction;
+import
org.apache.doris.nereids.trees.plans.commands.execute.ExecuteActionFactory;
+import org.apache.doris.persist.EditLog;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.StmtExecutor;
+
+import org.apache.iceberg.ManageSnapshots;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.Table;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+class ExecuteActionCommandTest {
+
+ @Test
+ void executesActionWithinCatalogAuthenticationScope() throws Exception {
+ Env env = Mockito.mock(Env.class);
+ CatalogMgr catalogMgr = Mockito.mock(CatalogMgr.class);
+ CatalogIf<?> catalog = Mockito.mock(CatalogIf.class);
+ DatabaseIf<?> database = Mockito.mock(DatabaseIf.class);
+ IcebergExternalCatalog externalCatalog =
Mockito.mock(IcebergExternalCatalog.class);
+ IcebergExternalTable table = Mockito.mock(IcebergExternalTable.class);
+ ExecuteAction action = Mockito.mock(ExecuteAction.class);
+ TableNameInfo tableName = Mockito.mock(TableNameInfo.class);
+ ConnectContext context = Mockito.mock(ConnectContext.class);
+ StmtExecutor executor = Mockito.mock(StmtExecutor.class);
+ EditLog editLog = Mockito.mock(EditLog.class);
+ RecordingAuthenticator authenticator = new RecordingAuthenticator();
+ AtomicBoolean actionExecutedInScope = new AtomicBoolean();
+
+ Mockito.when(env.getCatalogMgr()).thenReturn(catalogMgr);
+ Mockito.when(env.getEditLog()).thenReturn(editLog);
+ Mockito.when(tableName.getCtl()).thenReturn("iceberg");
+ Mockito.when(tableName.getDb()).thenReturn("db");
+ Mockito.when(tableName.getTbl()).thenReturn("tbl");
+ Mockito.when(catalogMgr.getCatalog("iceberg")).thenReturn(catalog);
+ Mockito.doReturn(database).when(catalog).getDbNullable("db");
+ Mockito.doReturn(table).when(database).getTableNullable("tbl");
+ Mockito.when(table.getCatalog()).thenReturn(externalCatalog);
+ Mockito.when(table.getDbName()).thenReturn("db");
+ Mockito.when(table.getName()).thenReturn("tbl");
+
Mockito.when(externalCatalog.getExecutionAuthenticator()).thenReturn(authenticator);
+ Mockito.when(action.isSupported(table)).thenReturn(true);
+ Mockito.when(action.execute(table)).thenAnswer(invocation -> {
+ actionExecutedInScope.set(authenticator.inScope);
+ return null;
+ });
+
+ ExecuteActionCommand command = new ExecuteActionCommand(tableName,
"expire_snapshots",
+ Collections.emptyMap(), Optional.empty(), Optional.empty());
+
+ try (MockedStatic<Env> envMock = Mockito.mockStatic(Env.class);
+ MockedStatic<ExecuteActionFactory> factoryMock =
Mockito.mockStatic(ExecuteActionFactory.class)) {
+ envMock.when(Env::getCurrentEnv).thenReturn(env);
+ factoryMock.when(() -> ExecuteActionFactory.createAction(
+ "expire_snapshots", Collections.emptyMap(),
Optional.empty(), Optional.empty(), table))
+ .thenReturn(action);
+
+ command.run(context, executor);
+ }
+
+ Assertions.assertEquals(1, authenticator.executionCount);
+ Assertions.assertTrue(actionExecutedInScope.get());
+ }
+
+ @Test
+ void retriesRollbackOnCatalogAuthenticationGenerationChange() throws
Exception {
+ Env env = Mockito.mock(Env.class);
+ CatalogMgr catalogMgr = Mockito.mock(CatalogMgr.class);
+ CatalogIf<?> commandCatalog = Mockito.mock(CatalogIf.class);
+ DatabaseIf<?> database = Mockito.mock(DatabaseIf.class);
+ IcebergExternalCatalog externalCatalog =
Mockito.mock(IcebergExternalCatalog.class);
+ IcebergExternalTable table = Mockito.mock(IcebergExternalTable.class);
+ IcebergMetadataOps generationOneOps =
Mockito.mock(IcebergMetadataOps.class);
+ IcebergMetadataOps generationTwoOps =
Mockito.mock(IcebergMetadataOps.class);
+ Table generationTwoTable = Mockito.mock(Table.class);
+ Snapshot targetSnapshot = Mockito.mock(Snapshot.class);
+ Snapshot previousSnapshot = Mockito.mock(Snapshot.class);
+ ManageSnapshots manageSnapshots = Mockito.mock(ManageSnapshots.class);
+ TableNameInfo tableName = Mockito.mock(TableNameInfo.class);
+ ConnectContext context = Mockito.mock(ConnectContext.class);
+ UserIdentity userIdentity = Mockito.mock(UserIdentity.class);
+ StmtExecutor executor = Mockito.mock(StmtExecutor.class);
+ EditLog editLog = Mockito.mock(EditLog.class);
+ AccessControllerManager accessManager =
Mockito.mock(AccessControllerManager.class);
+ ExternalMetaCacheMgr externalMetaCacheMgr =
Mockito.mock(ExternalMetaCacheMgr.class);
+ AtomicReference<ExecutionAuthenticator> currentAuthenticator = new
AtomicReference<>();
+ AtomicReference<IcebergMetadataOps> currentOps = new
AtomicReference<>(generationOneOps);
+ AtomicReference<ExecutionAuthenticator> activeAuthenticator = new
AtomicReference<>();
+ RecordingAuthenticator generationTwo = new
RecordingAuthenticator(activeAuthenticator, null);
+ RecordingAuthenticator generationOne = new
RecordingAuthenticator(activeAuthenticator, () -> {
+ currentOps.set(generationTwoOps);
+ currentAuthenticator.set(generationTwo);
+ });
+ currentAuthenticator.set(generationOne);
+ Map<String, String> properties =
Collections.singletonMap("snapshot_id", "123");
+ NameMapping mapping = new NameMapping(1L, "test_db", "test_table",
"remote_db", "remote_table");
+ ExecutorService cacheExecutor = Executors.newSingleThreadExecutor();
+ IcebergExternalMetaCache cache = new
IcebergExternalMetaCache(cacheExecutor) {
+ @Override
+ protected CatalogIf<?> getCatalog(long catalogId) {
+ return externalCatalog;
+ }
+ };
+
+ Mockito.when(env.getCatalogMgr()).thenReturn(catalogMgr);
+ Mockito.when(env.getEditLog()).thenReturn(editLog);
+ Mockito.when(env.getAccessManager()).thenReturn(accessManager);
+
Mockito.when(env.getExtMetaCacheMgr()).thenReturn(externalMetaCacheMgr);
+ Mockito.when(tableName.getCtl()).thenReturn("iceberg_catalog");
+ Mockito.when(tableName.getDb()).thenReturn("test_db");
+ Mockito.when(tableName.getTbl()).thenReturn("test_table");
+
Mockito.when(catalogMgr.getCatalog("iceberg_catalog")).thenReturn(commandCatalog);
+
Mockito.doReturn(database).when(commandCatalog).getDbNullable("test_db");
+ Mockito.doReturn(table).when(database).getTableNullable("test_table");
+ Mockito.when(table.getCatalog()).thenReturn(externalCatalog);
+ Mockito.when(externalCatalog.getId()).thenReturn(1L);
+ Mockito.when(table.getDbName()).thenReturn("test_db");
+ Mockito.when(table.getName()).thenReturn("test_table");
+ Mockito.when(table.getOrBuildNameMapping()).thenReturn(mapping);
+
Mockito.when(context.getCurrentUserIdentity()).thenReturn(userIdentity);
+
Mockito.when(accessManager.checkTblPriv(Mockito.nullable(ConnectContext.class),
+ Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
Mockito.eq(PrivPredicate.ALTER)))
+ .thenReturn(true);
+ Mockito.when(externalCatalog.getExecutionAuthenticator()).thenAnswer(
+ invocation -> currentAuthenticator.get());
+ Mockito.when(externalCatalog.getMetadataOps()).thenAnswer(invocation
-> currentOps.get());
+
Mockito.when(generationOneOps.getExecutionAuthenticator()).thenReturn(generationOne);
+
Mockito.when(generationTwoOps.getExecutionAuthenticator()).thenReturn(generationTwo);
+ Mockito.when(generationTwoOps.loadTable("remote_db",
"remote_table")).thenAnswer(invocation -> {
+ Assertions.assertSame(generationTwo, activeAuthenticator.get());
+ return generationTwoTable;
+ });
+
Mockito.when(generationTwoTable.snapshot(123L)).thenReturn(targetSnapshot);
+
Mockito.when(generationTwoTable.currentSnapshot()).thenReturn(previousSnapshot);
+ Mockito.when(previousSnapshot.snapshotId()).thenReturn(456L);
+
Mockito.when(generationTwoTable.manageSnapshots()).thenReturn(manageSnapshots);
+
Mockito.when(manageSnapshots.rollbackTo(123L)).thenReturn(manageSnapshots);
+ ExecuteActionCommand command = new ExecuteActionCommand(tableName,
"rollback_to_snapshot",
+ properties, Optional.empty(), Optional.empty());
+
+ try {
+ cache.initCatalog(1L, Collections.emptyMap());
+ Mockito.when(externalMetaCacheMgr.iceberg(1L)).thenReturn(cache);
+ try (MockedStatic<Env> envMock = Mockito.mockStatic(Env.class)) {
+ envMock.when(Env::getCurrentEnv).thenReturn(env);
+
+ command.run(context, executor);
+ }
+
+ Assertions.assertEquals(1, generationOne.executionCount);
+ Assertions.assertEquals(2, generationTwo.executionCount);
+ Mockito.verify(generationOneOps,
Mockito.never()).loadTable(Mockito.anyString(), Mockito.anyString());
+ Mockito.verify(generationTwoOps).loadTable("remote_db",
"remote_table");
+ Mockito.verify(manageSnapshots).commit();
+ } finally {
+ cache.close();
+ cacheExecutor.shutdownNow();
+ }
+ }
+
+ private static class RecordingAuthenticator implements
ExecutionAuthenticator {
+ private boolean inScope;
+ private int executionCount;
+ private final AtomicReference<ExecutionAuthenticator>
activeAuthenticator;
+ private final Runnable beforeTask;
+
+ private RecordingAuthenticator() {
+ this(null, null);
+ }
+
+ private RecordingAuthenticator(AtomicReference<ExecutionAuthenticator>
activeAuthenticator,
+ Runnable beforeTask) {
+ this.activeAuthenticator = activeAuthenticator;
+ this.beforeTask = beforeTask;
+ }
+
+ @Override
+ public <T> T execute(Callable<T> task) throws Exception {
+ executionCount++;
+ inScope = true;
+ ExecutionAuthenticator previousAuthenticator = null;
+ if (activeAuthenticator != null) {
+ previousAuthenticator = activeAuthenticator.get();
+ activeAuthenticator.set(this);
+ }
+ try {
+ if (beforeTask != null) {
+ beforeTask.run();
+ }
+ return task.call();
+ } finally {
+ inScope = false;
+ if (activeAuthenticator != null) {
+ activeAuthenticator.set(previousAuthenticator);
+ }
+ }
+ }
+ }
+}
diff --git
a/regression-test/suites/external_table_p0/kerberos/test_iceberg_hadoop_catalog_kerberos.groovy
b/regression-test/suites/external_table_p0/kerberos/test_iceberg_hadoop_catalog_kerberos.groovy
index 484b698d27c..01cfa5d9961 100644
---
a/regression-test/suites/external_table_p0/kerberos/test_iceberg_hadoop_catalog_kerberos.groovy
+++
b/regression-test/suites/external_table_p0/kerberos/test_iceberg_hadoop_catalog_kerberos.groovy
@@ -98,6 +98,72 @@ suite("test_iceberg_hadoop_catalog_kerberos",
"p0,external,kerberos,external_doc
'partition_val2'
);
"""
+ def snapshotsAfterFirstInsert = sql """
+ select snapshot_id from ${test_tbl_name}\$snapshots order by
committed_at
+ """
+ assert snapshotsAfterFirstInsert.size() == 1
+ String firstSnapshotId = String.valueOf(snapshotsAfterFirstInsert[0][0])
+
+ sql """
+ insert into ${test_tbl_name} values (
+ '2024-05-27 12:34:56',
+ false,
+ 456,
+ 4567890123456,
+ 45.67,
+ 89.012,
+ 45678.9012,
+ 'another example',
+ '2024-05-27',
+ '2024-05-27 14:00:00',
+ 'partition_val1',
+ 'partition_val2'
+ );
+ """
+ def dataBeforeRollback = sql """select count(1) from ${test_tbl_name}"""
+ assert dataBeforeRollback.get(0).get(0) == 2
+
+ // Rollback must commit metadata with the same Kerberos identity used to
acquire the table.
+ sql """
+ alter table ${catalog_name}.${database_name}.${test_tbl_name}
+ execute rollback_to_snapshot("snapshot_id" = "${firstSnapshotId}")
+ """
+ def dataAfterRollback = sql """select count(1) from ${test_tbl_name}"""
+ assert dataAfterRollback.get(0).get(0) == 1
+
+ sql """
+ insert into ${test_tbl_name} values (
+ '2024-05-27 12:34:56',
+ false,
+ 456,
+ 4567890123456,
+ 45.67,
+ 89.012,
+ 45678.9012,
+ 'another example',
+ '2024-05-27',
+ '2024-05-27 14:00:00',
+ 'partition_val1',
+ 'partition_val2'
+ );
+ """
+
+ def snapshotsBeforeExpire = sql """
+ select snapshot_id from ${test_tbl_name}\$snapshots order by
committed_at
+ """
+ assert snapshotsBeforeExpire.size() == 3
+
+ // The action must reuse the catalog's Kerberos identity while committing
new metadata.
+ sql """
+ alter table ${catalog_name}.${database_name}.${test_tbl_name}
+ execute expire_snapshots("retain_last" = "1")
+ """
+
+ def snapshotsAfterExpire = sql """
+ select snapshot_id from ${test_tbl_name}\$snapshots order by
committed_at
+ """
+ assert snapshotsAfterExpire.size() == 1
+
def dataResult = sql """select count(1) from ${test_tbl_name} """
- assert dataResult.get(0).get(0) == 1
+ assert dataResult.get(0).get(0) == 2
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]