This is an automated email from the ASF dual-hosted git repository.

zhztheplayer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git


The following commit(s) were added to refs/heads/main by this push:
     new fb23d83fa2 [GLUTEN-11895][VL] Fix SIGSEGV on IOThreadPool threads 
during HDFS scan (#11896)
fb23d83fa2 is described below

commit fb23d83fa25c4dd1c445a1d3712b5a2fa3301838
Author: Guo Wangyang <[email protected]>
AuthorDate: Tue Apr 21 16:17:44 2026 +0800

    [GLUTEN-11895][VL] Fix SIGSEGV on IOThreadPool threads during HDFS scan 
(#11896)
---
 cpp/core/jni/JniCommon.cc                     |  8 +++-
 cpp/core/jni/JniCommon.h                      | 60 ++++++++++++++++++++++++++-
 cpp/core/jni/JniWrapper.cc                    |  6 ++-
 cpp/velox/compute/WholeStageResultIterator.cc |  9 +++-
 4 files changed, 78 insertions(+), 5 deletions(-)

diff --git a/cpp/core/jni/JniCommon.cc b/cpp/core/jni/JniCommon.cc
index 9d435bb592..3c17e1085b 100644
--- a/cpp/core/jni/JniCommon.cc
+++ b/cpp/core/jni/JniCommon.cc
@@ -26,7 +26,7 @@ void gluten::JniCommonState::ensureInitialized(JNIEnv* env) {
   initialized_ = true;
 }
 
-void gluten::JniCommonState::assertInitialized() {
+void gluten::JniCommonState::assertInitialized() const {
   if (!initialized_) {
     throw gluten::GlutenException("Fatal: JniCommonState::Initialize(...) was 
not called before using the utility");
   }
@@ -95,7 +95,11 @@ 
gluten::JniColumnarBatchIterator::~JniColumnarBatchIterator() {
   attachCurrentThreadAsDaemonOrThrow(vm_, &env);
   env->DeleteGlobalRef(jColumnarBatchItr_);
   env->DeleteGlobalRef(serializedColumnarBatchIteratorClass_);
-  vm_->DetachCurrentThread();
+  // Do NOT call DetachCurrentThread() here.
+  // libhdfs.so caches JNIEnv* in thread-local storage after 
AttachCurrentThread.
+  // If we detach, libhdfs's TLS cache becomes stale — the next HDFS call via
+  // libhdfs returns the stale env, causing SIGSEGV in jni_NewStringUTF.
+  // Daemon-attached threads are safe to leave attached; they won't block JVM 
shutdown.
 }
 
 std::shared_ptr<gluten::ColumnarBatch> 
gluten::JniColumnarBatchIterator::next() {
diff --git a/cpp/core/jni/JniCommon.h b/cpp/core/jni/JniCommon.h
index 783f8edfcc..ee16412e3a 100644
--- a/cpp/core/jni/JniCommon.h
+++ b/cpp/core/jni/JniCommon.h
@@ -21,6 +21,9 @@
 #include <arrow/ipc/writer.h>
 #include <execinfo.h>
 #include <jni.h>
+#include <thread>
+
+#include <folly/executors/thread_factory/ThreadFactory.h>
 
 #include "compute/ProtobufUtils.h"
 #include "compute/Runtime.h"
@@ -156,12 +159,17 @@ class JniCommonState {
 
   void ensureInitialized(JNIEnv* env);
 
-  void assertInitialized();
+  void assertInitialized() const;
 
   void close();
 
   jmethodID runtimeAwareCtxHandle();
 
+  JavaVM* vm() const {
+    assertInitialized();
+    return vm_;
+  }
+
  private:
   void initialize(JNIEnv* env);
 
@@ -179,6 +187,56 @@ inline JniCommonState* getJniCommonState() {
   return &jniCommonState;
 }
 
+/// A folly::ThreadFactory for spill thread pools. Attaches each thread to the
+/// JVM as a daemon at creation and calls DetachCurrentThread inside the thread
+/// function body — after all work completes but before any pthread_key 
destructor
+/// fires — to prevent unbounded JavaThread accumulation.
+///
+/// INVARIANT: threads created by this factory must never call libhdfs. libhdfs
+/// registers hdfsThreadDestructor via pthread_key on first HDFS call; that
+/// destructor calls DetachCurrentThread at actual thread exit. Calling it
+/// earlier (inside the thread body) would invalidate libhdfs's cached JNIEnv*,
+/// causing SIGSEGV on the next HDFS call.
+///
+/// REQUIRES: JniCommonState::ensureInitialized() must have been called before
+/// constructing this factory (i.e. after JNI_OnLoad completes).
+class JniAwareThreadFactory : public folly::ThreadFactory {
+ public:
+  JniAwareThreadFactory() : vm_(getJniCommonState()->vm()) {}
+
+  std::thread newThread(folly::Func&& func) override {
+    return std::thread([vm = vm_, f = std::move(func)]() mutable {
+      JNIEnv* env = nullptr;
+      bool weAttached = (vm->GetEnv(reinterpret_cast<void**>(&env), 
jniVersion) == JNI_EDETACHED);
+      if (weAttached) {
+        if (vm->AttachCurrentThreadAsDaemon(reinterpret_cast<void**>(&env), 
nullptr) != JNI_OK) {
+          LOG(WARNING) << "JniAwareThreadFactory: failed to attach thread to 
JVM";
+          weAttached = false;
+        }
+      }
+      // RAII guard: ensures DetachCurrentThread is called even if f() throws.
+      struct DetachGuard {
+        JavaVM* vm;
+        bool active;
+        ~DetachGuard() {
+          if (active) {
+            vm->DetachCurrentThread();
+          }
+        }
+      } guard{vm, weAttached};
+      f();
+    });
+  }
+
+  const std::string& getNamePrefix() const override {
+    static const std::string kEmpty;
+    return kEmpty;
+  }
+
+ private:
+  JavaVM* vm_;
+};
+
 Runtime* getRuntime(JNIEnv* env, jobject runtimeAware);
 
 // Safe version of JNI {Get|Release}<PrimitiveType>ArrayElements routines.
diff --git a/cpp/core/jni/JniWrapper.cc b/cpp/core/jni/JniWrapper.cc
index a08e55b741..9e194be6ea 100644
--- a/cpp/core/jni/JniWrapper.cc
+++ b/cpp/core/jni/JniWrapper.cc
@@ -103,7 +103,11 @@ class JavaInputStreamAdaptor final : public 
arrow::io::InputStream {
     env->CallVoidMethod(jniIn_, jniByteInputStreamClose);
     checkException(env);
     env->DeleteGlobalRef(jniIn_);
-    vm_->DetachCurrentThread();
+    // Do NOT call DetachCurrentThread() here.
+    // libhdfs.so caches JNIEnv* in thread-local storage after 
AttachCurrentThread.
+    // If we detach, libhdfs's TLS cache becomes stale — the next HDFS call via
+    // libhdfs returns the stale env, causing SIGSEGV in jni_NewStringUTF.
+    // Daemon-attached threads are safe to leave attached; they won't block 
JVM shutdown.
     closed_ = true;
     return arrow::Status::OK();
   }
diff --git a/cpp/velox/compute/WholeStageResultIterator.cc 
b/cpp/velox/compute/WholeStageResultIterator.cc
index dafca3fd51..356913b5f7 100644
--- a/cpp/velox/compute/WholeStageResultIterator.cc
+++ b/cpp/velox/compute/WholeStageResultIterator.cc
@@ -19,6 +19,7 @@
 #include "VeloxPlanConverter.h"
 #include "VeloxRuntime.h"
 #include "config/VeloxConfig.h"
+#include "jni/JniCommon.h"
 #include "utils/ConfigExtractor.h"
 #include "velox/connectors/hive/HiveConfig.h"
 #include "velox/connectors/hive/HiveConnectorSplit.h"
@@ -91,7 +92,13 @@ WholeStageResultIterator::WholeStageResultIterator(
   spillStrategy_ = veloxCfg_->get<std::string>(kSpillStrategy, 
kSpillStrategyDefaultValue);
   auto spillThreadNum = veloxCfg_->get<uint32_t>(kSpillThreadNum, 
kSpillThreadNumDefaultValue);
   if (spillThreadNum > 0) {
-    spillExecutor_ = 
std::make_shared<folly::CPUThreadPoolExecutor>(spillThreadNum);
+    // INVARIANT: spillExecutor_ threads must never call libhdfs.
+    // JniAwareThreadFactory calls DetachCurrentThread at thread exit (inside 
the
+    // thread fn body, before any pthread_key destructor). If libhdfs were 
used on
+    // these threads, hdfsThreadDestructor would fire afterward with a stale 
JNIEnv*,
+    // causing SIGSEGV. Spill always uses local or heap-over-local filesystem.
+    spillExecutor_ = std::make_shared<folly::CPUThreadPoolExecutor>(
+        spillThreadNum, std::make_shared<gluten::JniAwareThreadFactory>());
   }
   getOrderedNodeIds(veloxPlan_, orderedNodeIds_);
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to