Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.apache.hudi.hive.ddl.JDBCBasedMetadataOperator;
import org.apache.hudi.hive.ddl.JDBCExecutor;
import org.apache.hudi.hive.util.HiveDriverPool;
import org.apache.hudi.hive.util.IMetaStoreClientPool;
import org.apache.hudi.hive.util.IMetaStoreClientUtil;
import org.apache.hudi.hive.util.PartitionFilterGenerator;
import org.apache.hudi.sync.common.HoodieSyncClient;
Expand Down Expand Up @@ -89,6 +90,11 @@ public class HoodieHiveSyncClient extends HoodieSyncClient {
private final Map<String, Table> initialTableByName = new HashMap<>();
DDLExecutor ddlExecutor;
private IMetaStoreClient client;
// Present only when HIVE_SYNC_BATCHING_ENABLED and sync mode is HIVEQL. Owned by
// this class; closed in close() before Hive.closeCurrent(). HiveQueryDDLExecutor
// uses it only for DROP (Hive Thrift, not Hive Driver) — see IMetaStoreClientPool
// javadoc.
private Option<IMetaStoreClientPool> partitionClientPool = Option.empty();
// Present only when HIVE_SYNC_BATCHING_ENABLED and sync mode is HIVEQL (explicit
// or legacy default). Owned by HiveQueryDDLExecutor; this field is kept for
// reference only — close() is delegated through ddlExecutor.close().
Expand Down Expand Up @@ -131,8 +137,7 @@ public HoodieHiveSyncClient(HiveSyncConfig config, HoodieTableMetaClient metaCli
ddlExecutor = new HMSDDLExecutor(config, this.client);
break;
case HIVEQL:
this.partitionDriverPool = maybeBuildHiveDriverPool(config);
ddlExecutor = new HiveQueryDDLExecutor(config, this.client, this.partitionDriverPool);
ddlExecutor = buildHiveQueryDDLExecutor(config);
break;
case JDBC:
JDBCExecutor jdbcExecutor = new JDBCExecutor(config);
Expand All @@ -150,30 +155,39 @@ public HoodieHiveSyncClient(HiveSyncConfig config, HoodieTableMetaClient metaCli
jdbcMetadataOperator = new JDBCBasedMetadataOperator(
jdbcExecutor.getConnection(), databaseName);
} else {
this.partitionDriverPool = maybeBuildHiveDriverPool(config);
ddlExecutor = new HiveQueryDDLExecutor(config, this.client, this.partitionDriverPool);
ddlExecutor = buildHiveQueryDDLExecutor(config);
}
}
} catch (Exception e) {
// The pool owns live daemon threads and Hive Drivers, and is built before the
// executor that would otherwise own its lifecycle. Any throw between those two
// points would leak it -- 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; the pool's
// close() is idempotent, so overlapping with the executor's cleanup is harmless.
closePartitionDriverPoolQuietly();
// 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.
closePartitionPoolsQuietly();
throw new HoodieHiveSyncException("Failed to create HiveMetaStoreClient", e);
}
}

