This is an automated email from the ASF dual-hosted git repository.

danny0405 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new a2d88d9229fd fix(hive-sync): keep the sync client's metastore client 
out of Hive's thread cache (#20219)
a2d88d9229fd is described below

commit a2d88d9229fd9bb20f33c9826ca75a138127a559
Author: Shihuan Liu <[email protected]>
AuthorDate: Mon Oct 12 22:14:18 2026 -0700

    fix(hive-sync): keep the sync client's metastore client out of Hive's 
thread cache (#20219)
    
    * fix(hive-sync): keep the sync client's metastore client out of Hive's 
thread cache
    
    HoodieHiveSyncClient took its metastore client from the Hive instance cached
    for the thread. In HiveQL mode, the Driver session's authorization setup
    changes the metastore filter hook and asks Hive for an instance with that 
conf,
    so Hive replaced the cached instance and closed the sync client's metastore
    client. Every sync then relied on RetryingMetaStoreClient's single 
reconnect.
    
    Take the instance out of the cache with Hive.set(null) once the client is
    created, so a Hive session on the thread builds an instance of its own.
    
    * fix(hive-sync): release the metastore client from Hive's cache lazily, 
close it on failed construction
    
    - Leave the sync client's metastore client in Hive's thread cache until the 
session's Driver is
      about to run its first statement, so a sync that runs none lets the 
session teardown reuse it
      instead of opening a connection of its own.
    - Close the metastore client and partition pools when HoodieHiveSyncClient 
fails to construct.
    - Make HiveQueryDDLExecutor's session teardown run once: HiveSyncTool is 
closed by
      syncHoodieTable() and again by try-with-resources callers, and closing 
the session a second
      time opened a metastore connection.
    
    * refactor(hive-sync): name the no-op session hook, rename 
threadCachingClient, explain closing the client on failed construction
    
    * docs(hive-sync): simplify closeClient() Javadoc
    
    * refactor(hive-sync): centralize constructor failure cleanup
    
    * refactor(hive-sync): detach cached client in the HiveQL executor
    
    * refactor(hive-sync): use Option for the cache-owning thread
    
    * refactor(hive-sync): leave failed-construction pool cleanup to the caller
    
    ---------
    
    Co-authored-by: danny0405 <[email protected]>
---
 .../org/apache/hudi/hive/HoodieHiveSyncClient.java |  71 ++++------
 .../apache/hudi/hive/ddl/HiveQueryDDLExecutor.java |  30 +++--
 .../org/apache/hudi/hive/TestHiveSyncTool.java     | 146 +++++++++++++++++++++
 .../hive/ddl/TestHiveQueryDDLExecutorSession.java  |  36 +++++
 4 files changed, 230 insertions(+), 53 deletions(-)

diff --git 
a/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HoodieHiveSyncClient.java
 
b/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HoodieHiveSyncClient.java
index f46bbffce638..fd7dde82a96a 100644
--- 
a/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HoodieHiveSyncClient.java
+++ 
b/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HoodieHiveSyncClient.java
@@ -137,7 +137,7 @@ public class HoodieHiveSyncClient extends HoodieSyncClient {
             ddlExecutor = new HMSDDLExecutor(config, this.client);
             break;
           case HIVEQL:
-            ddlExecutor = buildHiveQueryDDLExecutor(config);
+            ddlExecutor = buildHiveQueryDDLExecutor();
             break;
           case JDBC:
             JDBCExecutor jdbcExecutor = new JDBCExecutor(config);
@@ -155,22 +155,36 @@ public class HoodieHiveSyncClient extends 
HoodieSyncClient {
           jdbcMetadataOperator = new JDBCBasedMetadataOperator(
               jdbcExecutor.getConnection(), databaseName);
         } else {
-          ddlExecutor = buildHiveQueryDDLExecutor(config);
+          ddlExecutor = buildHiveQueryDDLExecutor();
         }
       }
     } catch (Exception e) {
-      // The pools own live daemon threads, Hive Drivers, and Thrift sockets, 
and are
-      // built before the executor that would otherwise own their lifecycle. 
Any throw
-      // between those two points would leak them -- notably 
QueryBasedDDLExecutor's
-      // super(config), which runs the PartitionValueExtractor reflection 
before
-      // HiveQueryDDLExecutor's own try block is even entered. Closing here 
covers every
-      // such window; both close() methods are idempotent, so overlapping with 
the
-      // executor's cleanup (or buildHiveQueryDDLExecutor's rollback) is 
harmless.
+      // Construction failed, so no caller can close these resources. This 
also covers
+      // failures before HiveQueryDDLExecutor enters its own constructor try 
block.
       closePartitionPoolsQuietly();
+      closeClient();
       throw new HoodieHiveSyncException("Failed to create 
HiveMetaStoreClient", e);
     }
   }
 
