-
Notifications
You must be signed in to change notification settings - Fork 2.5k
feat(hive-sync): parallelize DROP partitions in HiveQL sync mode #19033
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
02c9611
3225807
8712218
7610c51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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(). | ||
|
|
@@ -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); | ||
|
|
@@ -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(); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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 | ||
| // 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)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Update the public batching config contract for DROP
|
||
| } | ||
|
|
||
| private Option<HiveDriverPool> maybeBuildHiveDriverPool(HiveSyncConfig config) { | ||
| if (!config.getBooleanOrDefault(HIVE_SYNC_BATCHING_ENABLED)) { | ||
| return Option.empty(); | ||
|
|
@@ -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 { | ||
|
|
@@ -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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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; | ||
|
|
@@ -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()); | ||
|
|
@@ -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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 nit: the parallel-path error-collection loop inside - AI-generated; verify before applying. React 👍/👎 to flag quality.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| } 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 nit: the parameter is named |
||
| 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 | ||
|
|
||
There was a problem hiding this comment.
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"?