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

lxy-9602 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new 5cc51a7c fix(core): resolve ScanContextBuilder executor lazily in 
Finish() (#282)
5cc51a7c is described below

commit 5cc51a7c1466b259b8c37d4f250d321458500f7d
Author: wangyong9999 <[email protected]>
AuthorDate: Mon Sep 7 19:11:00 2026 +0800

    fix(core): resolve ScanContextBuilder executor lazily in Finish() (#282)
---
 .../common/executor/default_executor_test.cpp      | 87 ++++++++++++++++++++++
 src/paimon/common/executor/executor.cpp            | 16 +++-
 src/paimon/core/operation/scan_context.cpp         | 12 +--
 src/paimon/core/operation/scan_context_test.cpp    | 25 +++++++
 4 files changed, 131 insertions(+), 9 deletions(-)

diff --git a/src/paimon/common/executor/default_executor_test.cpp 
b/src/paimon/common/executor/default_executor_test.cpp
index 07e78681..80775f2f 100644
--- a/src/paimon/common/executor/default_executor_test.cpp
+++ b/src/paimon/common/executor/default_executor_test.cpp
@@ -26,6 +26,10 @@
 #include <thread>
 #include <vector>
 
+#ifdef __linux__
+#include <dirent.h>
+#endif
+
 #include "gtest/gtest.h"
 #include "paimon/common/executor/future.h"
 #include "paimon/executor.h"
@@ -35,6 +39,89 @@
 
 namespace paimon::test {
 
+#ifdef __linux__
+// Number of threads of the current process according to /proc.
+int32_t CountProcessThreads() {
+    DIR* dir = opendir("/proc/self/task");
+    if (dir == nullptr) {
+        return -1;
+    }
+    int32_t count = 0;
+    while (struct dirent* entry = readdir(dir)) {
+        if (entry->d_name[0] != '.') {
+            ++count;
+        }
+    }
+    closedir(dir);
+    return count;
+}
+
+// A worker joined by an earlier test can trail in /proc for a moment, so take
+// the count only once two consecutive reads agree.
+int32_t StableProcessThreadCount() {
+    int32_t last = CountProcessThreads();
+    for (int32_t i = 0; i < 100; ++i) {
+        std::this_thread::sleep_for(std::chrono::milliseconds(10));
+        int32_t current = CountProcessThreads();
+        if (current == last) {
+            return current;
+        }
+        last = current;
+    }
+    return last;
+}
+#endif
+
+TEST(DefaultExecutorTest, TestWorkersStartOnFirstTask) {
+#ifdef __linux__
+    const int32_t threads_before = StableProcessThreadCount();
+    ASSERT_GT(threads_before, 0);
+#endif
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<Executor> executor, 
CreateDefaultExecutor(4));
+    ASSERT_EQ(4u, executor->GetThreadNum());
+#ifdef __linux__
+    // Constructing the executor does not spawn any worker thread.
+    ASSERT_LE(CountProcessThreads(), threads_before);
+#endif
+
+    std::atomic<int64_t> sum = {0};
+    std::vector<std::future<void>> futures;
+    for (int32_t index = 0; index < 8; ++index) {
+        futures.push_back(Via(executor.get(), [&sum]() { sum++; }));
+    }
+    Wait(futures);
+    ASSERT_EQ(8, sum.load());
+#ifdef __linux__
+    // The first task started all four workers.
+    ASSERT_GE(CountProcessThreads(), threads_before + 4);
+#endif
+    executor.reset();
+#ifdef __linux__
+    // Destroying the executor joined them; the joined threads may trail in
+    // /proc for a moment, so poll briefly.
+    int32_t threads_after = CountProcessThreads();
+    for (int32_t i = 0; i < 100 && threads_after > threads_before; ++i) {
+        std::this_thread::sleep_for(std::chrono::milliseconds(10));
+        threads_after = CountProcessThreads();
+    }
+    ASSERT_LE(threads_after, threads_before);
+#endif
+}
+
+TEST(DefaultExecutorTest, TestShutdownWithoutTasks) {
+    // Shutting down or destroying an executor that never ran a task must not
+    // block or touch workers that were never started.
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<Executor> executor, 
CreateDefaultExecutor(2));
+    executor->ShutdownNow();
+    std::atomic<bool> ran = {false};
+    executor->Add([&ran]() { ran = true; });
+    std::this_thread::sleep_for(std::chrono::milliseconds(50));
+    ASSERT_FALSE(ran.load());
+    executor.reset();
+    std::unique_ptr<Executor> idle_executor = CreateDefaultExecutor();
+    idle_executor.reset();
+}
+
 TEST(DefaultExecutorTest, TestViaVoidFunc) {
     auto executor = GetGlobalDefaultExecutor();
     std::atomic<int64_t> sum = {0};
diff --git a/src/paimon/common/executor/executor.cpp 
b/src/paimon/common/executor/executor.cpp
index 45253fe1..92aef4a0 100644
--- a/src/paimon/common/executor/executor.cpp
+++ b/src/paimon/common/executor/executor.cpp
@@ -52,15 +52,15 @@ class DefaultExecutor : public Executor {
 
  private:
     uint32_t thread_count_;
+    // Guarded by state_->mutex; populated on the first Add().
     std::vector<std::thread> workers_;
     std::shared_ptr<State> state_ = std::make_shared<State>();
 };
 
 DefaultExecutor::DefaultExecutor(uint32_t thread_count) : 
thread_count_(thread_count) {
     assert(thread_count > 0);
-    for (uint32_t i = 0; i < thread_count_; ++i) {
-        workers_.emplace_back(&DefaultExecutor::WorkerThread, state_);
-    }
+    // Worker threads are started lazily by the first Add(): an executor that
+    // never receives a task never spawns a thread.
 }
 
 uint32_t DefaultExecutor::GetThreadNum() const {
@@ -68,6 +68,7 @@ uint32_t DefaultExecutor::GetThreadNum() const {
 }
 
 void DefaultExecutor::ShutdownInternal(bool wait_for_pending_tasks) {
+    std::vector<std::thread> workers;
     {
         std::unique_lock<std::mutex> lock(state_->mutex);
         if (state_->stop) {
@@ -80,8 +81,9 @@ void DefaultExecutor::ShutdownInternal(bool 
wait_for_pending_tasks) {
             state_->tasks.swap(empty);
         }
         state_->condition.notify_all();
+        workers.swap(workers_);
     }
-    for (std::thread& worker : workers_) {
+    for (std::thread& worker : workers) {
         if (worker.joinable()) {
             if (worker.get_id() == std::this_thread::get_id()) {
                 worker.detach();
@@ -112,6 +114,12 @@ void DefaultExecutor::Add(std::function<void()> func) {
             return;
         }
         state_->tasks.emplace(std::move(func));
+        if (workers_.empty()) {
+            workers_.reserve(thread_count_);
+            for (uint32_t i = 0; i < thread_count_; ++i) {
+                workers_.emplace_back(&DefaultExecutor::WorkerThread, state_);
+            }
+        }
     }
     state_->condition.notify_one();
 }
diff --git a/src/paimon/core/operation/scan_context.cpp 
b/src/paimon/core/operation/scan_context.cpp
index 16a80731..0b2ab855 100644
--- a/src/paimon/core/operation/scan_context.cpp
+++ b/src/paimon/core/operation/scan_context.cpp
@@ -67,7 +67,7 @@ class ScanContextBuilder::Impl {
         global_index_result_.reset();
         realtime_context_.reset();
         memory_pool_ = GetDefaultPool();
-        executor_ = CreateDefaultExecutor();
+        executor_.reset();
         specific_file_system_.reset();
         table_schema_ = std::nullopt;
         options_.clear();
@@ -84,7 +84,8 @@ class ScanContextBuilder::Impl {
     std::shared_ptr<GlobalIndexResult> global_index_result_;
     std::shared_ptr<RealtimeContext> realtime_context_;
     std::shared_ptr<MemoryPool> memory_pool_ = GetDefaultPool();
-    std::shared_ptr<Executor> executor_ = CreateDefaultExecutor();
+    // Resolved in Finish(); a builder never owns an executor of its own.
+    std::shared_ptr<Executor> executor_;
     std::shared_ptr<FileSystem> specific_file_system_;
     std::optional<std::string> table_schema_;
     std::map<std::string, std::string> options_;
@@ -178,13 +179,14 @@ Result<std::unique_ptr<ScanContext>> 
ScanContextBuilder::Finish() {
     if (impl_->path_.empty()) {
         return Status::Invalid("cannot scan with empty table path");
     }
+    std::shared_ptr<Executor> executor =
+        impl_->executor_ ? impl_->executor_ : CreateDefaultExecutor();
     auto ctx = std::make_unique<ScanContext>(
         impl_->path_, impl_->is_streaming_mode_, impl_->limit_,
         std::make_shared<ScanFilter>(impl_->predicates_, 
impl_->partition_filters_,
                                      impl_->bucket_filter_),
-        impl_->global_index_result_, impl_->realtime_context_, 
impl_->memory_pool_,
-        impl_->executor_, impl_->specific_file_system_, impl_->table_schema_, 
impl_->options_,
-        impl_->cache_);
+        impl_->global_index_result_, impl_->realtime_context_, 
impl_->memory_pool_, executor,
+        impl_->specific_file_system_, impl_->table_schema_, impl_->options_, 
impl_->cache_);
     impl_->Reset();
     return ctx;
 }
diff --git a/src/paimon/core/operation/scan_context_test.cpp 
b/src/paimon/core/operation/scan_context_test.cpp
index 6c86c049..3b965889 100644
--- a/src/paimon/core/operation/scan_context_test.cpp
+++ b/src/paimon/core/operation/scan_context_test.cpp
@@ -101,4 +101,29 @@ TEST(ScanContextTest, TestSetOptionsOverridesAddedOptions) 
{
     ASSERT_EQ(expected_options, ctx->GetOptions());
 }
 
+TEST(ScanContextTest, TestDefaultExecutorIsCreatedPerContext) {
+    // A builder without WithExecutor() gives every context a default executor
+    // of its own; nothing is shared across contexts.
+    ScanContextBuilder first_builder("table_root_path");
+    ASSERT_OK_AND_ASSIGN(auto first_ctx, first_builder.Finish());
+    ScanContextBuilder second_builder("table_root_path");
+    ASSERT_OK_AND_ASSIGN(auto second_ctx, second_builder.Finish());
+    ASSERT_TRUE(first_ctx->GetExecutor());
+    ASSERT_TRUE(second_ctx->GetExecutor());
+    ASSERT_NE(first_ctx->GetExecutor(), second_ctx->GetExecutor());
+    // Neither falls back to the process wide singleton.
+    ASSERT_NE(GetGlobalDefaultExecutor(), first_ctx->GetExecutor());
+    ASSERT_NE(GetGlobalDefaultExecutor(), second_ctx->GetExecutor());
+
+    // Finish() resets the builder; an explicit executor set before does not
+    // leak into the next context built from the same builder.
+    std::shared_ptr<Executor> executor = CreateDefaultExecutor();
+    first_builder.WithExecutor(executor);
+    ASSERT_OK_AND_ASSIGN(auto explicit_ctx, first_builder.Finish());
+    ASSERT_EQ(executor, explicit_ctx->GetExecutor());
+    ASSERT_OK_AND_ASSIGN(auto reset_ctx, first_builder.Finish());
+    ASSERT_TRUE(reset_ctx->GetExecutor());
+    ASSERT_NE(executor, reset_ctx->GetExecutor());
+}
+
 }  // namespace paimon::test

Reply via email to