Skip to content

Commit 46cc9d6

Browse files
authored
fix(executor): do not retain callbacks when worker startup fails (#437)
1 parent 40a7d05 commit 46cc9d6

3 files changed

Lines changed: 204 additions & 1 deletion

File tree

‎src/paimon/CMakeLists.txt‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,21 @@ add_subdirectory(common/file_index)
542542
add_subdirectory(common/global_index)
543543

544544
if(PAIMON_BUILD_TESTS)
545+
# Wrap actual executor allocations, not calls inside an already linked DSO.
546+
if(PAIMON_BUILD_STATIC
547+
AND CMAKE_SYSTEM_NAME STREQUAL "Linux"
548+
AND CMAKE_SIZEOF_VOID_P EQUAL 8
549+
AND CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang)$")
550+
add_paimon_test(executor_submission_test
551+
SOURCES
552+
common/executor/executor_submission_test.cpp
553+
STATIC_LINK_LIBS
554+
paimon_static
555+
${GTEST_LINK_TOOLCHAIN})
556+
target_link_options(paimon-executor-submission-test PRIVATE "LINKER:--wrap=_Znwm")
557+
set_tests_properties(paimon-executor-submission-test PROPERTIES TIMEOUT 30)
558+
endif()
559+
545560
# The shared HTTP client is compiled only for the components that need libcurl,
546561
# so its test follows the same gate.
547562
set(PAIMON_COMMON_HTTP_CLIENT_TEST_SRCS)

‎src/paimon/common/executor/executor.cpp‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,14 @@ void DefaultExecutor::Add(std::function<void()> func) {
113113
if (state_->stop) {
114114
return;
115115
}
116-
state_->tasks.emplace(std::move(func));
117116
if (workers_.empty()) {
118117
workers_.reserve(thread_count_);
119118
for (uint32_t i = 0; i < thread_count_; ++i) {
120119
workers_.emplace_back(&DefaultExecutor::WorkerThread, state_);
121120
}
122121
}
122+
// Startup may throw; do not accept the callback until it succeeds.
123+
state_->tasks.emplace(std::move(func));
123124
}
124125
state_->condition.notify_one();
125126
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include <array>
21+
#include <atomic>
22+
#include <chrono>
23+
#include <cstddef>
24+
#include <cstdint>
25+
#include <functional>
26+
#include <future>
27+
#include <memory>
28+
#include <new>
29+
#include <utility>
30+
31+
#include "gtest/gtest.h"
32+
#include "paimon/common/utils/scope_guard.h"
33+
#include "paimon/executor.h"
34+
35+
namespace {
36+
thread_local bool armed = false;
37+
thread_local bool fired = false;
38+
thread_local int32_t allocations = 0;
39+
thread_local int32_t fail_at = 0;
40+
41+
class AllocationFault {
42+
public:
43+
explicit AllocationFault(int32_t ordinal) {
44+
allocations = 0;
45+
fail_at = ordinal;
46+
fired = false;
47+
armed = true;
48+
}
49+
~AllocationFault() {
50+
armed = false;
51+
}
52+
};
53+
} // namespace
54+
55+
extern "C" void* __real__Znwm(std::size_t size);
56+
extern "C" void* __wrap__Znwm(std::size_t size) {
57+
if (armed && ++allocations == fail_at) {
58+
armed = false;
59+
fired = true;
60+
throw std::bad_alloc();
61+
}
62+
return __real__Znwm(size);
63+
}
64+
65+
namespace paimon::test {
66+
namespace {
67+
struct SubmissionResult {
68+
int32_t allocations;
69+
bool fired;
70+
bool caught;
71+
bool recovered;
72+
bool released;
73+
int32_t runs;
74+
};
75+
76+
SubmissionResult Submit(uint32_t workers, int32_t ordinal, bool recover, bool shutdown) {
77+
std::atomic<int32_t> runs = 0;
78+
std::promise<void> marker;
79+
auto ready = marker.get_future();
80+
auto executor = CreateDefaultExecutor(workers).value();
81+
auto owner = std::make_shared<int32_t>(0);
82+
std::weak_ptr<int32_t> retained = owner;
83+
std::function<void()> task = [&, owner]() { ++runs; };
84+
owner.reset();
85+
std::function<void()> recovery = [&]() { marker.set_value(); };
86+
bool caught = false;
87+
{
88+
AllocationFault fault(ordinal);
89+
try {
90+
executor->Add(std::move(task));
91+
} catch (const std::bad_alloc&) {
92+
caught = true;
93+
}
94+
}
95+
SubmissionResult result = {allocations, fired, caught, false, retained.expired(), 0};
96+
if (recover) {
97+
executor->Add(std::move(recovery));
98+
result.recovered = ready.wait_for(std::chrono::seconds(5)) == std::future_status::ready;
99+
}
100+
if (shutdown) {
101+
executor->ShutdownNow();
102+
}
103+
// Graceful destruction also drains callbacks on the multi-worker oracle.
104+
executor.reset();
105+
result.runs = runs.load();
106+
return result;
107+
}
108+
} // namespace
109+
110+
TEST(ExecutorSubmissionTest, StartupAllocationFailuresDoNotAcceptCallback) {
111+
for (uint32_t workers : {1u, 4u}) {
112+
const auto observation = Submit(workers, 0, true, false);
113+
ASSERT_GT(observation.allocations, 0);
114+
ASSERT_FALSE(observation.fired);
115+
ASSERT_FALSE(observation.caught);
116+
ASSERT_TRUE(observation.recovered);
117+
ASSERT_EQ(observation.runs, 1);
118+
for (int32_t ordinal = 1; ordinal <= observation.allocations; ++ordinal) {
119+
SCOPED_TRACE(::testing::Message() << "workers=" << workers << ", ordinal=" << ordinal);
120+
const auto result = Submit(workers, ordinal, true, false);
121+
EXPECT_TRUE(result.fired);
122+
EXPECT_TRUE(result.caught);
123+
EXPECT_EQ(result.allocations, ordinal);
124+
EXPECT_TRUE(result.recovered);
125+
EXPECT_TRUE(result.released);
126+
EXPECT_EQ(result.runs, 0);
127+
for (bool shutdown : {false, true}) {
128+
const auto cleanup = Submit(workers, ordinal, false, shutdown);
129+
EXPECT_TRUE(cleanup.fired);
130+
EXPECT_TRUE(cleanup.caught);
131+
EXPECT_TRUE(cleanup.released);
132+
EXPECT_EQ(cleanup.runs, 0);
133+
}
134+
}
135+
}
136+
}
137+
138+
TEST(ExecutorSubmissionTest, QueueAllocationFailurePreservesAcceptedPrefix) {
139+
std::promise<void> release;
140+
auto gate = release.get_future().share();
141+
std::promise<void> marker;
142+
auto ready = marker.get_future();
143+
std::atomic<int32_t> runs = 0;
144+
std::array<std::function<void()>, 128> tasks;
145+
std::array<std::weak_ptr<int32_t>, 128> retained;
146+
for (std::size_t index = 0; index < tasks.size(); ++index) {
147+
auto owner = std::make_shared<int32_t>(0);
148+
retained[index] = owner;
149+
tasks[index] = [&, owner]() { ++runs; };
150+
}
151+
std::function<void()> recovery = [&]() { marker.set_value(); };
152+
auto executor = CreateDefaultExecutor(1).value();
153+
ScopeGuard release_on_error([&]() { release.set_value(); });
154+
executor->Add([gate]() { gate.wait(); });
155+
int32_t accepted = 0;
156+
bool caught = false;
157+
bool hit = false;
158+
bool released = false;
159+
for (auto& task : tasks) {
160+
{
161+
AllocationFault fault(1);
162+
try {
163+
executor->Add(std::move(task));
164+
} catch (const std::bad_alloc&) {
165+
caught = true;
166+
}
167+
}
168+
if (fired) {
169+
hit = true;
170+
released = retained[accepted].expired();
171+
break;
172+
}
173+
++accepted;
174+
}
175+
release.set_value();
176+
release_on_error.Release();
177+
executor->Add(std::move(recovery));
178+
const bool recovered = ready.wait_for(std::chrono::seconds(5)) == std::future_status::ready;
179+
executor.reset();
180+
EXPECT_TRUE(hit);
181+
EXPECT_TRUE(caught);
182+
EXPECT_TRUE(released);
183+
EXPECT_TRUE(recovered);
184+
EXPECT_GT(accepted, 0);
185+
EXPECT_EQ(runs.load(), accepted);
186+
}
187+
} // namespace paimon::test

0 commit comments

Comments
 (0)