This is an automated email from the ASF dual-hosted git repository.
diqiu50 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 dbfe2cfd0d [#14710] fix(catalog-glue): Fail fast when Iceberg metadata
cannot be read (#14711)
dbfe2cfd0d is described below
commit dbfe2cfd0d66956af7bd64c8892648adf996250d
Author: Yuhui <[email protected]>
AuthorDate: Fri Nov 6 19:18:05 2026 +0800
[#14710] fix(catalog-glue): Fail fast when Iceberg metadata cannot be read
(#14711)
### What changes were proposed in this pull request?
Load Iceberg tables for reads through `StaticTableOperations`, using the
`metadata_location` already fetched from Glue, instead of going through
Iceberg's metadata refresh.
`GravitinoGlueCatalog` extends `GlueCatalog` to reuse its `FileIO`, and
keeps the catalog's table operations reachable because the catalog
closes a `FileIO` once its operations are garbage collected. Paths that
commit changes still use `GlueCatalog.loadTable`.
This is a workaround until Iceberg stops retrying permission errors, see
apache/iceberg#17400. `GravitinoGlueCatalog` can be removed after
upgrading to a release that includes it.
### Why are the changes needed?
Iceberg's refresh retries every failure except `NotFoundException` up to
20 times, so a load denied by S3 takes about 90 seconds to fail while
holding the tree lock.
Fixes #14710
### Does this PR introduce _any_ user-facing change?
Loading an Iceberg table that S3 denies now fails in under a second.
Transient S3 failures are retried only by the layers below Iceberg's
refresh, so an outage that outlasts them fails the load sooner than
before.
### How was this patch tested?
- Added `TestGravitinoGlueCatalog` and updated
`TestGlueCatalogOperationsForIceberg` and `TestGlueIcebergTableHelper`.
- Ran `./gradlew :catalogs:catalog-glue:test -PskipITs`.
- Ran the load path against local fake Glue and S3 endpoints for 403,
404, 500 and 503 responses.
---
catalogs/catalog-glue/build.gradle.kts | 7 ++
.../catalog/glue/GlueCatalogOperations.java | 7 +-
.../catalog/glue/GlueIcebergTableHelper.java | 21 ++--
.../catalog/glue/GravitinoGlueCatalog.java | 75 ++++++++++++
.../glue/TestGlueCatalogOperationsForIceberg.java | 24 ++--
.../catalog/glue/TestGlueIcebergTableHelper.java | 10 +-
.../catalog/glue/TestGravitinoGlueCatalog.java | 126 +++++++++++++++++++++
7 files changed, 244 insertions(+), 26 deletions(-)
diff --git a/catalogs/catalog-glue/build.gradle.kts
b/catalogs/catalog-glue/build.gradle.kts
index 825cae2793..6cb855ffa2 100644
--- a/catalogs/catalog-glue/build.gradle.kts
+++ b/catalogs/catalog-glue/build.gradle.kts
@@ -30,6 +30,10 @@ dependencies {
compileOnly(project(":core"))
compileOnly(libs.aws.dynamodb)
+ // GlueCatalog implements Configurable<Configuration>, which subclasses need
only to compile.
+ compileOnly(libs.hadoop3.common) {
+ exclude("*")
+ }
compileOnly(libs.lombok)
implementation(project(":catalogs:catalog-common")) {
@@ -54,6 +58,9 @@ dependencies {
testImplementation(project(":common"))
testImplementation(project(":core"))
+ testImplementation(libs.hadoop3.common) {
+ exclude("*")
+ }
testImplementation(libs.junit.jupiter.api)
testImplementation(libs.mockito.core)
testImplementation(libs.slf4j.api)
diff --git
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogOperations.java
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogOperations.java
index abdaa86d05..8460eca316 100644
---
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogOperations.java
+++
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogOperations.java
@@ -148,7 +148,7 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
@VisibleForTesting String warehouseLocation;
/** Iceberg SDK Glue catalog used for creating Iceberg-format tables. */
- @VisibleForTesting org.apache.iceberg.catalog.Catalog icebergGlueCatalog;
+ @VisibleForTesting GravitinoGlueCatalog icebergGlueCatalog;
private final GlueTypeConverter typeConverter = new GlueTypeConverter();
@@ -451,10 +451,11 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
// Iceberg table; the Glue columns are a Hive-style copy. A table whose
metadata cannot be
// read is reported as an error rather than with the Glue copy.
if (GlueIcebergTableHelper.isIcebergTable(rawGlueTable)) {
+ String metadataLocation =
rawGlueTable.parameters().get(GlueConstants.METADATA_LOCATION);
try {
- GlueIcebergTableHelper.loadTable(icebergGlueCatalog, dbName,
ident.name(), table);
+ GlueIcebergTableHelper.loadTable(
+ icebergGlueCatalog, dbName, ident.name(), metadataLocation,
table);
} catch (RuntimeException e) {
- String metadataLocation =
rawGlueTable.parameters().get(GlueConstants.METADATA_LOCATION);
throw GlueExceptionConverter.toIcebergException(
e,
metadataLocation == null
diff --git
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueIcebergTableHelper.java
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueIcebergTableHelper.java
index 46e990fdad..cc9d18b7bb 100644
---
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueIcebergTableHelper.java
+++
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueIcebergTableHelper.java
@@ -61,7 +61,6 @@ import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants;
-import org.apache.gravitino.exceptions.NoSuchTableException;
import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.TableChange;
import org.apache.gravitino.rel.expressions.Expression;
@@ -84,7 +83,6 @@ import org.apache.iceberg.Transaction;
import org.apache.iceberg.UpdateProperties;
import org.apache.iceberg.UpdateSchema;
import org.apache.iceberg.aws.AwsProperties;
-import org.apache.iceberg.aws.glue.GlueCatalog;
import org.apache.iceberg.catalog.Catalog;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.exceptions.AlreadyExistsException;
@@ -186,10 +184,10 @@ final class GlueIcebergTableHelper {
* @return an initialized Iceberg Glue catalog
* @throws IllegalArgumentException if {@code aws-region} or {@code
warehouse} is not configured
*/
- static Catalog createGlueCatalog(Map<String, String> config) {
+ static GravitinoGlueCatalog createGlueCatalog(Map<String, String> config) {
Map<String, String> icebergProps = buildIcebergCatalogProperties(config);
- GlueCatalog glueCatalog = new GlueCatalog();
+ GravitinoGlueCatalog glueCatalog = new GravitinoGlueCatalog();
glueCatalog.initialize(ICEBERG_CATALOG_NAME, icebergProps);
LOG.info("Initialized Iceberg GlueCatalog for region {}",
config.get(GlueConstants.AWS_REGION));
return glueCatalog;
@@ -277,14 +275,17 @@ final class GlueIcebergTableHelper {
* @param icebergCatalog the Iceberg Glue catalog
* @param dbName the Glue database name
* @param tableName the table name
+ * @param metadataLocation the metadata file location recorded in Glue
* @param table the GlueTable to populate with recovered metadata
*/
- static void loadTable(Catalog icebergCatalog, String dbName, String
tableName, GlueTable table) {
- Table icebergTable = icebergCatalog.loadTable(TableIdentifier.of(dbName,
tableName));
- if (icebergTable == null) {
- throw new NoSuchTableException(
- "Iceberg table %s.%s not found in Iceberg catalog", dbName,
tableName);
- }
+ static void loadTable(
+ GravitinoGlueCatalog icebergCatalog,
+ String dbName,
+ String tableName,
+ String metadataLocation,
+ GlueTable table) {
+ Table icebergTable =
+ icebergCatalog.loadStaticTable(TableIdentifier.of(dbName, tableName),
metadataLocation);
// Merge Iceberg table properties (stored in metadata.json) into the
Gravitino properties.
// Iceberg properties take precedence over Glue parameters for overlapping
keys, except for the
diff --git
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GravitinoGlueCatalog.java
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GravitinoGlueCatalog.java
new file mode 100644
index 0000000000..266d7d690e
--- /dev/null
+++
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GravitinoGlueCatalog.java
@@ -0,0 +1,75 @@
+/*
+ * 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.gravitino.catalog.glue;
+
+import org.apache.iceberg.BaseTable;
+import org.apache.iceberg.StaticTableOperations;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableOperations;
+import org.apache.iceberg.aws.glue.GlueCatalog;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.exceptions.NoSuchTableException;
+import org.apache.iceberg.io.FileIO;
+
+/**
+ * A {@link GlueCatalog} that can open a table from a metadata location
already read from Glue.
+ *
+ * <p>This can be removed once the Iceberg dependency stops retrying
permission errors during
+ * metadata refresh, see https://github.com/apache/iceberg/pull/17400.
+ */
+class GravitinoGlueCatalog extends GlueCatalog {
+
+ /**
+ * Opens a read-only table at the given metadata location. Unlike {@link
#loadTable}, this neither
+ * queries Glue nor goes through the metadata refresh, which retries most
read failures for about
+ * 90 seconds. The metadata file is read on the first access to the returned
table.
+ *
+ * @param identifier the table identifier
+ * @param metadataLocation the metadata file location recorded in Glue
+ * @return a read-only table that cannot commit changes
+ * @throws NoSuchTableException if {@code metadataLocation} is null
+ */
+ Table loadStaticTable(TableIdentifier identifier, String metadataLocation) {
+ if (metadataLocation == null) {
+ throw new NoSuchTableException("Table %s has no metadata location",
identifier);
+ }
+
+ return new BaseTable(
+ new CatalogStaticTableOperations(metadataLocation,
newTableOps(identifier)),
+ identifier.toString());
+ }
+
+ /**
+ * Static operations that hold on to the catalog's operations, because the
catalog closes a {@link
+ * FileIO} once the operations it was created for are garbage collected.
+ */
+ private static class CatalogStaticTableOperations extends
StaticTableOperations {
+ private final TableOperations catalogOperations;
+
+ CatalogStaticTableOperations(String metadataLocation, TableOperations
catalogOperations) {
+ super(metadataLocation, catalogOperations.io());
+ this.catalogOperations = catalogOperations;
+ }
+
+ @Override
+ public FileIO io() {
+ return catalogOperations.io();
+ }
+ }
+}
diff --git
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogOperationsForIceberg.java
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogOperationsForIceberg.java
index 255593ed65..a78a060774 100644
---
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogOperationsForIceberg.java
+++
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogOperationsForIceberg.java
@@ -76,12 +76,12 @@ class TestGlueCatalogOperationsForIceberg {
private GlueClient mockClient;
private GlueCatalogOperations ops;
- private Catalog mockIcebergCatalog;
+ private GravitinoGlueCatalog mockIcebergCatalog;
@BeforeEach
void setup() {
mockClient = mock(GlueClient.class);
- mockIcebergCatalog = mock(Catalog.class);
+ mockIcebergCatalog = mock(GravitinoGlueCatalog.class);
ops = new GlueCatalogOperations();
ops.glueClient = mockClient;
ops.catalogId = null;
@@ -198,7 +198,8 @@ class TestGlueCatalogOperationsForIceberg {
ops.alterTable(
ident, TableChange.addColumn(new String[] {"score"},
Types.DoubleType.get(), true));
- verify(mockIcebergCatalog,
Mockito.times(2)).loadTable(any(TableIdentifier.class));
+ verify(mockIcebergCatalog).loadTable(any(TableIdentifier.class));
+ verify(mockIcebergCatalog).loadStaticTable(any(TableIdentifier.class),
any());
verify(mockUpdateSchema)
.addColumn("score", org.apache.iceberg.types.Types.DoubleType.get(),
(String) null);
verify(mockUpdateSchema).commit();
@@ -235,7 +236,7 @@ class TestGlueCatalogOperationsForIceberg {
String metadataLocation = LOCATION + "/metadata/00001.metadata.json";
stubGlueTable(
Map.of(TABLE_TYPE_PARAM, ICEBERG_TABLE_TYPE_VALUE, METADATA_LOCATION,
metadataLocation));
- when(mockIcebergCatalog.loadTable(any(TableIdentifier.class)))
+ when(mockIcebergCatalog.loadStaticTable(any(TableIdentifier.class), any()))
.thenThrow(
new org.apache.iceberg.exceptions.NotFoundException(
"Location does not exist: %s", metadataLocation));
@@ -255,19 +256,19 @@ class TestGlueCatalogOperationsForIceberg {
ICEBERG_TABLE_TYPE_VALUE,
METADATA_LOCATION,
LOCATION + "/metadata/00001.metadata.json"));
- when(mockIcebergCatalog.loadTable(any(TableIdentifier.class)))
+ when(mockIcebergCatalog.loadStaticTable(any(TableIdentifier.class), any()))
.thenThrow(new
org.apache.iceberg.exceptions.NotFoundException("Location does not exist"));
assertTrue(ops.dropTable(NameIdentifier.of("cat", "ns", DB, TABLE)));
verify(mockClient).deleteTable(any(DeleteTableRequest.class));
- verify(mockIcebergCatalog, never()).loadTable(any(TableIdentifier.class));
+ verify(mockIcebergCatalog,
never()).loadStaticTable(any(TableIdentifier.class), any());
}
@Test
void testLoadTableFailsWhenS3DeniesIcebergMetadata() {
stubGlueTable(Map.of(TABLE_TYPE_PARAM, ICEBERG_TABLE_TYPE_VALUE));
- when(mockIcebergCatalog.loadTable(any(TableIdentifier.class)))
+ when(mockIcebergCatalog.loadStaticTable(any(TableIdentifier.class), any()))
.thenThrow(
S3Exception.builder()
.statusCode(403)
@@ -305,6 +306,8 @@ class TestGlueCatalogOperationsForIceberg {
when(mockIcebergCatalog.registerTable(TableIdentifier.of(DB, TABLE),
metadataLocation))
.thenReturn(registered);
when(mockIcebergCatalog.loadTable(any(TableIdentifier.class))).thenReturn(registered);
+ when(mockIcebergCatalog.loadStaticTable(any(TableIdentifier.class), any()))
+ .thenReturn(registered);
stubGlueTable(
Map.of(TABLE_TYPE_PARAM, ICEBERG_TABLE_TYPE_VALUE, METADATA_LOCATION,
metadataLocation));
@@ -393,7 +396,8 @@ class TestGlueCatalogOperationsForIceberg {
assertEquals("v1", captor.getValue().tableInput().parameters().get("k1"));
assertEquals("3", captor.getValue().versionId());
verify(mockIcebergCatalog, never()).renameTable(any(), any());
- verify(mockIcebergCatalog,
Mockito.times(2)).loadTable(any(TableIdentifier.class));
+ verify(mockIcebergCatalog).loadTable(any(TableIdentifier.class));
+ verify(mockIcebergCatalog).loadStaticTable(any(TableIdentifier.class),
any());
}
@Test
@@ -495,7 +499,8 @@ class TestGlueCatalogOperationsForIceberg {
Table icebergTable = mockIcebergTable();
when(icebergTable.properties())
.thenReturn(Map.of(TABLE_TYPE_PARAM, "HIVE",
GlueConstants.TABLE_FORMAT, "HIVE", "k", "v"));
-
when(mockIcebergCatalog.loadTable(any(TableIdentifier.class))).thenReturn(icebergTable);
+ when(mockIcebergCatalog.loadStaticTable(any(TableIdentifier.class), any()))
+ .thenReturn(icebergTable);
GlueTable table = ops.loadTable(NameIdentifier.of("cat", "ns", DB, TABLE));
@@ -546,6 +551,7 @@ class TestGlueCatalogOperationsForIceberg {
private Table stubIcebergTable() {
Table table = mockIcebergTable();
when(mockIcebergCatalog.loadTable(any(TableIdentifier.class))).thenReturn(table);
+ when(mockIcebergCatalog.loadStaticTable(any(TableIdentifier.class),
any())).thenReturn(table);
return table;
}
diff --git
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueIcebergTableHelper.java
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueIcebergTableHelper.java
index 7bca5ef101..b5d6fb202f 100644
---
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueIcebergTableHelper.java
+++
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueIcebergTableHelper.java
@@ -24,7 +24,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -74,6 +73,8 @@ import org.junit.jupiter.api.Test;
class TestGlueIcebergTableHelper {
+ private static final String METADATA_FILE =
"s3://bucket/db1/t1/metadata/00001.metadata.json";
+
// -------------------------------------------------------------------------
// fromIcebergType — primitive types
// -------------------------------------------------------------------------
@@ -228,8 +229,9 @@ class TestGlueIcebergTableHelper {
when(mockIcebergTable.sortOrder()).thenReturn(SortOrder.unsorted());
when(mockIcebergTable.properties()).thenReturn(new HashMap<>());
- Catalog mockCatalog = mock(Catalog.class);
-
when(mockCatalog.loadTable(any(TableIdentifier.class))).thenReturn(mockIcebergTable);
+ GravitinoGlueCatalog mockCatalog = mock(GravitinoGlueCatalog.class);
+ when(mockCatalog.loadStaticTable(TableIdentifier.of("db1", "t1"),
METADATA_FILE))
+ .thenReturn(mockIcebergTable);
// GlueTable with wrong Hive-style column types (as Glue would store them).
GlueColumn[] glueCols = {
@@ -245,7 +247,7 @@ class TestGlueIcebergTableHelper {
.withAuditInfo(AuditInfo.EMPTY)
.build();
- GlueIcebergTableHelper.loadTable(mockCatalog, "db1", "t1", table);
+ GlueIcebergTableHelper.loadTable(mockCatalog, "db1", "t1", METADATA_FILE,
table);
Column[] cols = table.columns();
assertEquals(3, cols.length);
diff --git
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGravitinoGlueCatalog.java
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGravitinoGlueCatalog.java
new file mode 100644
index 0000000000..cf1faa5e4c
--- /dev/null
+++
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGravitinoGlueCatalog.java
@@ -0,0 +1,126 @@
+/*
+ * 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.gravitino.catalog.glue;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.lang.ref.WeakReference;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.StaticTableOperations;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableMetadata;
+import org.apache.iceberg.TableMetadataParser;
+import org.apache.iceberg.TableOperations;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.exceptions.NoSuchTableException;
+import org.apache.iceberg.inmemory.InMemoryFileIO;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.types.Types;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.services.s3.model.S3Exception;
+
+class TestGravitinoGlueCatalog {
+
+ private static final TableIdentifier IDENTIFIER = TableIdentifier.of("db",
"t");
+ private static final String METADATA_LOCATION =
"s3://bucket/db/t/metadata/00001.metadata.json";
+
+ @Test
+ void testLoadStaticTableReadsMetadataFile() {
+ Schema schema = new Schema(Types.NestedField.required(1, "id",
Types.LongType.get()));
+ TableMetadata metadata =
+ TableMetadata.newTableMetadata(
+ schema, PartitionSpec.unpartitioned(), "s3://bucket/db/t",
Map.of("k", "v"));
+ InMemoryFileIO io = new InMemoryFileIO();
+ io.addFile(
+ METADATA_LOCATION,
TableMetadataParser.toJson(metadata).getBytes(StandardCharsets.UTF_8));
+
+ Table table =
+ catalogWith(mock(TableOperations.class),
io).loadStaticTable(IDENTIFIER, METADATA_LOCATION);
+
+ assertEquals("id", table.schema().columns().get(0).name());
+ assertEquals("v", table.properties().get("k"));
+ assertEquals(IDENTIFIER.toString(), table.name());
+ }
+
+ @Test
+ void testLoadStaticTableDoesNotRetryDeniedRead() {
+ FileIO io = mock(FileIO.class);
+ when(io.newInputFile(METADATA_LOCATION))
+ .thenThrow(S3Exception.builder().statusCode(403).build());
+ TableOperations ops = mock(TableOperations.class);
+
+ Table table = catalogWith(ops, io).loadStaticTable(IDENTIFIER,
METADATA_LOCATION);
+
+ assertThrows(S3Exception.class, table::properties);
+ verify(io, times(1)).newInputFile(METADATA_LOCATION);
+ verify(ops, never()).current();
+ verify(ops, never()).refresh();
+ }
+
+ @Test
+ void testLoadStaticTableRequiresMetadataLocation() {
+ GravitinoGlueCatalog catalog = catalogWith(mock(TableOperations.class),
mock(FileIO.class));
+
+ assertThrows(NoSuchTableException.class, () ->
catalog.loadStaticTable(IDENTIFIER, null));
+ }
+
+ @Test
+ void testLoadStaticTableKeepsSourceOperationsReachable() {
+ AtomicReference<WeakReference<TableOperations>> source = new
AtomicReference<>();
+ GravitinoGlueCatalog catalog =
+ new GravitinoGlueCatalog() {
+ @Override
+ protected TableOperations newTableOps(TableIdentifier identifier) {
+ TableOperations ops = new StaticTableOperations(METADATA_LOCATION,
mock(FileIO.class));
+ source.set(new WeakReference<>(ops));
+ return ops;
+ }
+ };
+
+ Table table = catalog.loadStaticTable(IDENTIFIER, METADATA_LOCATION);
+ for (int i = 0; i < 10 && source.get().get() != null; i++) {
+ System.gc();
+ }
+
+ // The catalog closes the FileIO of operations that were garbage collected.
+ assertNotNull(source.get().get());
+ assertEquals(IDENTIFIER.toString(), table.name());
+ }
+
+ private static GravitinoGlueCatalog catalogWith(TableOperations ops, FileIO
io) {
+ when(ops.io()).thenReturn(io);
+ return new GravitinoGlueCatalog() {
+ @Override
+ protected TableOperations newTableOps(TableIdentifier identifier) {
+ return ops;
+ }
+ };
+ }
+}