private void closePartitionDriverPoolQuietly() {
private void closePartitionPoolsQuietly() {
partitionDriverPool.ifPresent(pool -> {
try {
pool.close();
} catch (Exception e) {
log.warn("Error closing HiveDriverPool during failed sync client construction", e);
}
});
partitionDriverPool = Option.empty();
partitionClientPool.ifPresent(pool -> {
try {
pool.close();
} catch (Exception e) {
log.warn("Error closing IMetaStoreClient pool during failed sync client construction", e);
}
});
partitionClientPool = Option.empty();
}

/**
Expand Down Expand Up @@ -227,6 +241,22 @@ private IMetaStoreClient createMetaStoreClient(HiveSyncConfig config) {
}
}

private Option<IMetaStoreClientPool> maybeBuildPartitionClientPool(HiveSyncConfig config) {
if (!config.getBooleanOrDefault(HIVE_SYNC_BATCHING_ENABLED)) {
return Option.empty();
}
if (config.getBooleanOrDefault(HIVE_SYNC_USE_SPARK_CATALOG)) {
// The Spark catalog client is constructed via reflection against a Spark-side

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: the config key names in this warning (hive_sync.batching.enabled, use_spark_catalog) don't match the actual property keys an operator would search for in logs. Could you use the real constant's .key() value, e.g. HIVE_SYNC_BATCHING_ENABLED.key() + "=true is not supported with " + HIVE_SYNC_USE_SPARK_CATALOG.key() + "=true"?

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

// class and isn't compatible with the direct RetryingMetaStoreClient pool path.
// Fall back to single-client sequential behavior rather than failing the sync.
log.warn("hive_sync.batching.enabled=true is not supported with use_spark_catalog=true; "
+ "falling back to sequential partition sync.");
return Option.empty();
}
int size = config.getIntOrDefault(HIVE_SYNC_BATCHING_THREADS);
return Option.of(new IMetaStoreClientPool(config, size));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Update the public batching config contract for DROP

HIVE_SYNC_BATCHING_ENABLED in HiveSyncConfigHolder still documents that "DROP remains serial," but this pool makes DROP parallel under that exact flag. That text is the user-facing configuration contract, so it currently tells operators the opposite of the new behavior. Please update it to cover the Thrift-client DROP fan-out (including the Spark-catalog fallback) as part of this change.

}

private Option<HiveDriverPool> maybeBuildHiveDriverPool(HiveSyncConfig config) {
if (!config.getBooleanOrDefault(HIVE_SYNC_BATCHING_ENABLED)) {
return Option.empty();
Expand All @@ -235,6 +265,25 @@ private Option<HiveDriverPool> maybeBuildHiveDriverPool(HiveSyncConfig config) {
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 Table getInitialTable(String table) {
return initialTableByName.computeIfAbsent(table, t -> {
try {
Expand Down Expand Up @@ -630,22 +679,38 @@ public void deleteLastReplicatedTimeStamp(String tableName) {
@Override
public void close() {
try {
ddlExecutor.close();
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);
try {
ddlExecutor.close();
} finally {
// Close the partition client pool before Hive.closeCurrent() so the
// RetryingMetaStoreClient instances held by the pool release their Thrift
// sockets without racing the ThreadLocal Hive cleanup. Runs even if
// ddlExecutor.close() above threw, so the pool's Thrift sockets and
// worker threads aren't leaked.
if (partitionClientPool.isPresent()) {
try {
partitionClientPool.get().close();
} catch (Exception e) {
log.warn("Error closing IMetaStoreClient pool", e);
}
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;
}
Hive.closeCurrent();
client = null;
}
} catch (Exception e) {
log.error("Could not close connection ", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@

package org.apache.hudi.hive.ddl;

import org.apache.hudi.common.util.CollectionUtils;
import org.apache.hudi.common.util.HoodieTimer;
import org.apache.hudi.common.util.Option;
import org.apache.hudi.hive.HiveSyncConfig;
import org.apache.hudi.hive.HoodieHiveSyncException;
import org.apache.hudi.hive.util.HiveDriverPool;
import org.apache.hudi.hive.util.HivePartitionUtil;
import org.apache.hudi.hive.util.IMetaStoreClientPool;

import lombok.extern.slf4j.Slf4j;
import org.apache.hadoop.hive.metastore.IMetaStoreClient;
Expand All @@ -41,6 +43,8 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Future;
import java.util.stream.Collectors;

import static org.apache.hudi.hive.HiveSyncConfigHolder.HIVE_BATCH_SYNC_PARTITION_NUM;
Expand All @@ -59,16 +63,22 @@ public class HiveQueryDDLExecutor extends QueryBasedDDLExecutor {
// (createTable, schema evolution, single-statement runSQL callers) always uses the
// session `hiveDriver` above. See HiveDriverPool javadoc.
private final Option<HiveDriverPool> driverPool;
// When present, dropPartitionsToTable fans batches across this Thrift client pool.
// Owned by HoodieHiveSyncClient; close() is delegated through there. See
// IMetaStoreClientPool javadoc for the usage contract (partition-row ops only).
private final Option<IMetaStoreClientPool> metaStoreClientPool;

public HiveQueryDDLExecutor(HiveSyncConfig config, IMetaStoreClient metaStoreClient) {
this(config, metaStoreClient, Option.empty());
this(config, metaStoreClient, Option.empty(), Option.empty());
}

public HiveQueryDDLExecutor(HiveSyncConfig config, IMetaStoreClient metaStoreClient,
Option<HiveDriverPool> driverPool) {
Option<HiveDriverPool> driverPool,
Option<IMetaStoreClientPool> metaStoreClientPool) {
super(config);
this.metaStoreClient = metaStoreClient;
this.driverPool = driverPool;
this.metaStoreClientPool = metaStoreClientPool;
try {
this.sessionState = new SessionState(config.getHiveConf(),
UserGroupInformation.getCurrentUser().getShortUserName());
Expand Down Expand Up @@ -209,21 +219,96 @@ public void dropPartitionsToTable(String tableName, List<String> partitionsToDro

log.info("Drop partitions {} on {}", partitionsToDrop.size(), tableName);
try {
for (String dropPartition : partitionsToDrop) {
if (HivePartitionUtil.partitionExists(metaStoreClient, tableName, dropPartition, partitionValueExtractor,
config)) {
String partitionClause =
HivePartitionUtil.getPartitionClauseForDrop(dropPartition, partitionValueExtractor, config);
metaStoreClient.dropPartition(databaseName, tableName, partitionClause, false);
}
log.info("Drop partition {} on {}", dropPartition, tableName);
}
int batchSyncPartitionNum = config.getIntOrDefault(HIVE_BATCH_SYNC_PARTITION_NUM);
List<List<String>> batches = CollectionUtils.batches(partitionsToDrop, batchSyncPartitionNum);
runDropBatches(tableName, batches);
} catch (Exception e) {
log.error("{} drop partition failed", tableId(databaseName, tableName), e);
throw new HoodieHiveSyncException(tableId(databaseName, tableName) + " drop partition failed", e);
}
}

/**
* Drops partitions one batch at a time. When {@link #metaStoreClientPool} is present,
* batches fan out across the pool's worker threads (each borrowing an independent
* IMetaStoreClient); otherwise batches are dispatched sequentially against the
* session client. Hive has no batch-drop primitive that matches dropPartition's
* semantics, so each worker still iterates its chunk one partition at a time — the
* win is fanning chunks across independent Thrift clients.
*
* <p>First-error semantics match {@code HiveDriverPool.awaitAll}: the first failure is
* rethrown, not-yet-started batches are cancelled, and later failures are logged at WARN.
*/
private void runDropBatches(String tableName, List<List<String>> batches) throws Exception {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: the parallel-path error-collection loop inside runDropBatches (Exception firstError = null; for (Future<Void> f : futures) …) is nearly identical to the one in HMSDDLExecutor.runBatches. Since HiveDriverPool already has a well-factored awaitAll(List<Future<?>>) for exactly this pattern, it might be worth adding a similar awaitAll(List<Future<Void>>) to IMetaStoreClientPool so both callers can delegate to it instead.

- AI-generated; verify before applying. React 👍/👎 to flag quality.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion — once #18984 merges and this branch rebases, HiveDriverPool.awaitAll(List<Future<?>>) will be available; will look at whether IMetaStoreClientPool's runDropBatches error-collection loop here can delegate to an analogous awaitAll on IMetaStoreClientPool at that point, to de-duplicate the two nearly-identical loops.

if (!metaStoreClientPool.isPresent()) {
for (List<String> batch : batches) {
applyDropBatch(metaStoreClient, tableName, batch);
}
return;
}
IMetaStoreClientPool pool = metaStoreClientPool.get();
List<Future<Void>> futures = new ArrayList<>(batches.size());
for (List<String> batch : batches) {
futures.add(pool.executor().submit(() ->
pool.run(poolClient -> {
applyDropBatch(poolClient, tableName, batch);
return null;
})
));
}

Exception firstError = null;
int cancelled = 0;
for (Future<Void> f : futures) {
// Once a batch has failed the sync is going to abort anyway, so stop handing
// more DROPs to the metastore. mayInterruptIfRunning=false mirrors
// HiveDriverPool.awaitAll: a batch already mid-flight runs to completion rather
// than leaving the partition list half-dropped at an arbitrary point.
if (firstError != null && f.cancel(false)) {
cancelled++;
continue;
}
try {
f.get();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Stop queued DROP batches as soon as a worker fails

This waits on futures in submission order. If a later batch fails quickly while an earlier batch is slow, this thread remains blocked on the earlier f.get(), and the shared executor can keep starting every queued batch because the tasks do not observe an abort flag. By the time firstError is set, most or all "not-yet-started" DROPs may already have run, so the advertised abort-on-first-error behavior is not achieved. Please mirror HiveDriverPool.Dispatch (shared task-side abort plus completion-order notification), or use a completion service/abort flag so pending batches are stopped immediately after any worker fails.

} catch (CancellationException ce) {
cancelled++;
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
if (firstError == null) {
firstError = ie;
}
} catch (Exception e) {
if (firstError == null) {
firstError = e;
} else {
log.warn("Additional drop batch failed on {} (suppressed in favor of first error)", tableName, e);
}
}
}
if (firstError != null) {
log.error("Drop partition dispatch on {} aborted after first failure ({} batches cancelled)",
tableName, cancelled);
throw firstError;
}
}

private void applyDropBatch(IMetaStoreClient poolClient, String tableName, List<String> batch) throws Exception {
int dropped = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: the parameter is named poolClient but the sequential code path calls this same method with the plain session metaStoreClient — not a pool client. Something like client would read correctly in both call sites.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

for (String dropPartition : batch) {
if (HivePartitionUtil.partitionExists(poolClient, tableName, dropPartition,
partitionValueExtractor, config)) {
String partitionClause =
HivePartitionUtil.getPartitionClauseForDrop(dropPartition, partitionValueExtractor, config);
poolClient.dropPartition(databaseName, tableName, partitionClause, false);
dropped++;
}
// Per-partition detail stays at debug: a batch can hold thousands of partitions
// and N workers log concurrently, so INFO carries the per-batch summary instead.
log.debug("Dropped partition {} on {}", dropPartition, tableName);
}
log.info("Dropped {} of {} partitions in batch on {}", dropped, batch.size(), tableName);
}

@Override
public void close() {
// Close the pool first so the worker threads stop dispatching against their
Expand Down
Loading
Loading