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;
+      }
+    };
+  }
+}

Reply via email to