This is an automated email from the ASF dual-hosted git repository.
yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new ddf617b6d5 [#12232] fix(core): Preserve metadata during concurrent
drop and rename (#12235)
ddf617b6d5 is described below
commit ddf617b6d5199d7b3cda1cb68692a8d4f7e1dce1
Author: Qi Yu <[email protected]>
AuthorDate: Mon Aug 24 19:50:18 2026 +0800
[#12232] fix(core): Preserve metadata during concurrent drop and rename
(#12235)
### What changes were proposed in this pull request?
- Delete unmanaged table, view, and schema registrations only when the
external catalog confirms the drop or purge.
- Surface store failures around external table and view renames instead
of reporting success.
- Add regression tests for the related drop, purge, and rename paths.
### Why are the changes needed?
A concurrent drop and rename can cause the drop to return `false`
because the object has already been renamed externally. Deleting the old
stored registration in this case removes metadata associated with the
surviving object.
The same two-phase external-catalog and Gravitino-store behavior exists
for views, and an out-of-band schema rename has the corresponding
stale-name drop risk. Table purge also followed the unsafe deletion
path.
The rename paths could additionally perform an external rename when the
stored registration could not be read or updated, leaving inconsistent
state while reporting success.
Fix: #12232
### Does this PR introduce _any_ user-facing change?
Yes. For unmanaged tables, views, and schemas, a drop or purge that
returns `false` no longer deletes the stored registration. Table and
view renames now return an error when Gravitino cannot safely read or
update the stored registration.
No API or configuration changes are introduced.
### How was this patch tested?
- Added regression coverage to `TestTableOperationDispatcher`,
`TestViewOperationDispatcher`, and `TestSchemaOperationDispatcher`.
- Ran `./gradlew spotlessApply`.
- Ran `./gradlew :core:check -PskipITs -PskipDockerTests=true`.
- Ran `git diff --check`.
---
.../catalog/SchemaOperationDispatcher.java | 26 +++---
.../catalog/TableOperationDispatcher.java | 93 ++++++++++++++--------
.../gravitino/catalog/ViewOperationDispatcher.java | 63 +++++++++++----
.../catalog/TestSchemaOperationDispatcher.java | 18 +++++
.../catalog/TestTableOperationDispatcher.java | 87 ++++++++++++++++++--
.../catalog/TestViewOperationDispatcher.java | 71 ++++++++++++++++-
6 files changed, 288 insertions(+), 70 deletions(-)
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java
index 31c752b21b..695f244aec 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java
@@ -344,20 +344,18 @@ public class SchemaOperationDispatcher extends
OperationDispatcher implements Sc
return droppedFromCatalog;
}
- // For the unmanaged schema, it could happen that the schema:
- // 1. It's not found in the catalog (dropped directly from
underlying sources)
- // 2. It's found in the catalog but not in the store (not managed by
Gravitino)
- // 3. It's found in the catalog and the store (managed by Gravitino)
- // 4. Neither found in the catalog nor in the store.
- // In all situations, we try to delete the schema from the store,
but we don't take the
- // return value of the store operation into account. We only take
the return value of the
- // catalog into account.
- try {
- store.delete(ident, SCHEMA, true);
- } catch (NoSuchEntityException e) {
- LOG.warn("The schema to be dropped does not exist in the store:
{}", ident, e);
- } catch (Exception e) {
- throw new RuntimeException(e);
+ // A false result is ambiguous: the external schema may have been
renamed or dropped out
+ // of band. Preserve the registration because deleting it after a
rename would lose
+ // Gravitino-only metadata. A true out-of-band drop can therefore
leave a stale
+ // registration that requires separate cleanup.
+ if (droppedFromCatalog) {
+ try {
+ store.delete(ident, SCHEMA, true);
+ } catch (NoSuchEntityException e) {
+ LOG.warn("The schema to be dropped does not exist in the store:
{}", ident, e);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
}
SchemaEntityCleaner.deleteOrphanedSchemaEntities(
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
index e2d7bd9d86..a53722c4f4 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
@@ -33,6 +33,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -46,6 +47,7 @@ import org.apache.gravitino.Namespace;
import org.apache.gravitino.StringIdentifier;
import org.apache.gravitino.connector.HasPropertyMetadata;
import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.exceptions.GravitinoRuntimeException;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.exceptions.NoSuchSchemaException;
import org.apache.gravitino.exceptions.NoSuchTableException;
@@ -243,11 +245,15 @@ public class TableOperationDispatcher extends
OperationDispatcher implements Tab
* @return The altered {@link Table} object after applying the changes.
* @throws NoSuchTableException If the table to alter does not exist.
* @throws IllegalArgumentException If an unsupported or invalid change is
specified.
+ * @throws GravitinoRuntimeException If a rename succeeds in the external
catalog but Gravitino
+ * cannot update the stored table registration consistently.
*/
@Override
public Table alterTable(NameIdentifier ident, TableChange... changes)
throws NoSuchTableException, IllegalArgumentException {
validateAlterProperties(ident,
HasPropertyMetadata::tablePropertiesMetadata, changes);
+ boolean isRenameTable =
+ Arrays.stream(changes).anyMatch(change -> change instanceof
TableChange.RenameTable);
// use the read lock on the table if there does not exist
TableChange.RenameTable in the
// changes, or:
@@ -276,6 +282,11 @@ public class TableOperationDispatcher extends
OperationDispatcher implements Tab
nameIdentifierForLock.equals(ident) ? LockType.READ : LockType.WRITE,
() -> {
NameIdentifier catalogIdent = getCatalogIdentifier(ident);
+ boolean isManagedTable = isManagedEntity(catalogIdent,
Capability.Scope.TABLE);
+ Optional<TableEntity> tableEntityBeforeRename =
+ isRenameTable && !isManagedTable
+ ? getTableEntityBeforeRename(ident)
+ : Optional.empty();
Table alteredTable =
doWithCatalog(
catalogIdent,
@@ -285,7 +296,6 @@ public class TableOperationDispatcher extends
OperationDispatcher implements Tab
NoSuchTableException.class,
IllegalArgumentException.class);
- boolean isManagedTable = isManagedEntity(catalogIdent,
Capability.Scope.TABLE);
if (isManagedTable) {
return EntityCombinedTable.of(alteredTable)
.withHiddenProperties(
@@ -297,9 +307,11 @@ public class TableOperationDispatcher extends
OperationDispatcher implements Tab
StringIdentifier stringId =
getStringIdFromProperties(alteredTable.properties());
// Case 1: The table is not created by Gravitino and this table is
never imported.
- TableEntity te = null;
+ TableEntity te = tableEntityBeforeRename.orElse(null);
if (stringId == null) {
- te = getEntity(ident, TABLE, TableEntity.class);
+ if (te == null) {
+ te = getEntity(ident, TABLE, TableEntity.class);
+ }
if (te == null) {
return EntityCombinedTable.of(alteredTable)
.withHiddenProperties(
@@ -350,6 +362,17 @@ public class TableOperationDispatcher extends
OperationDispatcher implements Tab
"UPDATE",
tableId);
+ // operateOnEntity returns null if the store update fails or if the
returned entity ID
+ // does not match the ID from the external catalog.
+ if (isRenameTable && updatedTableEntity == null) {
+ NameIdentifier newIdent =
+ NameIdentifier.of(getNewNamespace(ident, changes),
alteredTable.name());
+ throw new GravitinoRuntimeException(
+ "Table %s was renamed to %s in the external catalog, but its
registration in "
+ + "Gravitino could not be updated consistently",
+ ident, newIdent);
+ }
+
return EntityCombinedTable.of(alteredTable, updatedTableEntity)
.withHiddenProperties(
getHiddenPropertyNames(
@@ -386,20 +409,18 @@ public class TableOperationDispatcher extends
OperationDispatcher implements Tab
return droppedFromCatalog;
}
- // For unmanaged table, it could happen that the table:
- // 1. Is not found in the catalog (dropped directly from underlying
sources)
- // 2. Is found in the catalog but not in the store (not managed by
Gravitino)
- // 3. Is found in the catalog and the store (managed by Gravitino)
- // 4. Neither found in the catalog nor in the store.
- // In all situations, we try to delete the table from the store, but
we don't take the
- // return value of the store operation into account. We only take
the return value of the
- // catalog into account.
- try {
- store.delete(ident, TABLE);
- } catch (NoSuchEntityException e) {
- LOG.warn("The table to be dropped does not exist in the store:
{}", ident, e);
- } catch (Exception e) {
- throw new RuntimeException(e);
+ // A false result is ambiguous: the external table may have been
renamed or dropped out of
+ // band. Preserve the registration because deleting it after a
rename would lose
+ // Gravitino-only metadata. A true out-of-band drop can therefore
leave a stale
+ // registration that requires separate cleanup.
+ if (droppedFromCatalog) {
+ try {
+ store.delete(ident, TABLE);
+ } catch (NoSuchEntityException e) {
+ LOG.warn("The table to be dropped does not exist in the store:
{}", ident, e);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
}
// Run unconditionally: an out-of-band drop may have left orphaned
schema entities. The
// cleanup is best-effort and stops as soon as a schema still exists.
@@ -442,21 +463,18 @@ public class TableOperationDispatcher extends
OperationDispatcher implements Tab
return droppedFromCatalog;
}
- // For unmanaged table, it could happen that the table:
- // 1. Is not found in the catalog (dropped directly from underlying
sources)
- // 2. Is found in the catalog but not in the store (not managed by
Gravitino)
- // 3. Is found in the catalog and the store (managed by Gravitino)
- // 4. Neither found in the catalog nor in the store.
- // In all situations, we try to delete the table from the store, but
we don't take the
- // return value of the store operation into account. We only take
the return value of the
- // catalog into account.
- try {
- store.delete(ident, TABLE);
- } catch (NoSuchEntityException e) {
- LOG.warn("The table to be purged does not exist in the store: {}",
ident, e);
- return false;
- } catch (Exception e) {
- throw new RuntimeException(e);
+ // A false result is ambiguous: the external table may have been
renamed or dropped out of
+ // band. Preserve the registration because deleting it after a
rename would lose
+ // Gravitino-only metadata. A true out-of-band purge can therefore
leave a stale
+ // registration that requires separate cleanup.
+ if (droppedFromCatalog) {
+ try {
+ store.delete(ident, TABLE);
+ } catch (NoSuchEntityException e) {
+ LOG.warn("The table to be purged does not exist in the store:
{}", ident, e);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
}
// Run unconditionally: an out-of-band purge may have left orphaned
schema entities. The
// cleanup is best-effort and stops as soon as a schema still exists.
@@ -483,6 +501,17 @@ public class TableOperationDispatcher extends
OperationDispatcher implements Tab
.orElse(tableIdent.namespace());
}
+ private Optional<TableEntity> getTableEntityBeforeRename(NameIdentifier
ident) {
+ try {
+ return Optional.of(store.get(ident, TABLE, TableEntity.class));
+ } catch (NoSuchEntityException e) {
+ return Optional.empty();
+ } catch (Exception e) {
+ throw new GravitinoRuntimeException(
+ e, "Failed to read the stored registration for table %s before
renaming it", ident);
+ }
+ }
+
private EntityCombinedTable importTable(NameIdentifier identifier) {
EntityCombinedTable table = internalLoadTable(identifier);
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
index 731639dca9..d2c17fabdc 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
@@ -29,6 +29,7 @@ import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
+import java.util.Optional;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.apache.gravitino.EntityAlreadyExistsException;
@@ -39,6 +40,7 @@ import org.apache.gravitino.Namespace;
import org.apache.gravitino.StringIdentifier;
import org.apache.gravitino.connector.HasPropertyMetadata;
import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.exceptions.GravitinoRuntimeException;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.exceptions.NoSuchSchemaException;
import org.apache.gravitino.exceptions.NoSuchViewException;
@@ -209,11 +211,15 @@ public class ViewOperationDispatcher extends
OperationDispatcher implements View
* @return The altered {@link View} object after applying the changes.
* @throws NoSuchViewException If the view to alter does not exist.
* @throws IllegalArgumentException If an unsupported or invalid change is
specified.
+ * @throws GravitinoRuntimeException If a rename succeeds in the external
catalog but Gravitino
+ * cannot update the stored view registration consistently.
*/
@Override
public View alterView(NameIdentifier ident, ViewChange... changes)
throws NoSuchViewException, IllegalArgumentException {
validateAlterProperties(ident,
HasPropertyMetadata::tablePropertiesMetadata, changes);
+ boolean isRenameView =
+ Arrays.stream(changes).anyMatch(change -> change instanceof
ViewChange.RenameView);
NameIdentifier lockIdent = ident;
for (ViewChange change : changes) {
if (change instanceof ViewChange.RenameView) {
@@ -228,6 +234,9 @@ public class ViewOperationDispatcher extends
OperationDispatcher implements View
nameIdentifierForLock.equals(ident) ? LockType.READ : LockType.WRITE,
() -> {
NameIdentifier catalogIdent = getCatalogIdentifier(ident);
+ boolean isManagedView = isManagedEntity(catalogIdent,
Capability.Scope.VIEW);
+ Optional<ViewEntity> viewEntityBeforeRename =
+ isRenameView && !isManagedView ?
getViewEntityBeforeRename(ident) : Optional.empty();
View alteredView =
doWithCatalog(
catalogIdent,
@@ -237,7 +246,6 @@ public class ViewOperationDispatcher extends
OperationDispatcher implements View
NoSuchViewException.class,
IllegalArgumentException.class);
- boolean isManagedView = isManagedEntity(catalogIdent,
Capability.Scope.VIEW);
if (isManagedView) {
return EntityCombinedView.of(alteredView)
.withHiddenProperties(
@@ -249,9 +257,11 @@ public class ViewOperationDispatcher extends
OperationDispatcher implements View
StringIdentifier stringId =
getStringIdFromProperties(alteredView.properties());
// Case 1: The view is not created by Gravitino and this view is
never imported.
- ViewEntity existing = null;
+ ViewEntity existing = viewEntityBeforeRename.orElse(null);
if (stringId == null) {
- existing = getEntity(ident, VIEW, ViewEntity.class);
+ if (existing == null) {
+ existing = getEntity(ident, VIEW, ViewEntity.class);
+ }
if (existing == null) {
return EntityCombinedView.of(alteredView)
.withHiddenProperties(
@@ -275,6 +285,16 @@ public class ViewOperationDispatcher extends
OperationDispatcher implements View
"UPDATE",
viewId);
+ // operateOnEntity returns null if the store update fails or if the
returned entity ID
+ // does not match the ID from the external catalog.
+ if (isRenameView && updatedViewEntity == null) {
+ NameIdentifier newIdent = NameIdentifier.of(ident.namespace(),
alteredView.name());
+ throw new GravitinoRuntimeException(
+ "View %s was renamed to %s in the external catalog, but its
registration in "
+ + "Gravitino could not be updated consistently",
+ ident, newIdent);
+ }
+
return EntityCombinedView.of(alteredView, updatedViewEntity)
.withHiddenProperties(
getHiddenPropertyNames(
@@ -311,20 +331,18 @@ public class ViewOperationDispatcher extends
OperationDispatcher implements View
return droppedFromCatalog;
}
- // For unmanaged view, it could happen that the view:
- // 1. Is not found in the catalog (dropped directly from underlying
sources)
- // 2. Is found in the catalog but not in the store (not managed by
Gravitino)
- // 3. Is found in the catalog and the store (managed by Gravitino)
- // 4. Neither found in the catalog nor in the store.
- // In all situations, we try to delete the view from the store, but
we don't take the
- // return value of the store operation into account. We only take
the return value of the
- // catalog into account.
- try {
- store.delete(ident, VIEW);
- } catch (NoSuchEntityException e) {
- LOG.warn("The view to be dropped does not exist in the store: {}",
ident, e);
- } catch (Exception e) {
- throw new RuntimeException(e);
+ // A false result is ambiguous: the external view may have been
renamed or dropped out of
+ // band. Preserve the registration because deleting it after a
rename would lose
+ // Gravitino-only metadata. A true out-of-band drop can therefore
leave a stale
+ // registration that requires separate cleanup.
+ if (droppedFromCatalog) {
+ try {
+ store.delete(ident, VIEW);
+ } catch (NoSuchEntityException e) {
+ LOG.warn("The view to be dropped does not exist in the store:
{}", ident, e);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
}
// Run unconditionally: an out-of-band drop may have left orphaned
schema entities. The
// cleanup is best-effort and stops as soon as a schema still exists.
@@ -430,6 +448,17 @@ public class ViewOperationDispatcher extends
OperationDispatcher implements View
catalogView.properties()));
}
+ private Optional<ViewEntity> getViewEntityBeforeRename(NameIdentifier ident)
{
+ try {
+ return Optional.of(store.get(ident, VIEW, ViewEntity.class));
+ } catch (NoSuchEntityException e) {
+ return Optional.empty();
+ } catch (Exception e) {
+ throw new GravitinoRuntimeException(
+ e, "Failed to read the stored registration for view %s before
renaming it", ident);
+ }
+ }
+
private EntityCombinedView internalLoadView(NameIdentifier ident) throws
NoSuchViewException {
NameIdentifier catalogIdentifier = getCatalogIdentifier(ident);
View view =
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestSchemaOperationDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestSchemaOperationDispatcher.java
index bb2b0d0576..c5ebe8ff74 100644
---
a/core/src/test/java/org/apache/gravitino/catalog/TestSchemaOperationDispatcher.java
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestSchemaOperationDispatcher.java
@@ -46,7 +46,9 @@ import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.Schema;
import org.apache.gravitino.SchemaChange;
+import org.apache.gravitino.TestCatalog;
import org.apache.gravitino.auth.AuthConstants;
+import org.apache.gravitino.connector.TestCatalogOperations;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.lock.LockManager;
import org.apache.gravitino.meta.AuditInfo;
@@ -370,6 +372,22 @@ public class TestSchemaOperationDispatcher extends
TestOperationDispatcher {
RuntimeException.class, () -> dispatcher.dropSchema(schemaIdent,
false));
}
+ @Test
+ public void testDropMissingSchemaPreservesStoredEntity() throws IOException {
+ reset(entityStore);
+ NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog,
"schema_renamed_out_of_band");
+ Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+ dispatcher.createSchema(schemaIdent, "comment", props);
+
+ TestCatalog testCatalog =
+ (TestCatalog) catalogManager.loadCatalog(NameIdentifier.of(metalake,
catalog));
+ TestCatalogOperations testCatalogOperations = (TestCatalogOperations)
testCatalog.ops();
+ Assertions.assertTrue(testCatalogOperations.dropSchema(schemaIdent,
false));
+
+ Assertions.assertFalse(dispatcher.dropSchema(schemaIdent, false));
+ Assertions.assertTrue(entityStore.exists(schemaIdent, SCHEMA));
+ }
+
@Test
public void testDropHierarchicalSchemaCleansUpOrphanedAncestors() throws
IOException {
// Clear any spy stubs leaked from other tests sharing the static
entityStore.
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
index a834398c71..0bf03cfb06 100644
---
a/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
@@ -57,7 +57,9 @@ import org.apache.gravitino.TestCatalog;
import org.apache.gravitino.TestColumn;
import org.apache.gravitino.auth.AuthConstants;
import org.apache.gravitino.connector.TestCatalogOperations;
+import org.apache.gravitino.exceptions.GravitinoRuntimeException;
import org.apache.gravitino.exceptions.NoSuchEntityException;
+import org.apache.gravitino.exceptions.NoSuchTableException;
import org.apache.gravitino.lock.LockManager;
import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.ColumnEntity;
@@ -505,6 +507,78 @@ public class TestTableOperationDispatcher extends
TestOperationDispatcher {
Assertions.assertEquals("test", alteredTable4.auditInfo().lastModifier());
}
+ @Test
+ public void testRenameTableSurfacesStoreUpdateFailure() throws IOException {
+ Namespace tableNs = Namespace.of(metalake, catalog,
"schema_rename_store_failure");
+ NameIdentifier tableIdent = NameIdentifier.of(tableNs,
"table_before_rename");
+ NameIdentifier renamedTableIdent = NameIdentifier.of(tableNs,
"table_after_rename");
+ Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+ Column[] columns =
+ new Column[] {
+ TestColumn.builder()
+ .withName("col1")
+ .withPosition(0)
+ .withType(Types.StringType.get())
+ .build()
+ };
+
+
schemaOperationDispatcher.createSchema(NameIdentifier.of(tableNs.levels()),
"comment", props);
+ tableOperationDispatcher.createTable(tableIdent, columns, "comment",
props, new Transform[0]);
+
+ reset(entityStore);
+ doThrow(new NoSuchEntityException("mock update conflict"))
+ .when(entityStore)
+ .update(any(), any(), any(), any());
+
+ GravitinoRuntimeException exception =
+ Assertions.assertThrows(
+ GravitinoRuntimeException.class,
+ () ->
+ tableOperationDispatcher.alterTable(
+ tableIdent, TableChange.rename(renamedTableIdent.name())));
+
Assertions.assertTrue(exception.getMessage().contains(tableIdent.toString()));
+
Assertions.assertTrue(exception.getMessage().contains(renamedTableIdent.toString()));
+ reset(entityStore);
+ }
+
+ @Test
+ public void testRenameTableFailsBeforeExternalChangeWhenStoreReadFails()
throws IOException {
+ Namespace tableNs = Namespace.of(metalake, catalog,
"schema_rename_store_read_failure");
+ NameIdentifier tableIdent = NameIdentifier.of(tableNs,
"table_before_failed_rename");
+ NameIdentifier renamedTableIdent = NameIdentifier.of(tableNs,
"table_after_failed_rename");
+ Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+ Column[] columns =
+ new Column[] {
+ TestColumn.builder()
+ .withName("col1")
+ .withPosition(0)
+ .withType(Types.StringType.get())
+ .build()
+ };
+
+
schemaOperationDispatcher.createSchema(NameIdentifier.of(tableNs.levels()),
"comment", props);
+ tableOperationDispatcher.createTable(tableIdent, columns, "comment",
props, new Transform[0]);
+
+ reset(entityStore);
+ doThrow(new IOException("mock store read failure"))
+ .when(entityStore)
+ .get(any(), eq(TABLE), any());
+
+ Assertions.assertThrows(
+ GravitinoRuntimeException.class,
+ () ->
+ tableOperationDispatcher.alterTable(
+ tableIdent, TableChange.rename(renamedTableIdent.name())));
+
+ TestCatalog testCatalog =
+ (TestCatalog) catalogManager.loadCatalog(NameIdentifier.of(metalake,
catalog));
+ TestCatalogOperations testCatalogOperations = (TestCatalogOperations)
testCatalog.ops();
+ Assertions.assertDoesNotThrow(() ->
testCatalogOperations.loadTable(tableIdent));
+ Assertions.assertThrows(
+ NoSuchTableException.class, () ->
testCatalogOperations.loadTable(renamedTableIdent));
+ reset(entityStore);
+ }
+
@Test
public void testCreateAndDropTable() throws IOException {
NameIdentifier tableIdent = NameIdentifier.of(metalake, catalog,
"schema71", "table31");
@@ -611,9 +685,11 @@ public class TestTableOperationDispatcher extends
TestOperationDispatcher {
Assertions.assertFalse(testCatalogOperations.schemaExists(schemaIdent));
Assertions.assertFalse(testCatalogOperations.schemaExists(ancestorIdent));
- // dropTable returns false because the table is already gone from the
catalog, but the
- // orphaned schema entities must still be cleaned up.
+ // dropTable returns false because the table is already gone from the
catalog. Preserve the
+ // table entity because the same result can be caused by a concurrent
rename, while still
+ // cleaning up orphaned schema entities.
Assertions.assertFalse(tableOperationDispatcher.dropTable(tableIdent));
+ Assertions.assertTrue(entityStore.exists(tableIdent, TABLE));
Assertions.assertFalse(entityStore.exists(schemaIdent, SCHEMA));
Assertions.assertFalse(entityStore.exists(ancestorIdent, SCHEMA));
}
@@ -653,10 +729,11 @@ public class TestTableOperationDispatcher extends
TestOperationDispatcher {
Assertions.assertFalse(testCatalogOperations.schemaExists(schemaIdent));
Assertions.assertFalse(testCatalogOperations.schemaExists(ancestorIdent));
- // purgeTable returns false because the table is already gone from the
catalog, but the
- // orphaned schema entities must still be cleaned up. A regression that
re-guards the cleanup
- // behind the catalog drop result would leave the stale schema entities
behind.
+ // purgeTable returns false because the table is already gone from the
catalog. Preserve the
+ // table entity because the same result can be caused by a concurrent
rename, while still
+ // cleaning up orphaned schema entities.
Assertions.assertFalse(tableOperationDispatcher.purgeTable(tableIdent));
+ Assertions.assertTrue(entityStore.exists(tableIdent, TABLE));
Assertions.assertFalse(entityStore.exists(schemaIdent, SCHEMA));
Assertions.assertFalse(entityStore.exists(ancestorIdent, SCHEMA));
}
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
index 26372f662e..490e48052f 100644
---
a/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
@@ -25,8 +25,12 @@ import static org.apache.gravitino.Entity.EntityType.SCHEMA;
import static org.apache.gravitino.Entity.EntityType.VIEW;
import static org.apache.gravitino.StringIdentifier.ID_KEY;
import static org.apache.gravitino.TestBasePropertiesMetadata.COMMENT_KEY;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
@@ -50,6 +54,7 @@ import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.TestCatalog;
import org.apache.gravitino.connector.TestCatalogOperations;
+import org.apache.gravitino.exceptions.GravitinoRuntimeException;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.exceptions.NoSuchViewException;
import org.apache.gravitino.lock.LockManager;
@@ -570,9 +575,11 @@ public class TestViewOperationDispatcher extends
TestOperationDispatcher {
Assertions.assertFalse(testCatalogOperations.schemaExists(schemaIdent));
Assertions.assertFalse(testCatalogOperations.schemaExists(ancestorIdent));
- // dropView returns false because the view is already gone from the
catalog, but the
- // orphaned schema entities must still be cleaned up.
+ // dropView returns false because the view is already gone from the
catalog. Preserve the view
+ // entity because the same result can be caused by a concurrent rename,
while still cleaning up
+ // orphaned schema entities.
Assertions.assertFalse(viewOperationDispatcher.dropView(viewIdent));
+ Assertions.assertTrue(entityStore.exists(viewIdent, VIEW));
Assertions.assertFalse(entityStore.exists(schemaIdent, SCHEMA));
Assertions.assertFalse(entityStore.exists(ancestorIdent, SCHEMA));
}
@@ -649,6 +656,66 @@ public class TestViewOperationDispatcher extends
TestOperationDispatcher {
Assertions.assertEquals("new_view", altered.name());
}
+ @Test
+ public void testRenameViewSurfacesStoreUpdateFailure() throws IOException {
+ Namespace viewNs = Namespace.of(metalake, catalog,
"schema_rename_view_store_failure");
+ schemaOperationDispatcher.createSchema(
+ NameIdentifier.of(viewNs.levels()), "c", ImmutableMap.of("k1", "v1",
"k2", "v2"));
+
+ NameIdentifier oldIdent = NameIdentifier.of(viewNs, "view_before_rename");
+ NameIdentifier newIdent = NameIdentifier.of(viewNs, "view_after_rename");
+ Representation[] representations = {
+ SQLRepresentation.builder().withDialect("spark").withSql("SELECT
1").build()
+ };
+ viewOperationDispatcher.createView(
+ oldIdent, "c", new Column[0], representations, null, null,
ImmutableMap.of("k1", "v1"));
+
+ reset(entityStore);
+ doThrow(new NoSuchEntityException("mock update conflict"))
+ .when(entityStore)
+ .update(any(), any(), any(), any());
+
+ GravitinoRuntimeException exception =
+ Assertions.assertThrows(
+ GravitinoRuntimeException.class,
+ () -> viewOperationDispatcher.alterView(oldIdent,
ViewChange.rename(newIdent.name())));
+
Assertions.assertTrue(exception.getMessage().contains(oldIdent.toString()));
+
Assertions.assertTrue(exception.getMessage().contains(newIdent.toString()));
+ reset(entityStore);
+ }
+
+ @Test
+ public void testRenameViewFailsBeforeExternalChangeWhenStoreReadFails()
throws IOException {
+ Namespace viewNs = Namespace.of(metalake, catalog,
"schema_rename_view_store_read_failure");
+ schemaOperationDispatcher.createSchema(
+ NameIdentifier.of(viewNs.levels()), "c", ImmutableMap.of("k1", "v1",
"k2", "v2"));
+
+ NameIdentifier oldIdent = NameIdentifier.of(viewNs,
"view_before_failed_rename");
+ NameIdentifier newIdent = NameIdentifier.of(viewNs,
"view_after_failed_rename");
+ Representation[] representations = {
+ SQLRepresentation.builder().withDialect("spark").withSql("SELECT
1").build()
+ };
+ viewOperationDispatcher.createView(
+ oldIdent, "c", new Column[0], representations, null, null,
ImmutableMap.of("k1", "v1"));
+
+ reset(entityStore);
+ doThrow(new IOException("mock store read failure"))
+ .when(entityStore)
+ .get(any(), eq(VIEW), any());
+
+ Assertions.assertThrows(
+ GravitinoRuntimeException.class,
+ () -> viewOperationDispatcher.alterView(oldIdent,
ViewChange.rename(newIdent.name())));
+
+ TestCatalog testCatalog =
+ (TestCatalog) catalogManager.loadCatalog(NameIdentifier.of(metalake,
catalog));
+ TestCatalogOperations testCatalogOperations = (TestCatalogOperations)
testCatalog.ops();
+ Assertions.assertDoesNotThrow(() ->
testCatalogOperations.loadView(oldIdent));
+ Assertions.assertThrows(
+ NoSuchViewException.class, () ->
testCatalogOperations.loadView(newIdent));
+ reset(entityStore);
+ }
+
@Test
public void testAlterViewReplace() throws IOException {
Namespace viewNs = Namespace.of(metalake, catalog, "schema_alter_replace");