Skip to content

Commit c097bb9

Browse files
authored
fix(jindo): make async reads concurrency-safe (#427)
1 parent ff446f9 commit c097bb9

15 files changed

Lines changed: 179 additions & 73 deletions

CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ option(PAIMON_ENABLE_AVRO "Whether to enable avro file format" ON)
5757
option(PAIMON_ENABLE_ORC "Whether to enable orc file format" ON)
5858
option(PAIMON_ENABLE_LANCE "Whether to enable lance file format" OFF)
5959
option(PAIMON_ENABLE_JINDO "Whether to enable jindo file system" OFF)
60+
option(PAIMON_ENABLE_NETWORK_TESTS
61+
"Whether to enable tests that access real remote services over the network" OFF)
6062
option(PAIMON_ENABLE_LUMINA "Whether to enable lumina vector index" OFF)
6163
option(PAIMON_ENABLE_LUCENE "Whether to enable lucene index" OFF)
6264
option(PAIMON_ENABLE_TANTIVY
@@ -70,6 +72,11 @@ endif()
7072
if(PAIMON_ENABLE_JINDO)
7173
add_definitions(-DPAIMON_ENABLE_JINDO)
7274
endif()
75+
if(PAIMON_ENABLE_NETWORK_TESTS)
76+
if(NOT PAIMON_BUILD_TESTS)
77+
message(FATAL_ERROR "PAIMON_ENABLE_NETWORK_TESTS requires PAIMON_BUILD_TESTS=ON")
78+
endif()
79+
endif()
7380
if(PAIMON_USE_CXX11_ABI)
7481
add_definitions(-D_GLIBCXX_USE_CXX11_ABI=1)
7582
else()

cmake_modules/DefineOptions.cmake

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}")
107107

108108
define_option(PAIMON_BUILD_TESTS "Build the Paimon googletest unit tests" OFF)
109109

110+
define_option(PAIMON_ENABLE_NETWORK_TESTS
111+
"Enable tests that access real remote services over the network" OFF)
112+
110113
define_option(PAIMON_BUILD_BENCHMARKS
111114
"Build the Paimon Google Benchmark performance benchmarks" OFF)
112115

include/paimon/fs/file_system.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class PAIMON_EXPORT InputStream : public Stream {
9898
/// @param callback The callback function to be invoked upon completion of the read operation.
9999
/// The callback will receive a Status object indicating the success or failure
100100
/// of the read operation.
101+
/// @note The caller must keep the input stream and buffer alive until the callback is invoked.
101102
virtual void ReadAsync(char* buffer, int64_t size, int64_t offset,
102103
std::function<void(Status)>&& callback) = 0;
103104

src/paimon/CMakeLists.txt

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -791,30 +791,39 @@ if(PAIMON_BUILD_TESTS)
791791
${TEST_STATIC_LINK_LIBS}
792792
${GTEST_LINK_TOOLCHAIN})
793793

794-
# jindo_utils_test only checks the in-memory status conversion and does not need an
795-
# OSS cluster, so it runs whenever jindo is built. The other jindo tests need real
796-
# OSS access and stay disabled.
797-
set(FS_TEST_JINDO_SOURCES)
794+
set(PAIMON_JINDO_FS_TEST_SOURCES)
795+
set(PAIMON_JINDO_FS_TEST_LINK_LIBS)
798796
if(PAIMON_ENABLE_JINDO)
799-
list(APPEND FS_TEST_JINDO_SOURCES fs/jindo/jindo_file_system_unit_test.cpp
797+
# These unit tests do not access remote OSS.
798+
list(APPEND PAIMON_JINDO_FS_TEST_SOURCES fs/jindo/jindo_file_system_unit_test.cpp
800799
fs/jindo/jindo_utils_test.cpp)
800+
if(PAIMON_ENABLE_NETWORK_TESTS)
801+
# Factory and integration tests access real OSS.
802+
list(APPEND PAIMON_JINDO_FS_TEST_SOURCES
803+
fs/jindo/jindo_file_system_factory_test.cpp
804+
fs/jindo/jindo_file_system_test.cpp)
805+
endif()
806+
list(APPEND PAIMON_JINDO_FS_TEST_LINK_LIBS
807+
${PAIMON_JINDO_FILE_SYSTEM_STATIC_LINK_LIBS})
801808
endif()
802809

803810
add_paimon_test(fs_test
804811
SOURCES
805812
common/fs/file_system_test.cpp
806813
common/fs/resolving_file_system_test.cpp
807814
fs/local/local_file_test.cpp
808-
${FS_TEST_JINDO_SOURCES}
809-
# fs/jindo/jindo_file_system_factory_test.cpp
810-
# fs/jindo/jindo_file_system_test.cpp
815+
${PAIMON_JINDO_FS_TEST_SOURCES}
811816
STATIC_LINK_LIBS
812817
paimon_shared
813818
${PAIMON_LOCAL_FILE_SYSTEM_STATIC_LINK_LIBS}
814-
${PAIMON_JINDO_FILE_SYSTEM_STATIC_LINK_LIBS}
819+
${PAIMON_JINDO_FS_TEST_LINK_LIBS}
815820
test_utils_static
816821
${GTEST_LINK_TOOLCHAIN}
817822
EXTRA_INCLUDES
818823
${JINDOSDK_INCLUDE_DIR})
819824

825+
if(PAIMON_ENABLE_NETWORK_TESTS)
826+
target_compile_definitions(paimon-fs-test PRIVATE PAIMON_ENABLE_NETWORK_TESTS)
827+
endif()
828+
820829
endif()

src/paimon/common/fs/file_system_test.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1474,7 +1474,9 @@ TEST_P(FileSystemTest, TestAtomicStoreAlreadyExist) {
14741474
std::vector<std::string> GetTestValuesForFileSystemTest() {
14751475
std::vector<std::string> values;
14761476
values.emplace_back("local");
1477-
// values.emplace_back("jindo");
1477+
#if defined(PAIMON_ENABLE_NETWORK_TESTS) && defined(PAIMON_ENABLE_JINDO)
1478+
values.emplace_back("jindo");
1479+
#endif
14781480
return values;
14791481
}
14801482

src/paimon/core/operation/key_value_file_store_write_test.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,6 @@ TEST_F(KeyValueFileStoreWriteTest, TestSharedShreddingMapRestoreInitializesNextW
320320
{"fields.tags.map.shared-shredding.column-placement-policy", "plain"},
321321
{"write-only", "true"},
322322
{"bucket", "1"},
323-
{"enable-pk-commit-in-inte-test", ""},
324323
};
325324
auto logical_schema = arrow::schema({
326325
arrow::field("id", arrow::int32(), /*nullable=*/false),

src/paimon/fs/jindo/jindo_file_system.cpp

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
#include "paimon/fs/jindo/jindo_file_system.h"
1818

19+
#include <atomic>
1920
#include <cassert>
21+
#include <string_view>
2022
#include <utility>
2123

2224
#include "JdoFileInfo.hpp" // NOLINT(build/include_subdir)
@@ -53,6 +55,29 @@ class JindoFileSystemImpl {
5355
std::unique_ptr<JdoFileSystem> fs_;
5456
};
5557

58+
namespace {
59+
60+
class AsyncReadState {
61+
public:
62+
explicit AsyncReadState(std::function<void(Status)>&& callback)
63+
: callback_(std::move(callback)) {}
64+
65+
void Complete(JdoStatus status) {
66+
if (completed_.exchange(true)) {
67+
return;
68+
}
69+
callback_(status.ok() ? Status::OK() : Status::IOError(status.errMsg()));
70+
}
71+
72+
std::string_view result;
73+
74+
private:
75+
std::atomic<bool> completed_{false};
76+
std::function<void(Status)> callback_;
77+
};
78+
79+
} // namespace
80+
5681
JindoFileSystem::JindoFileSystem(std::unique_ptr<JdoFileSystem>&& fs)
5782
: impl_(std::make_shared<JindoFileSystemImpl>(std::move(fs))) {}
5883

@@ -228,15 +253,17 @@ Result<int64_t> JindoInputStream::Length() const {
228253

229254
Result<int64_t> JindoInputStream::Read(char* buffer, int64_t size) {
230255
PAIMON_RETURN_NOT_OK(ValidateValueNonNegative(size, "read length"));
231-
PAIMON_RETURN_NOT_OK_FROM_JINDO(reader_->read(size, &result_, buffer));
232-
return result_.length();
256+
std::string_view result;
257+
PAIMON_RETURN_NOT_OK_FROM_JINDO(reader_->read(size, &result, buffer));
258+
return result.length();
233259
}
234260

235261
Result<int64_t> JindoInputStream::Read(char* buffer, int64_t size, int64_t offset) {
236262
PAIMON_RETURN_NOT_OK(ValidateValueNonNegative(size, "read length"));
237263
PAIMON_RETURN_NOT_OK(ValidateValueNonNegative(offset, "read offset"));
238-
PAIMON_RETURN_NOT_OK_FROM_JINDO(reader_->pread(offset, size, &result_, buffer));
239-
return result_.length();
264+
std::string_view result;
265+
PAIMON_RETURN_NOT_OK_FROM_JINDO(reader_->pread(offset, size, &result, buffer));
266+
return result.length();
240267
}
241268

242269
void JindoInputStream::ReadAsync(char* buffer, int64_t size, int64_t offset,
@@ -251,12 +278,17 @@ void JindoInputStream::ReadAsync(char* buffer, int64_t size, int64_t offset,
251278
callback(validate_status);
252279
return;
253280
}
254-
auto outer_callback = [=](JdoStatus status) {
255-
callback(status.ok() ? Status::OK() : Status::IOError(status.errMsg()));
256-
};
257-
auto task = reader_->preadAsync(offset, size, &result_, buffer, outer_callback);
281+
std::shared_ptr<AsyncReadState> state = std::make_shared<AsyncReadState>(std::move(callback));
282+
auto task = reader_->preadAsync(offset, size, &state->result, buffer,
283+
[state](JdoStatus status) { state->Complete(status); });
258284
assert(task);
259-
[[maybe_unused]] auto perform_status = task->perform();
285+
286+
auto perform_status = task->perform();
287+
if (!perform_status.ok()) {
288+
state->Complete(perform_status);
289+
[[maybe_unused]] auto status = task->cancel();
290+
return;
291+
}
260292
}
261293

262294
Status JindoInputStream::Close() {

src/paimon/fs/jindo/jindo_file_system.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
#include <functional>
2121
#include <memory>
2222
#include <string>
23-
#include <string_view>
2423
#include <vector>
2524

2625
#include "JdoFileSystem.hpp" // NOLINT(build/include_subdir)
@@ -83,7 +82,6 @@ class JindoInputStream : public InputStream {
8382
// the Jindo Reader.
8483
std::shared_ptr<JindoFileSystemImpl> fs_;
8584
std::unique_ptr<JdoReader> reader_;
86-
std::string_view result_;
8785
};
8886

8987
class JindoOutputStream : public OutputStream {

src/paimon/fs/jindo/jindo_file_system_test.cpp

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,19 @@
1414
* limitations under the License.
1515
*/
1616

17+
#include <chrono>
18+
#include <cstdint>
19+
#include <future>
20+
#include <string>
21+
#include <unordered_set>
22+
#include <vector>
23+
1724
#include "gtest/gtest.h"
1825
#include "paimon/fs/jindo/jindo_file_system_factory.h"
1926
#include "paimon/testing/utils/testharness.h"
27+
2028
namespace paimon::jindo::test {
29+
2130
// This test shows inconsistent behavior with the local file system in some abnormal scenarios.
2231
class JindoFileSystemTest : public ::testing::Test {
2332
public:
@@ -124,4 +133,73 @@ TEST_F(JindoFileSystemTest, TestSeek) {
124133
ASSERT_OK(in_stream->Close());
125134
}
126135

136+
TEST(JindoFileSystemPaginationTest, TestListDirAcrossOssPageBoundary) {
137+
constexpr int32_t kFileCount = 1234;
138+
const std::string test_dir = "oss://paimon-unittest/test_data/jindo_listdir_truncated_1234/";
139+
std::map<std::string, std::string> options = paimon::test::GetJindoTestOptions();
140+
141+
auto fs_factory = std::make_shared<JindoFileSystemFactory>();
142+
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileSystem> fs, fs_factory->Create(test_dir, options));
143+
144+
std::vector<std::unique_ptr<BasicFileStatus>> file_statuses;
145+
ASSERT_OK(fs->ListDir(test_dir, &file_statuses));
146+
ASSERT_EQ(file_statuses.size(), kFileCount);
147+
148+
std::unordered_set<std::string> actual_paths;
149+
for (const std::unique_ptr<BasicFileStatus>& file_status : file_statuses) {
150+
ASSERT_TRUE(actual_paths.insert(file_status->GetPath()).second)
151+
<< "duplicate path: " << file_status->GetPath();
152+
}
153+
for (int32_t i = 0; i < kFileCount; ++i) {
154+
std::string index = std::to_string(i);
155+
index.insert(/*pos=*/0, /*count=*/4 - index.size(), /*ch=*/'0');
156+
ASSERT_NE(actual_paths.find(test_dir + "file-" + index + ".txt"), actual_paths.end());
157+
}
158+
}
159+
160+
TEST(JindoFileSystemAsyncReadTest, TestConcurrentReadAsyncAndReadFromOss) {
161+
constexpr int32_t kConcurrentReads = 64;
162+
constexpr int64_t kAsyncReadSize = 7;
163+
const std::string file_path = "oss://paimon-unittest/test_data/jindo_read_async_128mb.bin";
164+
std::map<std::string, std::string> options = paimon::test::GetJindoTestOptions();
165+
auto fs_factory = std::make_shared<JindoFileSystemFactory>();
166+
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileSystem> fs, fs_factory->Create(file_path, options));
167+
ASSERT_OK_AND_ASSIGN(std::unique_ptr<InputStream> input_stream, fs->Open(file_path));
168+
169+
std::vector<std::vector<char>> async_buffers(kConcurrentReads,
170+
std::vector<char>(kAsyncReadSize));
171+
std::vector<std::promise<Status>> promises(kConcurrentReads);
172+
std::vector<std::future<Status>> futures;
173+
futures.reserve(kConcurrentReads);
174+
for (std::promise<Status>& promise : promises) {
175+
futures.push_back(promise.get_future());
176+
}
177+
178+
std::vector<Status> sync_read_statuses;
179+
std::vector<int64_t> sync_read_sizes;
180+
sync_read_statuses.reserve(kConcurrentReads);
181+
sync_read_sizes.reserve(kConcurrentReads);
182+
for (int32_t i = 0; i < kConcurrentReads; ++i) {
183+
input_stream->ReadAsync(
184+
async_buffers[i].data(), async_buffers[i].size(), /*offset=*/0,
185+
[&promises, i](Status status) { promises[i].set_value(std::move(status)); });
186+
187+
char sync_buffer = 0;
188+
Result<int64_t> sync_read_result =
189+
input_stream->Read(&sync_buffer, /*size=*/1, /*offset=*/i);
190+
sync_read_statuses.push_back(sync_read_result.status());
191+
sync_read_sizes.push_back(sync_read_result.ok() ? sync_read_result.value() : -1);
192+
}
193+
194+
for (int32_t i = 0; i < kConcurrentReads; ++i) {
195+
ASSERT_EQ(futures[i].wait_for(std::chrono::seconds(60)), std::future_status::ready)
196+
<< "async read=" << i;
197+
ASSERT_OK(futures[i].get());
198+
}
199+
for (int32_t i = 0; i < kConcurrentReads; ++i) {
200+
ASSERT_OK(sync_read_statuses[i]) << "sync read=" << i;
201+
ASSERT_EQ(sync_read_sizes[i], 1) << "sync read=" << i;
202+
}
203+
}
204+
127205
} // namespace paimon::jindo::test

src/paimon/testing/utils/test_helper.h

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,16 @@ class TestHelper {
5959
const std::vector<std::string>& primary_keys,
6060
const std::map<std::string, std::string>& options, bool is_streaming_mode,
6161
bool ignore_if_exists = false, const std::string& temp_directory = "") {
62-
// only for test && only check the key
63-
auto new_options = options;
64-
new_options["enable-object-store-catalog-in-inte-test"] = "";
65-
PAIMON_ASSIGN_OR_RAISE(auto catalog, Catalog::Create(root_path, new_options));
66-
PAIMON_RETURN_NOT_OK(catalog->CreateDatabase("foo", new_options, ignore_if_exists));
62+
PAIMON_ASSIGN_OR_RAISE(auto catalog, Catalog::Create(root_path, options));
63+
PAIMON_RETURN_NOT_OK(catalog->CreateDatabase("foo", options, ignore_if_exists));
6764
::ArrowSchema c_schema;
6865
ScopeGuard guard([schema = &c_schema]() { ArrowSchemaRelease(schema); });
6966
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*schema, &c_schema));
7067
PAIMON_RETURN_NOT_OK(catalog->CreateTable(Identifier("foo", "bar"), &c_schema,
71-
partition_keys, primary_keys, new_options,
68+
partition_keys, primary_keys, options,
7269
ignore_if_exists));
7370
std::string table_path = PathUtil::JoinPath(root_path, "foo.db/bar");
74-
return Create(table_path, new_options, is_streaming_mode, temp_directory);
71+
return Create(table_path, options, is_streaming_mode, temp_directory);
7572
}
7673

7774
static Result<std::unique_ptr<TestHelper>> Create(
@@ -96,14 +93,10 @@ class TestHelper {
9693
.Finish());
9794
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreWrite> write,
9895
FileStoreWrite::Create(std::move(write_context)));
99-
std::map<std::string, std::string> new_options = options;
100-
// only for test && only check the key
101-
new_options["enable-pk-commit-in-inte-test"] = "";
102-
new_options["enable-object-store-commit-in-inte-test"] = "";
10396
CommitContextBuilder commit_context_builder(table_path, commit_user);
10497
PAIMON_ASSIGN_OR_RAISE(
10598
std::unique_ptr<CommitContext> commit_context,
106-
commit_context_builder.SetOptions(new_options).IgnoreEmptyCommit(false).Finish());
99+
commit_context_builder.SetOptions(options).IgnoreEmptyCommit(false).Finish());
107100
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreCommit> commit,
108101
FileStoreCommit::Create(std::move(commit_context)));
109102
return std::unique_ptr<TestHelper>(new TestHelper(std::move(file_system), std::move(write),

0 commit comments

Comments
 (0)