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 f76b7a703c [#12985] improvement(client): make GravitinoClient 
construction free of remote calls (#13104)
f76b7a703c is described below

commit f76b7a703c724978b9485d5d848d515a2f65506c
Author: li jie <[email protected]>
AuthorDate: Wed Sep 16 19:03:40 2026 +0800

    [#12985] improvement(client): make GravitinoClient construction free of 
remote calls (#13104)
    
    ### What changes were proposed in this pull request?
    
    - Move `loadMetalake` from the `GravitinoClient` constructor to lazy
    initialization in `getMetalake()` using double-checked locking (volatile
    + synchronized).
    - Store only the metalake name in the constructor; the metalake handle
    is loaded on first use.
    - Remove `@throws NoSuchMetalakeException` from the constructor and
    `build()` Javadoc; update `Command.buildClient()` Javadoc to reflect the
    deferred exception timing.
    - Add `testNoSuchMetalakeOnFirstOperation` to verify that
    `NoSuchMetalakeException` surfaces on the first operation, not at
    `build()`.
    
    ### Why are the changes needed?
    
    `GravitinoClient`'s constructor performs a remote `loadMetalake` call
    that runs with whatever credentials the `AuthDataProvider` resolves at
    construction time. For a shared client whose provider resolves the
    caller per request (the standalone Lance REST service after #12984),
    this causes the permission requirement to land non-deterministically on
    the first caller, failed construction to retry on every request, and
    metalake-load errors to surface instead of the operation the caller
    requested.
    
    Fix: #12985
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. `NoSuchMetalakeException` is now thrown on the first operation
    (e.g. `listCatalogs()`) instead of at `build()` time. No API or
    configuration changes.
    
    ### How was this patch tested?
    
    - `./gradlew :clients:client-java:spotlessCheck
    :clients:cli:spotlessCheck :clients:client-java:compileTestJava
    :clients:cli:compileJava`
    - Ran `TestGravitinoMetalake` (21 tests) and
    `TestGravitinoClientBuilder` (3 tests).
    
    ---------
    
    Co-authored-by: lijie <[email protected]>
---
 .../apache/gravitino/client/GravitinoClient.java   | 29 ++++++++++++++++------
 .../gravitino/client/TestGravitinoMetalake.java    | 21 ++++++++++++++++
 .../plugin/TestGravitinoDriverPlugin.java          | 29 +++++++++++-----------
 3 files changed, 57 insertions(+), 22 deletions(-)

diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
index 9a7fb8e749..3366e63a1d 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
@@ -85,11 +85,15 @@ import org.apache.gravitino.tag.TagValueConstraint;
 public class GravitinoClient extends GravitinoClientBase
     implements SupportsCatalogs, TagOperations, SupportsJobs, PolicyOperations 
{
 
-  private final GravitinoMetalake metalake;
+  private final String metalakeName;
+
+  private volatile GravitinoMetalake metalake;
 
   /**
    * Constructs a new GravitinoClient with the given URI, authenticator and 
AuthDataProvider.
    *
+   * <p>The metalake is loaded lazily on first use, not in the constructor.
+   *
    * @param uri The base URI for the Gravitino API.
    * @param metalakeName The specified metalake name.
    * @param authDataProvider The provider of the data which is used for 
authentication.
@@ -97,7 +101,6 @@ public class GravitinoClient extends GravitinoClientBase
    *     support the case that the client-side version is higher than the 
server-side version.
    * @param headers The base header for Gravitino API.
    * @param properties A map of properties (key-value pairs) used to configure 
the Gravitino client.
-   * @throws NoSuchMetalakeException if the metalake with specified name does 
not exist.
    */
   private GravitinoClient(
       String uri,
@@ -107,17 +110,27 @@ public class GravitinoClient extends GravitinoClientBase
       Map<String, String> headers,
       Map<String, String> properties) {
     super(uri, authDataProvider, checkVersion, headers, properties);
-    this.metalake = loadMetalake(metalakeName);
+    this.metalakeName = metalakeName;
   }
 
   /**
-   * Get the current metalake object
+   * Returns the metalake, loading it on first access.
    *
    * @return the {@link GravitinoMetalake} object
    * @throws NoSuchMetalakeException if the metalake with specified name does 
not exist.
    */
-  private GravitinoMetalake getMetalake() {
-    return metalake;
+  private GravitinoMetalake getMetalake() throws NoSuchMetalakeException {
+    GravitinoMetalake result = metalake;
+    if (result == null) {
+      synchronized (this) {
+        result = metalake;
+        if (result == null) {
+          result = loadMetalake(metalakeName);
+          metalake = result;
+        }
+      }
+    }
+    return result;
   }
 
   @Override
@@ -752,9 +765,11 @@ public class GravitinoClient extends GravitinoClientBase
     /**
      * Builds a new GravitinoClient instance.
      *
+     * <p>The metalake is loaded lazily on first use; {@link 
NoSuchMetalakeException} is thrown at
+     * that point, not here.
+     *
      * @return A new instance of GravitinoClient with the specified base URI.
      * @throws IllegalArgumentException If the base URI is null or empty.
-     * @throws NoSuchMetalakeException if the metalake with specified name 
does not exist.
      */
     @Override
     public GravitinoClient build() {
diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoMetalake.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoMetalake.java
index 09ba0761b7..59e413684c 100644
--- 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoMetalake.java
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoMetalake.java
@@ -1173,6 +1173,27 @@ public class TestGravitinoMetalake extends TestBase {
     Assertions.assertNotEquals(metalake1, new Object());
   }
 
+  @Test
+  public void testNoSuchMetalakeOnFirstOperation() throws 
JsonProcessingException {
+    // NoSuchMetalakeException surfaces on first operation, not at build().
+    String missingMetalake = "nonexistent-metalake";
+    ErrorResponse errorResp =
+        ErrorResponse.notFound(NoSuchMetalakeException.class.getSimpleName(), 
"metalake not found");
+    buildMockResource(
+        Method.GET, "/api/metalakes/" + missingMetalake, null, errorResp, 
HttpStatus.SC_NOT_FOUND);
+
+    try (GravitinoClient client =
+        GravitinoClient.builder("http://127.0.0.1:"; + 
mockServer.getLocalPort())
+            .withMetalake(missingMetalake)
+            .withVersionCheckDisabled()
+            .build()) {
+      // build() succeeded — exception must surface on first operation
+      Throwable ex =
+          Assertions.assertThrows(NoSuchMetalakeException.class, () -> 
client.listCatalogs());
+      Assertions.assertTrue(ex.getMessage().contains("metalake not found"));
+    }
+  }
+
   static GravitinoMetalake createMetalake(GravitinoAdminClient client, String 
metalakeName)
       throws JsonProcessingException {
     return createMetalake(client, metalakeName, false);
diff --git 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/TestGravitinoDriverPlugin.java
 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/TestGravitinoDriverPlugin.java
index 4ace94b2df..bc17c21a48 100644
--- 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/TestGravitinoDriverPlugin.java
+++ 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/TestGravitinoDriverPlugin.java
@@ -31,6 +31,7 @@ import java.nio.file.Path;
 import java.util.Arrays;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.auth.AuthProperties;
+import org.apache.gravitino.client.GravitinoClient;
 import org.apache.gravitino.spark.connector.GravitinoSparkConfig;
 import org.apache.gravitino.spark.connector.catalog.SparkCatalogKind;
 import 
org.apache.gravitino.spark.connector.iceberg.extensions.GravitinoIcebergSparkSessionExtensions;
@@ -314,16 +315,12 @@ public class TestGravitinoDriverPlugin {
     SparkConf sparkConf = tokenAuthConf();
     sparkConf.set(GravitinoSparkConfig.GRAVITINO_TOKEN_VALUE, "a-token");
 
-    // The client cannot reach a server here, but it must get past auth 
configuration first: an
-    // unsupported auth type or a missing token would fail before any 
connection is attempted.
-    Exception e =
-        Assertions.assertThrows(
-            Exception.class,
-            () ->
-                GravitinoDriverPlugin.createGravitinoClient(
-                    "http://127.0.0.1:1";, "metalake", sparkConf, "user", 
ImmutableMap.of()));
-    Assertions.assertFalse(e instanceof UnsupportedOperationException, 
e.toString());
-    Assertions.assertFalse(e instanceof IllegalArgumentException, 
e.toString());
+    // build() no longer contacts the server; the token is resolved on first 
use.
+    GravitinoClient client =
+        GravitinoDriverPlugin.createGravitinoClient(
+            "http://127.0.0.1:1";, "metalake", sparkConf, "user", 
ImmutableMap.of());
+    Assertions.assertNotNull(client);
+    client.close();
   }
 
   @Test
@@ -373,14 +370,16 @@ public class TestGravitinoDriverPlugin {
   void testTokenAuthTypeWithoutTokenFails() {
     SparkConf sparkConf = tokenAuthConf();
 
+    // build() succeeds without a token; the failure surfaces on first use 
when the
+    // token provider tries to resolve the token.
+    GravitinoClient client =
+        GravitinoDriverPlugin.createGravitinoClient(
+            "http://127.0.0.1:1";, "metalake", sparkConf, "user", 
ImmutableMap.of());
     IllegalArgumentException e =
-        Assertions.assertThrows(
-            IllegalArgumentException.class,
-            () ->
-                GravitinoDriverPlugin.createGravitinoClient(
-                    "http://127.0.0.1:1";, "metalake", sparkConf, "user", 
ImmutableMap.of()));
+        Assertions.assertThrows(IllegalArgumentException.class, 
client::listCatalogs);
     
Assertions.assertTrue(e.getMessage().contains(GravitinoSparkConfig.GRAVITINO_TOKEN_VALUE));
     
Assertions.assertTrue(e.getMessage().contains(GravitinoSparkConfig.GRAVITINO_TOKEN_FILE));
+    client.close();
   }
 
   private static SparkConf tokenAuthConf() {

Reply via email to