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;