nsivabalan commented on code in PR #19033:
URL: https://github.com/apache/hudi/pull/19033#discussion_r3763123193


##########
hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/util/HiveMetaStoreClientPool.java:
##########
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.hive.util;
+
+import org.apache.hudi.exception.HoodieException;
+import org.apache.hudi.hive.HiveSyncConfig;
+
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.metastore.IMetaStoreClient;
+import org.apache.hadoop.hive.metastore.RetryingMetaStoreClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Pool of {@link IMetaStoreClient} instances for parallel partition sync.
+ *
+ * <p>Each pooled client wraps an independent Thrift connection to the Hive 
Metastore.
+ * Callers borrow a client via {@link #run(ClientAction)}, which blocks until 
a client
+ * is available, executes the action, and returns the client to the pool. A 
worker
+ * thread pool of the same size is exposed via {@link #executor()} so callers 
can fan
+ * out their batches to match the number of available clients.
+ *
+ * <p><b>Usage contract:</b> pool clients must be used <i>only</i> for 
partition-row
+ * operations — {@code add_partitions}, {@code alter_partitions}, {@code 
dropPartition},
+ * {@code getPartition}. Table-row operations ({@code createTable}, {@code 
alter_table},
+ * {@code getTable} used as the read half of a read-modify-write of table 
parameters)
+ * must continue to go through the session client held by
+ * {@code HoodieHiveSyncClient.client} on the sync driver thread. Mixing the 
two would
+ * risk lost updates on table parameters such as the last-commit-time-synced 
marker.
+ *
+ * <p>The pool is gated behind {@code 
hoodie.datasource.hive_sync.batching.enabled} and
+ * is constructed for sync mode HIVEQL, where it backs the DROP path only. 
DROP goes
+ * through {@code IMetaStoreClient.dropPartition} (Thrift), whereas 
ADD/UPDATE/TOUCH go
+ * through the thread-bound Hive {@code Driver} and use {@code HiveDriverPool} 
instead.
+ */
+public class HiveMetaStoreClientPool implements AutoCloseable {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(HiveMetaStoreClientPool.class);
+
+  private final ArrayBlockingQueue<IMetaStoreClient> available;
+  private final List<IMetaStoreClient> all;
+  private final ExecutorService executor;
+  private final int size;
+  private volatile boolean closed;
+
+  public HiveMetaStoreClientPool(HiveSyncConfig config, int size) {
+    this(buildClients(config, size), size);
+  }
+
+  // Package-private for tests: accepts a pre-built list of clients so we can
+  // exercise borrow/return/close semantics without a live metastore.
+  HiveMetaStoreClientPool(List<IMetaStoreClient> clients, int size) {
+    if (size < 1) {
+      throw new IllegalArgumentException("Pool size must be >= 1, got " + 
size);
+    }
+    if (clients.size() != size) {
+      throw new IllegalArgumentException("Expected " + size + " clients, got " 
+ clients.size());
+    }
+    this.size = size;
+    this.available = new ArrayBlockingQueue<>(size);
+    this.all = new ArrayList<>(clients);
+    this.available.addAll(clients);
+    this.executor = Executors.newFixedThreadPool(size, new 
PoolThreadFactory());
+    LOG.info("Initialized IMetaStoreClient pool with {} clients", size);
+  }
+
+  private static List<IMetaStoreClient> buildClients(HiveSyncConfig config, 
int size) {
+    if (size < 1) {
+      throw new IllegalArgumentException("Pool size must be >= 1, got " + 
size);
+    }
+    HiveConf hiveConf = config.getHiveConf();
+    List<IMetaStoreClient> clients = new ArrayList<>(size);
+    try {
+      for (int i = 0; i < size; i++) {
+        clients.add(newClient(hiveConf));
+      }
+      return clients;
+    } catch (Exception e) {
+      // Construction failed mid-way; close any clients we already built before
+      // surfacing the error so we don't leak Thrift sockets.
+      for (IMetaStoreClient c : clients) {
+        try {
+          c.close();
+        } catch (Exception ignore) {
+          // intentional: best-effort cleanup during failure
+        }
+      }
+      throw new HoodieException("Failed to construct IMetaStoreClient pool of 
size " + size, e);
+    }
+  }
+
+  private static IMetaStoreClient newClient(HiveConf hiveConf) {
+    try {
+      // RetryingMetaStoreClient.getProxy returns an independent 
IMetaStoreClient
+      // (one Thrift socket per call), bypassing the Hive thread-local cache 
that
+      // Hive.get(conf) would use. This is what gives us N truly independent 
clients.
+      return RetryingMetaStoreClient.getProxy(hiveConf, true);
+    } catch (Exception e) {
+      throw new HoodieException("Failed to create IMetaStoreClient for pool", 
e);
+    }
+  }
+
+  /**
+   * Borrows a client, runs the action, and returns the client to the pool.
+   * Blocks if all clients are in use until one becomes available.
+   */
+  public <T> T run(ClientAction<T> action) throws Exception {
+    if (closed) {
+      throw new IllegalStateException("Cannot borrow from a closed 
IMetaStoreClient pool");
+    }
+    IMetaStoreClient client = available.take();
+    try {
+      return action.apply(client);
+    } finally {
+      // Always return the client to the pool, even on failure. Thrift clients
+      // recover transparently from transactional errors at the HMS layer;
+      // RetryingMetaStoreClient handles transient socket failures internally.
+      if (!closed) {
+        available.offer(client);
+      }
+    }
+  }
+
+  /**
+   * Submits one task per item, each borrowing a pooled client for the 
duration of its
+   * call, and returns a handle to the in-flight batch — this method does not 
block. The
+   * caller awaits completion via {@link #awaitAll(ParallelDispatch, String)}.
+   *
+   * <p>Tasks observe a shared abort flag, so a failure on any item stops 
items that have
+   * not started yet even while a slower sibling is still mid-call. See
+   * {@link ParallelDispatch} for why waiting on futures alone does not 
achieve that.
+   */
+  public <T> ParallelDispatch dispatchAll(List<T> items, ClientConsumer<T> 
action) {
+    if (closed) {
+      throw new IllegalStateException("Cannot dispatch to a closed 
IMetaStoreClient pool");
+    }
+    ParallelDispatch dispatch = new ParallelDispatch(items.size());
+    for (T item : items) {
+      dispatch.add(executor.submit(dispatch.guard(() -> {
+        run(client -> {
+          action.accept(client, item);
+          return null;
+        });
+        return null;
+      }, "Skipped after an earlier batch failed")));
+    }
+    dispatch.sealed();
+    return dispatch;
+  }
+
+  /**
+   * Awaits a batch from {@link #dispatchAll(List, ClientConsumer)}, cancels 
whatever had
+   * not started, and rethrows the first real failure. Later failures are 
logged at WARN.
+   */
+  public void awaitAll(ParallelDispatch dispatch, String description) throws 
Exception {
+    long start = System.currentTimeMillis();
+    ParallelDispatch.Outcome outcome = dispatch.awaitOutcome();
+    outcome.suppressed().forEach(e ->
+        LOG.warn("Additional {} batch failed (suppressed in favor of first 
error)", description, e));

Review Comment:
   Confirmed — no callers anywhere, removed in a9d9443.
   
   The class javadoc was also still pointing at `executor()` as the fan-out 
mechanism, which stopped being true when `dispatchAll` took that over; updated 
it to describe the internal worker pool instead. Public surface is now borrow / 
dispatch / await / close.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to