+  /**
+   * Closes {@link #client} directly, since Hive no longer closes it once it 
is out of the cache.
+   * This also releases a client that RetryingMetaStoreClient rebuilt after a 
transient TException.
+   * Hive.closeCurrent() then closes whatever instance is left in the cache.
+   */
+  private void closeClient() {
+    if (client == null) {
+      return;
+    }
+    try {
+      client.close();
+    } catch (Exception e) {
+      log.warn("Failed to close IMetaStoreClient directly; Hive.closeCurrent() 
will run anyway", e);
+    }
+    Hive.closeCurrent();
+    client = null;
+  }
+
   private void closePartitionPoolsQuietly() {
     partitionDriverPool.ifPresent(pool -> {
       try {
@@ -265,23 +279,11 @@ public class HoodieHiveSyncClient extends 
HoodieSyncClient {
     return Option.of(new HiveDriverPool(config, size));
   }
 
-  /**
-   * Builds the (optional) partition-phase pools and the {@link 
HiveQueryDDLExecutor}
-   * that uses them, rolling back whichever pool(s) already got built if a 
later step
-   * in this sequence throws. Without this, a failure in {@code 
maybeBuildPartitionClientPool}
-   * (after {@code partitionDriverPool} was already built) or in the 
executor's own
-   * constructor would leak the already-built pool's worker threads and 
Thrift/Driver
-   * connections, since this constructor's outer catch just rethrows.
-   */
-  private HiveQueryDDLExecutor buildHiveQueryDDLExecutor(HiveSyncConfig 
config) {
-    try {
-      this.partitionDriverPool = maybeBuildHiveDriverPool(config);
-      this.partitionClientPool = maybeBuildPartitionClientPool(config);
-      return new HiveQueryDDLExecutor(config, this.client, 
this.partitionDriverPool, this.partitionClientPool);
-    } catch (Exception e) {
-      closePartitionPoolsQuietly();
-      throw e;
-    }
+  private HiveQueryDDLExecutor buildHiveQueryDDLExecutor() {
+    partitionDriverPool = maybeBuildHiveDriverPool(config);
+    partitionClientPool = maybeBuildPartitionClientPool(config);
+    return new HiveQueryDDLExecutor(config, client, partitionDriverPool, 
partitionClientPool,
+        config.getBooleanOrDefault(HIVE_SYNC_USE_SPARK_CATALOG) ? 
Option.empty() : Option.of(Thread.currentThread()));
   }
 
   private Table getInitialTable(String table) {
@@ -695,22 +697,7 @@ public class HoodieHiveSyncClient extends HoodieSyncClient 
{
           }
           partitionClientPool = Option.empty();
         }
-        if (client != null) {
-          // Close the proxied IMetaStoreClient directly before 
Hive.closeCurrent().
-          // When RetryingMetaStoreClient rebuilds the underlying client on a 
transient
-          // TException, the fresh MSC is reachable only through this proxy, 
while the
-          // thread-local Hive singleton still references the older instance. 
So
-          // Hive.closeCurrent() alone closes the stale MSC and orphans the 
retry-created
-          // one, leaking a connection per sync cycle. client.close() releases 
the live
-          // MSC by identity; Hive.closeCurrent() remains a fallback for the 
singleton path.
-          try {
-            client.close();
-          } catch (Exception e) {
-            log.warn("Failed to close IMetaStoreClient directly; 
Hive.closeCurrent() will run anyway", e);
-          }
-          Hive.closeCurrent();
-          client = null;
-        }
+        closeClient();
       }
     } catch (Exception e) {
       log.error("Could not close connection ", e);
diff --git 
a/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/ddl/HiveQueryDDLExecutor.java
 
b/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/ddl/HiveQueryDDLExecutor.java
index ec674db4daba..91576a8b963d 100644
--- 
a/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/ddl/HiveQueryDDLExecutor.java
+++ 
b/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/ddl/HiveQueryDDLExecutor.java
@@ -65,18 +65,22 @@ public class HiveQueryDDLExecutor extends 
QueryBasedDDLExecutor {
   // Owned by HoodieHiveSyncClient; close() is delegated through there. See
   // HiveMetaStoreClientPool javadoc for the usage contract (partition-row ops 
only).
   private final Option<HiveMetaStoreClientPool> metaStoreClientPool;
+  // Present while the sync client's metastore client is cached on this thread.
+  private volatile Option<Thread> threadCachingClient;
 
   public HiveQueryDDLExecutor(HiveSyncConfig config, IMetaStoreClient 
metaStoreClient) {
-    this(config, metaStoreClient, Option.empty(), Option.empty());
+    this(config, metaStoreClient, Option.empty(), Option.empty(), 
Option.empty());
   }
 
   public HiveQueryDDLExecutor(HiveSyncConfig config, IMetaStoreClient 
metaStoreClient,
                               Option<HiveDriverPool> driverPool,
-                              Option<HiveMetaStoreClientPool> 
metaStoreClientPool) {
+                              Option<HiveMetaStoreClientPool> 
metaStoreClientPool,
+                              Option<Thread> threadCachingClient) {
     super(config);
     this.metaStoreClient = metaStoreClient;
     this.driverPool = driverPool;
     this.metaStoreClientPool = metaStoreClientPool;
+    this.threadCachingClient = threadCachingClient;
     // SessionState.start() binds the session it starts to this thread, 
displacing the caller's.
     // Statements and the teardown bind ours themselves, so the thread is 
handed back once the
     // Driver is built -- its constructor is what reads SessionState.get().
@@ -100,15 +104,7 @@ public class HiveQueryDDLExecutor extends 
QueryBasedDDLExecutor {
       } catch (Exception teardownException) {
         log.error("Error while closing Hive Driver and SessionState", 
teardownException);
       }
-      // driverPool (if present) was already constructed by the caller before 
this
-      // ctor ran; since we're about to throw, no one else will call close() 
on it.
-      driverPool.ifPresent(pool -> {
-        try {
-          pool.close();
-        } catch (Exception poolCloseException) {
-          log.error("Error while closing HiveDriverPool", poolCloseException);
-        }
-      });
+      // The caller closes the supplied pools if construction fails.
       throw new HoodieHiveSyncException("Failed to create HiveQueryDDL 
object", e);
     } finally {
       restoreThread(previousSession, previousLoader);
@@ -183,6 +179,13 @@ public class HiveQueryDDLExecutor extends 
QueryBasedDDLExecutor {
    */
   private void updateHiveSQLs(List<String> sqls) {
     HoodieTimer timer = HoodieTimer.start();
+    // Authorization can replace Hive's cached instance and close its client. 
Detach without
+    // closing, only on the owning thread and only once. A sync with no 
statements stays cached
+    // so session teardown can reuse its connection.
+    if (threadCachingClient.map(thread -> thread == 
Thread.currentThread()).orElse(false)) {
+      Hive.set(null);
+      threadCachingClient = Option.empty();
+    }
     SessionState previousSession = SessionState.get();
     ClassLoader previousLoader = 
Thread.currentThread().getContextClassLoader();
     try {
@@ -315,6 +318,7 @@ public class HiveQueryDDLExecutor extends 
QueryBasedDDLExecutor {
 
   @Override
   public void close() {
+    threadCachingClient = Option.empty();
     // Close the pool first so the worker threads stop dispatching against 
their
     // Drivers before we tear down anything else. The pool's close() runs
     // Driver/SessionState cleanup on each worker's own thread.
@@ -373,6 +377,10 @@ public class HiveQueryDDLExecutor extends 
QueryBasedDDLExecutor {
       if (previousSession != sessionState) {
         restoreThread(previousSession, previousLoader);
       }
+      // HiveSyncTool closes after a sync and again when its caller does, and 
closing the session a
+      // second time would open a metastore connection just to uncache the 
class loaders again
+      hiveDriver = null;
+      sessionState = null;
     }
   }
 
diff --git 
a/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncTool.java
 
b/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncTool.java
index 0bb527d0b013..7f758b79dbfa 100644
--- 
a/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncTool.java
+++ 
b/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncTool.java
@@ -49,6 +49,8 @@ import org.apache.hudi.hadoop.utils.HoodieInputFormatUtils;
 import org.apache.hudi.hive.ddl.HMSDDLExecutor;
 import org.apache.hudi.hive.ddl.HiveSyncMode;
 import org.apache.hudi.hive.testutils.HiveTestUtil;
+import org.apache.hudi.hive.util.HiveDriverPool;
+import org.apache.hudi.hive.util.HiveMetaStoreClientPool;
 import org.apache.hudi.hive.util.IMetaStoreClientUtil;
 import org.apache.hudi.metrics.MetricsReporterType;
 import org.apache.hudi.storage.hadoop.HadoopStorageConfiguration;
@@ -58,12 +60,20 @@ import org.apache.hudi.sync.common.model.PartitionEvent;
 import org.apache.hudi.sync.common.model.PartitionEvent.PartitionEventType;
 
 import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hive.conf.HiveConf;
 import org.apache.hadoop.hive.metastore.IMetaStoreClient;
 import org.apache.hadoop.hive.metastore.api.StorageDescriptor;
 import org.apache.hadoop.hive.metastore.api.Table;
 import org.apache.hadoop.hive.ql.Driver;
 import org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe;
+import org.apache.hadoop.hive.ql.metadata.Hive;
 import org.apache.hadoop.hive.ql.session.SessionState;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Property;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -73,6 +83,7 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.EnumSource;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.MockedConstruction;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
@@ -87,6 +98,8 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
 import static 
org.apache.hudi.common.table.HoodieTableMetaClient.TIMELINEFOLDER_NAME;
@@ -132,6 +145,8 @@ import static 
org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.verify;
 
 public class TestHiveSyncTool {
 
@@ -404,6 +419,137 @@ public class TestHiveSyncTool {
         "Incremental add via parallel HiveQL batching should sync the new 
partitions");
   }
 
+  /**
+   * The first statement the Hive Driver compiles sets up its session's 
authorization, which changes
+   * the session's metastore filter hook. If the sync client's metastore 
client were the one Hive
+   * caches for the thread, Hive would close it then, and the sync's next 
metastore call would fail.
+   */
+  @Test
+  void testHiveQLSyncKeepsItsMetastoreClientOpen() throws Exception {
+    hiveSyncProps.setProperty(HIVE_SYNC_MODE.key(), 
HiveSyncMode.HIVEQL.name());
+    HiveTestUtil.createCOWTable("100", 5, true);
+
+    try (HiveSyncTool tool = new HiveSyncTool(hiveSyncProps, 
getHiveClientConf())) {
+      tool.syncHoodieTable();
+    }
+
+    reInitHiveSyncClient();
+    assertEquals(5, 
hiveClient.getAllPartitions(HiveTestUtil.TABLE_NAME).size());
+  }
+
+  /**
+   * A sync that finds the table synced to its latest commit runs no HiveQL 
statement, so its
+   * session never sets up authorization, and tearing the session down reuses 
the sync's metastore
+   * client rather than opening a connection of its own.
+   */
+  @Test
+  void testHiveQLSyncThatRunsNoStatementOpensOneMetastoreConnection() throws 
Exception {
+    hiveSyncProps.setProperty(HIVE_SYNC_MODE.key(), 
HiveSyncMode.HIVEQL.name());
+    HiveTestUtil.createCOWTable("100", 5, true);
+    HiveConf hiveConf = getHiveClientConf();
+    try (HiveSyncTool tool = new HiveSyncTool(hiveSyncProps, hiveConf)) {
+      tool.syncHoodieTable();
+    }
+
+    try (MetastoreConnectionCounter connections = new 
MetastoreConnectionCounter()) {
+      try (HiveSyncTool tool = new HiveSyncTool(hiveSyncProps, hiveConf)) {
+        tool.syncHoodieTable();
+      }
+      assertEquals(1, connections.opened(), "Only the sync's own metastore 
client connects");
+      assertEquals(1, connections.closed());
+    }
+  }
+
+  @Test
+  void testSyncClientThatFailsToBuildClosesItsMetastoreClient() throws 
Exception {
+    HiveTestUtil.createCOWTable("100", 5, true);
+    // the sync mode is read after the metastore client is created
+    hiveSyncProps.setProperty(HIVE_SYNC_MODE.key(), "not_a_sync_mode");
+    // otherwise creating the client closes what the test setup left in Hive's 
cache for the thread
+    Hive.closeCurrent();
+
+    try (MetastoreConnectionCounter connections = new 
MetastoreConnectionCounter()) {
+      assertThrows(HoodieHiveSyncException.class, () -> new 
HiveSyncTool(hiveSyncProps, getHiveClientConf()));
+      assertEquals(1, connections.opened());
+      assertEquals(1, connections.closed(), "The metastore client of a sync 
client that failed to build is closed");
+    }
+  }
+
+  @Test
+  void testSyncClientClosesPoolsWhenHiveQLSessionFailsToStart() throws 
Exception {
+    HiveTestUtil.createCOWTable("100", 5, true);
+    hiveSyncProps.setProperty(HIVE_SYNC_MODE.key(), 
HiveSyncMode.HIVEQL.name());
+    hiveSyncProps.setProperty(HIVE_SYNC_BATCHING_ENABLED.key(), "true");
+    HiveConf hiveConf = getHiveClientConf();
+    // Fail session startup after both pools have been constructed.
+    hiveConf.setVar(HiveConf.ConfVars.SCRATCHDIR,
+        
Files.createFile(tempDir.resolve("not-a-directory")).toUri().toString() + 
"/scratch");
+
+    try (MockedConstruction<HiveDriverPool> drivers = 
mockConstruction(HiveDriverPool.class);
+         MockedConstruction<HiveMetaStoreClientPool> clients = 
mockConstruction(HiveMetaStoreClientPool.class)) {
+      assertThrows(HoodieHiveSyncException.class, () -> new 
HiveSyncTool(hiveSyncProps, hiveConf));
+      assertEquals(1, drivers.constructed().size());
+      assertEquals(1, clients.constructed().size());
+      verify(drivers.constructed().get(0)).close();
+      verify(clients.constructed().get(0)).close();
+    }
+  }
+
+  /**
+   * A client's conf, as opposed to the test server's, which has had 
authorization applied to it.
+   */
+  private static HiveConf getHiveClientConf() {
+    HiveConf hiveConf = new HiveConf(getHiveConf());
+    hiveConf.unset("hive.internal.ss.authz.settings.applied.marker");
+    hiveConf.setVar(HiveConf.ConfVars.METASTORE_FILTER_HOOK, 
HiveConf.ConfVars.METASTORE_FILTER_HOOK.getDefaultValue());
+    // without the retry that would reconnect it, a closed metastore client 
fails the call
+    hiveConf.setIntVar(HiveConf.ConfVars.METASTORETHRIFTFAILURERETRIES, 0);
+    return hiveConf;
+  }
+
+  /**
+   * Counts the connections to the metastore that clients open and close, from 
what
+   * HiveMetaStoreClient logs for each.
+   */
+  private static final class MetastoreConnectionCounter extends 
AbstractAppender implements AutoCloseable {
+    private final Logger logger = (Logger) 
LogManager.getLogger("hive.metastore");
+    private final Level previousLevel = logger.getLevel();
+    private final AtomicInteger opened = new AtomicInteger();
+    private final AtomicInteger closed = new AtomicInteger();
+
+    MetastoreConnectionCounter() {
+      super(UUID.randomUUID().toString(), null, null, false, 
Property.EMPTY_ARRAY);
+      start();
+      logger.addAppender(this);
+      logger.setLevel(Level.INFO);
+    }
+
+    @Override
+    public void append(LogEvent event) {
+      String message = event.getMessage().getFormattedMessage();
+      if (message.startsWith("Opened a connection to metastore")) {
+        opened.incrementAndGet();
+      } else if (message.startsWith("Closed a connection to metastore")) {
+        closed.incrementAndGet();
+      }
+    }
+
+    int opened() {
+      return opened.get();
+    }
+
+    int closed() {
+      return closed.get();
+    }
+
+    @Override
+    public void close() {
+      logger.removeAppender(this);
+      logger.setLevel(previousLevel);
+      stop();
+    }
+  }
+
   /**
    * Hive's Driver reports a rejected statement through its response code 
rather than by throwing.
    * A table registered with a different partition column than the one the 
sync is configured with
diff --git 
a/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/ddl/TestHiveQueryDDLExecutorSession.java
 
b/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/ddl/TestHiveQueryDDLExecutorSession.java
index 2c5947e10014..84644985fbf0 100644
--- 
a/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/ddl/TestHiveQueryDDLExecutorSession.java
+++ 
b/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/ddl/TestHiveQueryDDLExecutorSession.java
@@ -34,6 +34,8 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.InOrder;
 import org.mockito.MockedStatic;
 
@@ -176,6 +178,39 @@ class TestHiveQueryDDLExecutorSession {
         "detachSession() leaves the session's class loader on the thread, so 
the operation must take it off");
   }
 
+  @Test
+  void detachesCachedClientOnceBeforeRunningStatements() throws Exception {
+    Driver driver = mock(Driver.class);
+    HiveQueryDDLExecutor executor = executorWith(driver, 
mock(SessionState.class));
+    setField(executor, "threadCachingClient", 
Option.of(Thread.currentThread()));
+
+    try (MockedStatic<Hive> hive = mockStatic(Hive.class)) {
+      when(driver.run(anyString())).thenAnswer(invocation -> {
+        hive.verify(() -> Hive.set(null), times(1));
+        return new CommandProcessorResponse(0);
+      });
+      hive.verifyNoInteractions();
+      executor.runSQL("SHOW TABLES");
+      executor.runSQL("SHOW DATABASES");
+      hive.verify(() -> Hive.set(null), times(1));
+      hive.verifyNoMoreInteractions();
+    }
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  void leavesCacheAloneWhenClientIsNotCachedOnCallingThread(boolean 
cachedOnAnotherThread) throws Exception {
+    Driver driver = mock(Driver.class);
+    when(driver.run(anyString())).thenReturn(new CommandProcessorResponse(0));
+    HiveQueryDDLExecutor executor = executorWith(driver, 
mock(SessionState.class));
+    setField(executor, "threadCachingClient", cachedOnAnotherThread ? 
Option.of(new Thread()) : Option.empty());
+
+    try (MockedStatic<Hive> hive = mockStatic(Hive.class)) {
+      executor.runSQL("SHOW TABLES");
+      hive.verifyNoInteractions();
+    }
+  }
+
   /**
    * The thread belongs to the caller, which may be another executor or an 
application that embeds
    * the sync and holds a session of its own. Leaving this executor's session 
behind would silently
@@ -453,6 +488,7 @@ class TestHiveQueryDDLExecutorSession {
     setField(executor, "driverPool", Option.empty());
     setField(executor, "metaStoreClientPool", Option.empty());
     setField(executor, "metaStoreClient", null);
+    setField(executor, "threadCachingClient", Option.empty());
     setField(executor, "hiveDriver", driver);
     setField(executor, "sessionState", sessionState);
     return executor;

Reply via email to