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


##########
hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/util/HiveDriverPool.java:
##########
@@ -0,0 +1,342 @@
+/*
+ * 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.hudi.hive.HoodieHiveSyncException;
+
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.ql.Driver;
+import org.apache.hadoop.hive.ql.session.SessionState;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static 
org.apache.hudi.sync.common.HoodieSyncConfig.META_SYNC_DATABASE_NAME;
+
+/**
+ * Pool of Hive {@link Driver} + {@link SessionState} pairs for parallel 
HiveQL DDL.
+ *
+ * <p>Hive's {@code SessionState.start(state)} binds state to the calling 
thread's
+ * thread-local, and {@code Driver} reads from that thread-local during {@code 
run()}.
+ * A Driver constructed on one thread cannot be safely used from another. This 
pool
+ * solves that by giving each slot its own dedicated worker thread (a 
single-thread
+ * executor) — the Driver and SessionState are built on that thread by a 
bootstrap
+ * task, and all subsequent SQL for that slot runs on the same thread.
+ *
+ * <p><b>Usage contract:</b> use this pool only for partition-row DDL 
statements that
+ * are independent of each other and freely shuffleable across workers. 
Table-level
+ * statements (createTable, schema evolution, USE database) must continue to 
run on
+ * the session {@code Driver} held by {@code HiveQueryDDLExecutor} on the sync 
driver
+ * thread. The pool is gated behind {@code 
hoodie.datasource.hive_sync.batching.enabled}
+ * and is constructed only for HiveQL sync mode.
+ */
+public class HiveDriverPool implements AutoCloseable {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(HiveDriverPool.class);
+
+  // Per-worker Driver construction has to be fast in practice (a few hundred 
ms
+  // for the SessionState + Driver init). A 60s ceiling per worker leaves 
plenty of
+  // headroom for a slow JVM warm-up but bounds the failure mode if the 
metastore
+  // is unreachable or Hive hangs during init.
+  private static final long BOOTSTRAP_TIMEOUT_SECONDS = 60;
+
+  private final List<Worker> workers;
+  private final int size;
+  private volatile boolean closed;
+
+  public HiveDriverPool(HiveSyncConfig config, int size) {
+    this(config, size, new DefaultDriverFactory(config));
+  }
+
+  // Package-private for tests: accepts a DriverFactory so unit tests can 
inject
+  // mock Driver instances without standing up a real Hive instance.
+  HiveDriverPool(HiveSyncConfig config, int size, DriverFactory factory) {
+    if (size < 1) {
+      throw new IllegalArgumentException("Pool size must be >= 1, got " + 
size);
+    }
+    this.size = size;
+    this.workers = new ArrayList<>(size);
+    String databaseName = config.getStringOrDefault(META_SYNC_DATABASE_NAME);
+    PoolThreadFactory threadFactory = new PoolThreadFactory();
+    try {
+      // Bootstrap workers one at a time (not concurrently): each worker 
builds its
+      // own exclusively-owned SessionState, and constructing several 
SessionStates
+      // in parallel risks racing on shared scratch-dir creation. This only 
affects
+      // one-time pool startup cost, not per-statement dispatch latency.
+      for (int i = 0; i < size; i++) {
+        Worker worker = new Worker(threadFactory);
+        workers.add(worker);
+        worker.executor.submit(() -> {
+          worker.driver = factory.newDriver(databaseName);
+          worker.sessionState = SessionState.get();
+          return null;
+        }).get(BOOTSTRAP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+      }
+    } catch (Exception e) {
+      tearDown();
+      throw new HoodieException("Failed to construct HiveDriverPool of size " 
+ size, e);
+    }
+    LOG.info("Initialized HiveDriverPool with {} workers", size);
+  }
+
+  /**
+   * Runs each given SQL on <i>every</i> worker, in order. Used for setup 
statements
+   * (e.g. {@code USE database}) that must establish per-thread session context
+   * before any partition statement runs. Blocks until all workers have 
completed
+   * the setup. Throws on first error.
+   */
+  public void runOnEachWorker(List<String> setupSqls) {
+    if (closed) {
+      throw new IllegalStateException("Cannot dispatch to a closed 
HiveDriverPool");
+    }
+    if (setupSqls.isEmpty()) {
+      return;
+    }
+    List<Future<?>> futures = new ArrayList<>(workers.size());
+    for (Worker worker : workers) {
+      futures.add(worker.executor.submit(() -> {
+        for (String sql : setupSqls) {
+          worker.driver.run(sql);
+        }
+        return null;
+      }));
+    }
+    awaitAll(futures);
+  }
+
+  /**
+   * Dispatches each SQL string to a worker (round-robin) and returns the list 
of
+   * in-flight futures — this method does not block. The caller is responsible 
for
+   * awaiting completion via {@link #awaitAll(List)} and collecting errors. 
SQL text
+   * is intentionally not logged per-statement here: batched TOUCH/ADD 
statements can
+   * be many kilobytes, and N parallel workers would multiply the log volume. 
See
+   * {@link #awaitAll(List)} for the per-call summary log.
+   */
+  public List<Future<?>> dispatchAll(List<String> sqls) {
+    if (closed) {
+      throw new IllegalStateException("Cannot dispatch to a closed 
HiveDriverPool");
+    }
+    List<Future<?>> futures = new ArrayList<>(sqls.size());
+    for (int i = 0; i < sqls.size(); i++) {
+      String sql = sqls.get(i);
+      Worker worker = workers.get(i % workers.size());
+      futures.add(worker.executor.submit(() -> {
+        worker.driver.run(sql);
+        return null;
+      }));
+    }
+    return futures;
+  }
+
+  /**
+   * Awaits all futures and throws the first exception encountered. On first 
failure,
+   * cancels the remaining (not yet started) futures so workers don't keep 
running
+   * pointless work after a fatal error. Any errors that finished before 
cancellation
+   * are logged at WARN. Callers do not need per-statement results (Hive's 
Driver.run
+   * side-effects the metastore), so this method is void.
+   */
+  public void awaitAll(List<Future<?>> futures) {
+    long start = System.currentTimeMillis();
+    Exception firstError = null;
+    int completed = 0;
+    int cancelled = 0;
+    for (int i = 0; i < futures.size(); i++) {
+      Future<?> f = futures.get(i);
+      try {
+        f.get();

Review Comment:
   Good catch, and the diagnosis is exactly right. Fixed in `84691ce`.
   
   The futures returned by `dispatchAll` belong to N independent single-thread 
executors, each draining its own queue, so blocking on `Future.get()` in 
submission order meant a failure on a fast worker went unobserved while the 
awaiting thread was parked on a slow worker's earlier future — and that failed 
worker kept pulling more partition DDL off its own queue.
   
   `dispatchAll` now returns a `Dispatch` handle carrying a shared abort flag. 
Each task checks it on entry and bails with `CancellationException` without 
touching its Driver; the first task to fail sets it. `awaitAll` blocks on a 
latch that trips on either all-settled or first-abort, then sweeps 
`cancel(false)` before walking the futures.
   
   Worth noting why the in-task check is load-bearing rather than just 
consuming in completion order: cancelling from the awaiting thread is 
inherently late, since a worker can dequeue its next statement at any moment. 
The entry check is what actually bounds how much extra DDL a failed sync can 
apply. `mayInterruptIfRunning=false` is preserved, so in-flight statements 
still run to completion rather than leaving a Driver mid-statement.
   
   Regression test added — `awaitAllStopsLaterWorkerWhenEarlierFutureIsSlow` 
pins the interleaving you described (two workers; `SLOW` on worker 0, `FAIL` 
then `AFTER_FAIL` on worker 1, with `SLOW` released only after the abort) and 
asserts `AFTER_FAIL` never reaches a Driver. I also rewrote 
`awaitAllCancelsPendingFuturesOnFirstError` — its old `isCancelled() || 
!isDone()` assertion was hedging around this same race, and the abort flag 
makes the outcome deterministic.
   
   Verified both fail against the prior logic on all four surefire attempts and 
pass against the fix.



-- 
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