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 46cc9d63 fix(executor): do not retain callbacks when worker startup
fails (#437)
46cc9d63 is described below
commit 46cc9d6311efe8a26921ee142ba65920168eefd5
Author: Aaron Wu <[email protected]>
AuthorDate: Mon Oct 19 11:42:18 2026 +0800
fix(executor): do not retain callbacks when worker startup fails (#437)
---
src/paimon/CMakeLists.txt | 15 ++
src/paimon/common/executor/executor.cpp | 3 +-
.../common/executor/executor_submission_test.cpp | 187 +++++++++++++++++++++
3 files changed, 204 insertions(+), 1 deletion(-)
diff --git a/src/paimon/CMakeLists.txt b/src/paimon/CMakeLists.txt
index 489aaced..0cb9ccbe 100644
--- a/src/paimon/CMakeLists.txt
+++ b/src/paimon/CMakeLists.txt
@@ -542,6 +542,21 @@ add_subdirectory(common/file_index)
add_subdirectory(common/global_index)
if(PAIMON_BUILD_TESTS)
+ # Wrap actual executor allocations, not calls inside an already linked DSO.
+ if(PAIMON_BUILD_STATIC
+ AND CMAKE_SYSTEM_NAME STREQUAL "Linux"
+ AND CMAKE_SIZEOF_VOID_P EQUAL 8
+ AND CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang)$")
+ add_paimon_test(executor_submission_test
+ SOURCES
+ common/executor/executor_submission_test.cpp
+ STATIC_LINK_LIBS
+ paimon_static
+ ${GTEST_LINK_TOOLCHAIN})
+ target_link_options(paimon-executor-submission-test PRIVATE
"LINKER:--wrap=_Znwm")
+ set_tests_properties(paimon-executor-submission-test PROPERTIES
TIMEOUT 30)
+ endif()
+
# The shared HTTP client is compiled only for the components that need
libcurl,
# so its test follows the same gate.
set(PAIMON_COMMON_HTTP_CLIENT_TEST_SRCS)
diff --git a/src/paimon/common/executor/executor.cpp
b/src/paimon/common/executor/executor.cpp
index 92aef4a0..11c28039 100644
--- a/src/paimon/common/executor/executor.cpp
+++ b/src/paimon/common/executor/executor.cpp
@@ -113,13 +113,14 @@ void DefaultExecutor::Add(std::function<void()> func) {
if (state_->stop) {
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_);
}
}
+ // Startup may throw; do not accept the callback until it succeeds.
+ state_->tasks.emplace(std::move(func));
}
state_->condition.notify_one();
}
diff --git a/src/paimon/common/executor/executor_submission_test.cpp
b/src/paimon/common/executor/executor_submission_test.cpp
new file mode 100644
index 00000000..a735bac3
--- /dev/null
+++ b/src/paimon/common/executor/executor_submission_test.cpp
@@ -0,0 +1,187 @@
+/*
+ * 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.
+ */
+
+#include <array>
+#include <atomic>
+#include <chrono>
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <future>
+#include <memory>
+#include <new>
+#include <utility>
+
+#include "gtest/gtest.h"
+#include "paimon/common/utils/scope_guard.h"
+#include "paimon/executor.h"
+
+namespace {
+thread_local bool armed = false;
+thread_local bool fired = false;
+thread_local int32_t allocations = 0;
+thread_local int32_t fail_at = 0;
+
+class AllocationFault {
+ public:
+ explicit AllocationFault(int32_t ordinal) {
+ allocations = 0;
+ fail_at = ordinal;
+ fired = false;
+ armed = true;
+ }
+ ~AllocationFault() {
+ armed = false;
+ }
+};
+} // namespace
+
+extern "C" void* __real__Znwm(std::size_t size);
+extern "C" void* __wrap__Znwm(std::size_t size) {
+ if (armed && ++allocations == fail_at) {
+ armed = false;
+ fired = true;
+ throw std::bad_alloc();
+ }
+ return __real__Znwm(size);
+}
+
+namespace paimon::test {
+namespace {
+struct SubmissionResult {
+ int32_t allocations;
+ bool fired;
+ bool caught;
+ bool recovered;
+ bool released;
+ int32_t runs;
+};
+
+SubmissionResult Submit(uint32_t workers, int32_t ordinal, bool recover, bool
shutdown) {
+ std::atomic<int32_t> runs = 0;
+ std::promise<void> marker;
+ auto ready = marker.get_future();
+ auto executor = CreateDefaultExecutor(workers).value();
+ auto owner = std::make_shared<int32_t>(0);
+ std::weak_ptr<int32_t> retained = owner;
+ std::function<void()> task = [&, owner]() { ++runs; };
+ owner.reset();
+ std::function<void()> recovery = [&]() { marker.set_value(); };
+ bool caught = false;
+ {
+ AllocationFault fault(ordinal);
+ try {
+ executor->Add(std::move(task));
+ } catch (const std::bad_alloc&) {
+ caught = true;
+ }
+ }
+ SubmissionResult result = {allocations, fired, caught, false,
retained.expired(), 0};
+ if (recover) {
+ executor->Add(std::move(recovery));
+ result.recovered = ready.wait_for(std::chrono::seconds(5)) ==
std::future_status::ready;
+ }
+ if (shutdown) {
+ executor->ShutdownNow();
+ }
+ // Graceful destruction also drains callbacks on the multi-worker oracle.
+ executor.reset();
+ result.runs = runs.load();
+ return result;
+}
+} // namespace
+
+TEST(ExecutorSubmissionTest, StartupAllocationFailuresDoNotAcceptCallback) {
+ for (uint32_t workers : {1u, 4u}) {
+ const auto observation = Submit(workers, 0, true, false);
+ ASSERT_GT(observation.allocations, 0);
+ ASSERT_FALSE(observation.fired);
+ ASSERT_FALSE(observation.caught);
+ ASSERT_TRUE(observation.recovered);
+ ASSERT_EQ(observation.runs, 1);
+ for (int32_t ordinal = 1; ordinal <= observation.allocations;
++ordinal) {
+ SCOPED_TRACE(::testing::Message() << "workers=" << workers << ",
ordinal=" << ordinal);
+ const auto result = Submit(workers, ordinal, true, false);
+ EXPECT_TRUE(result.fired);
+ EXPECT_TRUE(result.caught);
+ EXPECT_EQ(result.allocations, ordinal);
+ EXPECT_TRUE(result.recovered);
+ EXPECT_TRUE(result.released);
+ EXPECT_EQ(result.runs, 0);
+ for (bool shutdown : {false, true}) {
+ const auto cleanup = Submit(workers, ordinal, false, shutdown);
+ EXPECT_TRUE(cleanup.fired);
+ EXPECT_TRUE(cleanup.caught);
+ EXPECT_TRUE(cleanup.released);
+ EXPECT_EQ(cleanup.runs, 0);
+ }
+ }
+ }
+}
+
+TEST(ExecutorSubmissionTest, QueueAllocationFailurePreservesAcceptedPrefix) {
+ std::promise<void> release;
+ auto gate = release.get_future().share();
+ std::promise<void> marker;
+ auto ready = marker.get_future();
+ std::atomic<int32_t> runs = 0;
+ std::array<std::function<void()>, 128> tasks;
+ std::array<std::weak_ptr<int32_t>, 128> retained;
+ for (std::size_t index = 0; index < tasks.size(); ++index) {
+ auto owner = std::make_shared<int32_t>(0);
+ retained[index] = owner;
+ tasks[index] = [&, owner]() { ++runs; };
+ }
+ std::function<void()> recovery = [&]() { marker.set_value(); };
+ auto executor = CreateDefaultExecutor(1).value();
+ ScopeGuard release_on_error([&]() { release.set_value(); });
+ executor->Add([gate]() { gate.wait(); });
+ int32_t accepted = 0;
+ bool caught = false;
+ bool hit = false;
+ bool released = false;
+ for (auto& task : tasks) {
+ {
+ AllocationFault fault(1);
+ try {
+ executor->Add(std::move(task));
+ } catch (const std::bad_alloc&) {
+ caught = true;
+ }
+ }
+ if (fired) {
+ hit = true;
+ released = retained[accepted].expired();
+ break;
+ }
+ ++accepted;
+ }
+ release.set_value();
+ release_on_error.Release();
+ executor->Add(std::move(recovery));
+ const bool recovered = ready.wait_for(std::chrono::seconds(5)) ==
std::future_status::ready;
+ executor.reset();
+ EXPECT_TRUE(hit);
+ EXPECT_TRUE(caught);
+ EXPECT_TRUE(released);
+ EXPECT_TRUE(recovered);
+ EXPECT_GT(accepted, 0);
+ EXPECT_EQ(runs.load(), accepted);
+}
+} // namespace paimon::test