| // 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 "exec/spill/spill_file.h" |
| |
| #include <gtest/gtest.h> |
| |
| #include <algorithm> |
| #include <filesystem> |
| #include <functional> |
| #include <memory> |
| #include <numeric> |
| #include <set> |
| #include <string> |
| #include <vector> |
| |
| #include "common/config.h" |
| #include "core/block/block.h" |
| #include "core/data_type/data_type_number.h" |
| #include "core/data_type/data_type_string.h" |
| #include "exec/operator/spill_counters.h" |
| #include "exec/pipeline/pipeline_fragment_context.h" |
| #include "exec/spill/spill_file_manager.h" |
| #include "exec/spill/spill_file_reader.h" |
| #include "exec/spill/spill_file_writer.h" |
| #include "io/fs/file_writer.h" |
| #include "io/fs/local_file_system.h" |
| #include "runtime/exec_env.h" |
| #include "runtime/fragment_mgr.h" |
| #include "runtime/runtime_profile.h" |
| #include "testutil/column_helper.h" |
| #include "testutil/mock/mock_query_context.h" |
| #include "testutil/mock/mock_runtime_state.h" |
| #include "util/debug_points.h" |
| #include "util/defer_op.h" |
| #include "util/uid_util.h" |
| |
| namespace doris::vectorized { |
| |
| class SpillFileTest : public testing::Test { |
| protected: |
| void SetUp() override { |
| _runtime_state = std::make_unique<MockRuntimeState>(); |
| |
| _profile = std::make_unique<RuntimeProfile>("test"); |
| _custom_profile = std::make_unique<RuntimeProfile>("CustomCounters"); |
| _common_profile = std::make_unique<RuntimeProfile>("CommonCounters"); |
| |
| _common_profile->AddHighWaterMarkCounter("MemoryUsage", TUnit::BYTES, "", 1); |
| ADD_TIMER_WITH_LEVEL(_common_profile.get(), "ExecTime", 1); |
| |
| // Register exactly what PipelineXSpillLocalState registers in production, by |
| // reusing the same initializers. Hand-copying the counter names here is what let |
| // a misspelled lookup in SpillFileReader ("SpillReadDerializeBlockTime") go |
| // unnoticed: the test registered the same misspelling, so the lookup "worked" |
| // here while silently returning null in a real query. |
| SpillWriteCounters write_counters; |
| write_counters.init(_custom_profile.get()); |
| SpillReadCounters read_counters; |
| read_counters.init(_custom_profile.get()); |
| |
| // Source-only extras, see PipelineXSpillLocalState::init_spill_{write,read}_counters. |
| ADD_TIMER_WITH_LEVEL(_custom_profile.get(), profile::SPILL_TOTAL_TIME, 1); |
| ADD_COUNTER_WITH_LEVEL(_custom_profile.get(), profile::SPILL_WRITE_FILE_BYTES, TUnit::BYTES, |
| 1); |
| ADD_COUNTER_WITH_LEVEL(_custom_profile.get(), profile::SPILL_WRITE_FILE_TOTAL_COUNT, |
| TUnit::UNIT, 1); |
| ADD_COUNTER_WITH_LEVEL(_custom_profile.get(), profile::SPILL_WRITE_FILE_CURRENT_BYTES, |
| TUnit::BYTES, 1); |
| |
| _profile->add_child(_custom_profile.get(), true); |
| _profile->add_child(_common_profile.get(), true); |
| |
| _spill_dir = "./ut_dir/spill_file_test"; |
| _second_spill_dir = "./ut_dir/spill_file_test_second"; |
| auto spill_data_dir = |
| std::make_unique<SpillDataDir>(_spill_dir, 1024L * 1024 * 128, TStorageMedium::SSD); |
| auto st = io::global_local_filesystem()->create_directory(spill_data_dir->path(), false); |
| ASSERT_TRUE(st.ok()) << "create directory failed: " << st.to_string(); |
| auto second_spill_data_dir = std::make_unique<SpillDataDir>( |
| _second_spill_dir, 1024L * 1024 * 128, TStorageMedium::HDD); |
| st = io::global_local_filesystem()->create_directory(second_spill_data_dir->path(), false); |
| ASSERT_TRUE(st.ok()) << "create directory failed: " << st.to_string(); |
| |
| std::unordered_map<std::string, std::unique_ptr<SpillDataDir>> data_map; |
| _data_dir_ptr = spill_data_dir.get(); |
| _second_data_dir_ptr = second_spill_data_dir.get(); |
| data_map.emplace("test", std::move(spill_data_dir)); |
| data_map.emplace("test_second", std::move(second_spill_data_dir)); |
| auto* spill_file_manager = new SpillFileManager(std::move(data_map)); |
| ExecEnv::GetInstance()->_spill_file_mgr = spill_file_manager; |
| st = spill_file_manager->init(); |
| ASSERT_TRUE(st.ok()) << "init spill file manager failed: " << st.to_string(); |
| } |
| |
| void TearDown() override { |
| ExecEnv::GetInstance()->spill_file_mgr()->stop(); |
| _runtime_state.reset(); |
| SAFE_DELETE(ExecEnv::GetInstance()->_spill_file_mgr); |
| // Clean up test directory |
| auto st = io::global_local_filesystem()->delete_directory(_spill_dir); |
| (void)st; |
| st = io::global_local_filesystem()->delete_directory(_second_spill_dir); |
| (void)st; |
| } |
| |
| Block _create_int_block(const std::vector<int32_t>& data) { |
| return ColumnHelper::create_block<DataTypeInt32>(data); |
| } |
| |
| Block _create_two_column_block(const std::vector<int32_t>& col1, |
| const std::vector<int64_t>& col2) { |
| auto block = ColumnHelper::create_block<DataTypeInt32>(col1); |
| block.insert(ColumnHelper::create_column_with_name<DataTypeInt64>(col2)); |
| return block; |
| } |
| |
| void _write_and_release_spill_file(const TUniqueId& query_id, QueryContext* query_ctx, |
| SpillDataDir* data_dir, const std::string& relative_path) { |
| TQueryGlobals query_globals; |
| auto runtime_state = std::make_unique<MockRuntimeState>( |
| query_id, 0, query_ctx->query_options(), query_globals, ExecEnv::GetInstance(), |
| query_ctx); |
| |
| auto spill_file = std::make_shared<SpillFile>( |
| data_dir, fmt::format("{}/{}", print_id(query_id), relative_path)); |
| |
| SpillFileWriterSPtr writer; |
| auto st = spill_file->create_writer(runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| auto block = _create_int_block({1, 2, 3}); |
| st = writer->write_block(runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| writer.reset(); |
| spill_file.reset(); |
| } |
| |
| void _create_residual_file(const std::string& file_path) { |
| auto st = io::global_local_filesystem()->create_directory( |
| std::filesystem::path(file_path).parent_path(), false); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| io::FileWriterPtr writer; |
| st = io::global_local_filesystem()->create_file(file_path, &writer); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| } |
| |
| std::set<std::string> _gc_subdirectories(SpillDataDir* data_dir) { |
| std::set<std::string> subdirectories; |
| for (const auto& entry : |
| std::filesystem::directory_iterator(data_dir->get_spill_data_gc_path())) { |
| if (entry.is_directory()) { |
| subdirectories.emplace(entry.path().filename().string()); |
| } |
| } |
| return subdirectories; |
| } |
| |
| std::unique_ptr<MockRuntimeState> _runtime_state; |
| std::unique_ptr<RuntimeProfile> _profile; |
| std::unique_ptr<RuntimeProfile> _custom_profile; |
| std::unique_ptr<RuntimeProfile> _common_profile; |
| std::string _spill_dir; |
| std::string _second_spill_dir; |
| SpillDataDir* _data_dir_ptr = nullptr; |
| SpillDataDir* _second_data_dir_ptr = nullptr; |
| }; |
| |
| // ═══════════════════════════════════════════════════════════════════════ |
| // SpillFile basic tests |
| // ═══════════════════════════════════════════════════════════════════════ |
| |
| TEST_F(SpillFileTest, CreateSpillFile) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/test_file", |
| spill_file); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| ASSERT_TRUE(spill_file != nullptr); |
| ASSERT_FALSE(spill_file->ready_for_reading()); |
| } |
| |
| TEST_F(SpillFileTest, CreateWriterAndReader) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/create_wr", |
| spill_file); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| |
| // Create writer |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| ASSERT_TRUE(writer != nullptr); |
| |
| // Close writer with no data written |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| ASSERT_TRUE(spill_file->ready_for_reading()); |
| |
| // Create reader on empty file (0 parts) |
| auto reader = spill_file->create_reader(_runtime_state.get(), _profile.get()); |
| ASSERT_TRUE(reader != nullptr); |
| |
| st = reader->open(); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| |
| Block block; |
| bool eos = false; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| ASSERT_TRUE(eos); |
| |
| st = reader->close(); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| } |
| |
| // ═══════════════════════════════════════════════════════════════════════ |
| // SpillFileWriter tests |
| // ═══════════════════════════════════════════════════════════════════════ |
| |
| TEST_F(SpillFileTest, WriteSingleBlock) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/single_block", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block = _create_int_block({1, 2, 3, 4, 5}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| |
| ASSERT_TRUE(spill_file->ready_for_reading()); |
| |
| auto* write_rows_counter = _custom_profile->get_counter("SpillWriteRows"); |
| ASSERT_TRUE(write_rows_counter != nullptr); |
| ASSERT_EQ(write_rows_counter->value(), 5); |
| } |
| |
| TEST_F(SpillFileTest, WriteMultipleBlocks) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/multi_blocks", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| for (int i = 0; i < 5; ++i) { |
| auto block = _create_int_block({i * 10, i * 10 + 1, i * 10 + 2}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()) << "write block " << i << " failed: " << st.to_string(); |
| } |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| |
| auto* write_rows_counter = _custom_profile->get_counter("SpillWriteRows"); |
| ASSERT_EQ(write_rows_counter->value(), 15); |
| |
| auto* write_block_counter = _custom_profile->get_counter("SpillWriteBlockCount"); |
| ASSERT_EQ(write_block_counter->value(), 5); |
| } |
| |
| TEST_F(SpillFileTest, WriteTwoColumnBlock) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/two_col", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block = _create_two_column_block({1, 2, 3}, {100, 200, 300}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| ASSERT_TRUE(spill_file->ready_for_reading()); |
| } |
| |
| TEST_F(SpillFileTest, WriteEmptyBlock) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/empty_block", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| Block empty_block; |
| st = writer->write_block(_runtime_state.get(), empty_block); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| } |
| |
| TEST_F(SpillFileTest, DoubleCloseWriter) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/double_close", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block = _create_int_block({1, 2, 3}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| |
| // Double close should be a no-op |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // ═══════════════════════════════════════════════════════════════════════ |
| // SpillFileReader tests |
| // ═══════════════════════════════════════════════════════════════════════ |
| |
| TEST_F(SpillFileTest, ReadSingleBlock) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/read_single", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| // Write |
| { |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block = _create_int_block({10, 20, 30, 40, 50}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // Read |
| auto reader = spill_file->create_reader(_runtime_state.get(), _profile.get()); |
| st = reader->open(); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| |
| Block block; |
| bool eos = false; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| ASSERT_FALSE(eos); |
| ASSERT_EQ(block.rows(), 5); |
| |
| // Verify data |
| auto col = block.get_by_position(0).column; |
| ASSERT_EQ(col->get_int(0), 10); |
| ASSERT_EQ(col->get_int(1), 20); |
| ASSERT_EQ(col->get_int(2), 30); |
| ASSERT_EQ(col->get_int(3), 40); |
| ASSERT_EQ(col->get_int(4), 50); |
| |
| // Next read should be EOS |
| Block block2; |
| st = reader->read(&block2, &eos); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(eos); |
| |
| st = reader->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| TEST_F(SpillFileTest, OpenCanRetryAfterFailure) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/open_retry", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| { |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block = _create_int_block({7, 8, 9}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| const auto part_path = |
| std::filesystem::path(_spill_dir) / "spill" / "test_query" / "open_retry" / "0"; |
| const auto backup_path = |
| std::filesystem::path(_spill_dir) / "spill" / "test_query" / "open_retry" / "0.bak"; |
| |
| std::filesystem::rename(part_path, backup_path); |
| |
| auto reader = spill_file->create_reader(_runtime_state.get(), _profile.get()); |
| st = reader->open(); |
| ASSERT_FALSE(st.ok()); |
| |
| std::filesystem::rename(backup_path, part_path); |
| |
| st = reader->open(); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| |
| Block block; |
| bool eos = false; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| ASSERT_FALSE(eos); |
| ASSERT_EQ(block.rows(), 3); |
| |
| auto col = block.get_by_position(0).column; |
| ASSERT_EQ(col->get_int(0), 7); |
| ASSERT_EQ(col->get_int(1), 8); |
| ASSERT_EQ(col->get_int(2), 9); |
| } |
| |
| TEST_F(SpillFileTest, ReadMultipleBlocks) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/read_multi", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| const int num_blocks = 10; |
| const int rows_per_block = 100; |
| |
| // Write |
| { |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| for (int b = 0; b < num_blocks; ++b) { |
| std::vector<int32_t> data(rows_per_block); |
| std::iota(data.begin(), data.end(), b * rows_per_block); |
| auto block = _create_int_block(data); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // Read all blocks |
| auto reader = spill_file->create_reader(_runtime_state.get(), _profile.get()); |
| st = reader->open(); |
| ASSERT_TRUE(st.ok()); |
| |
| size_t total_rows = 0; |
| int block_count = 0; |
| bool eos = false; |
| while (!eos) { |
| Block block; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| if (!eos) { |
| total_rows += block.rows(); |
| ++block_count; |
| } |
| } |
| |
| ASSERT_EQ(total_rows, num_blocks * rows_per_block); |
| ASSERT_EQ(block_count, num_blocks); |
| |
| st = reader->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| TEST_F(SpillFileTest, ReadTwoColumnBlock) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/read_two_col", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| // Write |
| { |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block = _create_two_column_block({1, 2, 3, 4}, {100, 200, 300, 400}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // Read |
| auto reader = spill_file->create_reader(_runtime_state.get(), _profile.get()); |
| st = reader->open(); |
| ASSERT_TRUE(st.ok()); |
| |
| Block block; |
| bool eos = false; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_FALSE(eos); |
| ASSERT_EQ(block.rows(), 4); |
| ASSERT_EQ(block.columns(), 2); |
| |
| // Verify col1 |
| auto col1 = block.get_by_position(0).column; |
| ASSERT_EQ(col1->get_int(0), 1); |
| ASSERT_EQ(col1->get_int(3), 4); |
| |
| // Verify col2 |
| auto col2 = block.get_by_position(1).column; |
| ASSERT_EQ(col2->get_int(0), 100); |
| ASSERT_EQ(col2->get_int(3), 400); |
| |
| st = reader->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // ═══════════════════════════════════════════════════════════════════════ |
| // Roundtrip tests (write -> read -> verify) |
| // ═══════════════════════════════════════════════════════════════════════ |
| |
| TEST_F(SpillFileTest, RoundtripSingleBlock) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file( |
| "test_query/roundtrip_single", spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| std::vector<int32_t> original_data = {42, 7, 99, 1, 0, -5, 1000}; |
| |
| // Write |
| { |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block = _create_int_block(original_data); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // Read & verify |
| auto reader = spill_file->create_reader(_runtime_state.get(), _profile.get()); |
| st = reader->open(); |
| ASSERT_TRUE(st.ok()); |
| |
| Block block; |
| bool eos = false; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_EQ(block.rows(), original_data.size()); |
| |
| auto col = block.get_by_position(0).column; |
| for (size_t i = 0; i < original_data.size(); ++i) { |
| ASSERT_EQ(col->get_int(i), original_data[i]) << "mismatch at index " << i; |
| } |
| |
| st = reader->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| TEST_F(SpillFileTest, RoundtripMultipleBlocks) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file( |
| "test_query/roundtrip_multi", spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| std::vector<std::vector<int32_t>> all_data = { |
| {1, 2, 3}, |
| {10, 20, 30, 40}, |
| {100, 200}, |
| {-1, -2, -3, -4, -5}, |
| }; |
| |
| // Write |
| { |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| for (const auto& data : all_data) { |
| auto block = _create_int_block(data); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // Read & verify |
| auto reader = spill_file->create_reader(_runtime_state.get(), _profile.get()); |
| st = reader->open(); |
| ASSERT_TRUE(st.ok()); |
| |
| size_t block_idx = 0; |
| bool eos = false; |
| while (!eos && block_idx < all_data.size()) { |
| Block block; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()); |
| if (eos) break; |
| |
| ASSERT_EQ(block.rows(), all_data[block_idx].size()) |
| << "block " << block_idx << " row count mismatch"; |
| |
| auto col = block.get_by_position(0).column; |
| for (size_t i = 0; i < all_data[block_idx].size(); ++i) { |
| ASSERT_EQ(col->get_int(i), all_data[block_idx][i]) |
| << "mismatch at block " << block_idx << " row " << i; |
| } |
| ++block_idx; |
| } |
| |
| ASSERT_EQ(block_idx, all_data.size()); |
| |
| st = reader->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| TEST_F(SpillFileTest, RoundtripLargeData) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file( |
| "test_query/roundtrip_large", spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| const size_t row_count = 100000; |
| std::vector<int32_t> data(row_count); |
| std::iota(data.begin(), data.end(), 0); |
| |
| // Write |
| { |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block = _create_int_block(data); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // Read & verify |
| auto reader = spill_file->create_reader(_runtime_state.get(), _profile.get()); |
| st = reader->open(); |
| ASSERT_TRUE(st.ok()); |
| |
| Block block; |
| bool eos = false; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_EQ(block.rows(), row_count); |
| |
| auto col = block.get_by_position(0).column; |
| for (size_t i = 0; i < row_count; i += 1000) { |
| ASSERT_EQ(col->get_int(i), (int32_t)i) << "mismatch at index " << i; |
| } |
| |
| st = reader->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // ═══════════════════════════════════════════════════════════════════════ |
| // Part rotation tests |
| // ═══════════════════════════════════════════════════════════════════════ |
| |
| TEST_F(SpillFileTest, PartRotation) { |
| // Set a very small part size to force rotation |
| auto saved_part_size = config::spill_file_part_size_bytes; |
| config::spill_file_part_size_bytes = 1024; // 1KB per part |
| |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/rotation", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| const int num_blocks = 20; |
| |
| // Write many blocks to trigger multiple part rotations |
| { |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| for (int i = 0; i < num_blocks; ++i) { |
| std::vector<int32_t> data(100); |
| std::iota(data.begin(), data.end(), i * 100); |
| auto block = _create_int_block(data); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // Read back and verify all data across multiple parts |
| auto reader = spill_file->create_reader(_runtime_state.get(), _profile.get()); |
| st = reader->open(); |
| ASSERT_TRUE(st.ok()); |
| |
| size_t total_rows = 0; |
| int block_count = 0; |
| bool eos = false; |
| while (!eos) { |
| Block block; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()); |
| if (!eos) { |
| total_rows += block.rows(); |
| ++block_count; |
| } |
| } |
| |
| ASSERT_EQ(total_rows, num_blocks * 100); |
| ASSERT_EQ(block_count, num_blocks); |
| |
| st = reader->close(); |
| ASSERT_TRUE(st.ok()); |
| |
| config::spill_file_part_size_bytes = saved_part_size; |
| } |
| |
| TEST_F(SpillFileTest, PartRotationDataIntegrity) { |
| // Set a small part size to force rotation |
| auto saved_part_size = config::spill_file_part_size_bytes; |
| config::spill_file_part_size_bytes = 512; |
| |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file( |
| "test_query/rotation_integrity", spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| std::vector<std::vector<int32_t>> all_data; |
| const int num_blocks = 30; |
| |
| // Write |
| { |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| for (int i = 0; i < num_blocks; ++i) { |
| std::vector<int32_t> data(50); |
| std::iota(data.begin(), data.end(), i * 1000); |
| all_data.push_back(data); |
| auto block = _create_int_block(data); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // Read & verify data integrity across parts |
| auto reader = spill_file->create_reader(_runtime_state.get(), _profile.get()); |
| st = reader->open(); |
| ASSERT_TRUE(st.ok()); |
| |
| size_t block_idx = 0; |
| bool eos = false; |
| while (!eos) { |
| Block block; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()); |
| if (eos) break; |
| |
| ASSERT_LT(block_idx, all_data.size()); |
| ASSERT_EQ(block.rows(), all_data[block_idx].size()); |
| |
| auto col = block.get_by_position(0).column; |
| for (size_t i = 0; i < all_data[block_idx].size(); ++i) { |
| ASSERT_EQ(col->get_int(i), all_data[block_idx][i]) |
| << "data mismatch at block " << block_idx << " row " << i; |
| } |
| ++block_idx; |
| } |
| |
| ASSERT_EQ(block_idx, all_data.size()); |
| |
| st = reader->close(); |
| ASSERT_TRUE(st.ok()); |
| |
| config::spill_file_part_size_bytes = saved_part_size; |
| } |
| |
| // ═══════════════════════════════════════════════════════════════════════ |
| // Seek tests |
| // ═══════════════════════════════════════════════════════════════════════ |
| |
| TEST_F(SpillFileTest, SeekToBlock) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/seek", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| const int num_blocks = 5; |
| |
| // Write |
| { |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| for (int i = 0; i < num_blocks; ++i) { |
| auto block = _create_int_block({i * 10, i * 10 + 1, i * 10 + 2}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // Seek to block 2 (0-based) and read |
| auto reader = spill_file->create_reader(_runtime_state.get(), _profile.get()); |
| st = reader->open(); |
| ASSERT_TRUE(st.ok()); |
| |
| st = reader->seek(2); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| |
| Block block; |
| bool eos = false; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_FALSE(eos); |
| ASSERT_EQ(block.rows(), 3); |
| |
| auto col = block.get_by_position(0).column; |
| ASSERT_EQ(col->get_int(0), 20); |
| ASSERT_EQ(col->get_int(1), 21); |
| ASSERT_EQ(col->get_int(2), 22); |
| |
| st = reader->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| TEST_F(SpillFileTest, SeekBeyondEnd) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/seek_beyond", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| // Write 3 blocks |
| { |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| for (int i = 0; i < 3; ++i) { |
| auto block = _create_int_block({i}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| auto reader = spill_file->create_reader(_runtime_state.get(), _profile.get()); |
| st = reader->open(); |
| ASSERT_TRUE(st.ok()); |
| |
| // Seek beyond the end |
| st = reader->seek(100); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| |
| Block block; |
| bool eos = false; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(eos); |
| |
| st = reader->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // ═══════════════════════════════════════════════════════════════════════ |
| // SpillFile GC/lifecycle tests |
| // ═══════════════════════════════════════════════════════════════════════ |
| |
| TEST_F(SpillFileTest, GCCleansUpFiles) { |
| std::string spill_file_dir; |
| |
| { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/gc_test", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block = _create_int_block({1, 2, 3}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| |
| // Remember the spill directory path |
| spill_file_dir = _data_dir_ptr->get_spill_data_path() + "/test_query/gc_test"; |
| |
| // Verify directory exists |
| bool exists = false; |
| st = io::global_local_filesystem()->exists(spill_file_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(exists); |
| |
| // spill_file goes out of scope here, destructor calls gc() |
| } |
| |
| // After SpillFile is destroyed, the directory should be cleaned up |
| bool exists = false; |
| auto st = io::global_local_filesystem()->exists(spill_file_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_FALSE(exists); |
| } |
| |
| TEST_F(SpillFileTest, QueryContextDeletesEmptySpillDirectory) { |
| ExecEnv::GetInstance()->spill_file_mgr()->stop(); |
| |
| TUniqueId query_id; |
| query_id.hi = 1; |
| query_id.lo = 2; |
| auto query_id_str = print_id(query_id); |
| auto query_ctx = MockQueryContext::create(query_id); |
| |
| auto query_dir = _data_dir_ptr->get_spill_data_path(query_id_str); |
| const auto gc_subdirectories_before = _gc_subdirectories(_data_dir_ptr); |
| _write_and_release_spill_file(query_id, query_ctx.get(), _data_dir_ptr, "query_context_gc"); |
| |
| bool exists = false; |
| auto st = io::global_local_filesystem()->exists(query_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(exists); |
| |
| query_ctx.reset(); |
| |
| st = io::global_local_filesystem()->exists(query_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_FALSE(exists); |
| st = io::global_local_filesystem()->exists(_data_dir_ptr->get_spill_data_path(), &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(exists); |
| EXPECT_EQ(_gc_subdirectories(_data_dir_ptr), gc_subdirectories_before); |
| } |
| |
| TEST_F(SpillFileTest, QueryContextCleansUpNestedSpillDirectory) { |
| TUniqueId query_id; |
| query_id.hi = 3; |
| query_id.lo = 4; |
| auto query_id_str = print_id(query_id); |
| auto query_ctx = MockQueryContext::create(query_id); |
| |
| auto query_dir = _data_dir_ptr->get_spill_data_path(query_id_str); |
| auto nested_dir = query_dir + "/nested"; |
| _write_and_release_spill_file(query_id, query_ctx.get(), _data_dir_ptr, |
| "nested/query_context_gc"); |
| |
| bool exists = false; |
| auto st = io::global_local_filesystem()->exists(query_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(exists); |
| st = io::global_local_filesystem()->exists(nested_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(exists); |
| |
| query_ctx.reset(); |
| |
| st = io::global_local_filesystem()->exists(query_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_FALSE(exists); |
| } |
| |
| TEST_F(SpillFileTest, QueryContextDeletesResidualSpillDirectory) { |
| ExecEnv::GetInstance()->spill_file_mgr()->stop(); |
| |
| TUniqueId query_id; |
| query_id.hi = 5; |
| query_id.lo = 6; |
| auto query_id_str = print_id(query_id); |
| auto query_ctx = MockQueryContext::create(query_id); |
| |
| auto query_dir = _data_dir_ptr->get_spill_data_path(query_id_str); |
| const auto gc_subdirectories_before = _gc_subdirectories(_data_dir_ptr); |
| _write_and_release_spill_file(query_id, query_ctx.get(), _data_dir_ptr, "query_context_gc"); |
| |
| auto residual_file = query_dir + "/residual/temporary-data"; |
| _create_residual_file(residual_file); |
| |
| bool exists = false; |
| auto st = io::global_local_filesystem()->exists(residual_file, &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(exists); |
| |
| query_ctx.reset(); |
| |
| st = io::global_local_filesystem()->exists(query_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_FALSE(exists); |
| |
| EXPECT_EQ(_gc_subdirectories(_data_dir_ptr), gc_subdirectories_before); |
| } |
| |
| TEST_F(SpillFileTest, QueryContextCleansUpAllTouchedSpillDirectories) { |
| TUniqueId query_id; |
| query_id.hi = 9; |
| query_id.lo = 10; |
| auto query_id_str = print_id(query_id); |
| auto query_ctx = MockQueryContext::create(query_id); |
| |
| auto first_query_dir = _data_dir_ptr->get_spill_data_path(query_id_str); |
| auto second_query_dir = _second_data_dir_ptr->get_spill_data_path(query_id_str); |
| const auto first_gc_subdirectories_before = _gc_subdirectories(_data_dir_ptr); |
| const auto second_gc_subdirectories_before = _gc_subdirectories(_second_data_dir_ptr); |
| _write_and_release_spill_file(query_id, query_ctx.get(), _data_dir_ptr, "first"); |
| _write_and_release_spill_file(query_id, query_ctx.get(), _second_data_dir_ptr, "second"); |
| |
| bool first_exists = false; |
| bool second_exists = false; |
| auto st = io::global_local_filesystem()->exists(first_query_dir, &first_exists); |
| ASSERT_TRUE(st.ok()); |
| st = io::global_local_filesystem()->exists(second_query_dir, &second_exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(first_exists); |
| ASSERT_TRUE(second_exists); |
| |
| query_ctx.reset(); |
| |
| st = io::global_local_filesystem()->exists(first_query_dir, &first_exists); |
| ASSERT_TRUE(st.ok()); |
| st = io::global_local_filesystem()->exists(second_query_dir, &second_exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_FALSE(first_exists); |
| ASSERT_FALSE(second_exists); |
| EXPECT_EQ(_gc_subdirectories(_data_dir_ptr), first_gc_subdirectories_before); |
| EXPECT_EQ(_gc_subdirectories(_second_data_dir_ptr), second_gc_subdirectories_before); |
| } |
| |
| TEST_F(SpillFileTest, QueryContextContinuesCleanupAfterRootFailure) { |
| ExecEnv::GetInstance()->spill_file_mgr()->stop(); |
| |
| TUniqueId query_id; |
| query_id.hi = 11; |
| query_id.lo = 12; |
| auto query_id_str = print_id(query_id); |
| auto query_ctx = MockQueryContext::create(query_id); |
| |
| auto first_query_dir = _data_dir_ptr->get_spill_data_path(query_id_str); |
| auto second_query_dir = _second_data_dir_ptr->get_spill_data_path(query_id_str); |
| const auto first_gc_subdirectories_before = _gc_subdirectories(_data_dir_ptr); |
| const auto second_gc_subdirectories_before = _gc_subdirectories(_second_data_dir_ptr); |
| _write_and_release_spill_file(query_id, query_ctx.get(), _data_dir_ptr, "first"); |
| _write_and_release_spill_file(query_id, query_ctx.get(), _second_data_dir_ptr, "second"); |
| _create_residual_file(first_query_dir + "/residual/temporary-data"); |
| _create_residual_file(second_query_dir + "/residual/temporary-data"); |
| |
| const auto live_query_dir = _data_dir_ptr->get_spill_data_path("live-query"); |
| const auto live_query_file = live_query_dir + "/sentinel"; |
| _create_residual_file(live_query_file); |
| |
| bool first_exists = false; |
| bool second_exists = false; |
| auto st = io::global_local_filesystem()->exists(first_query_dir, &first_exists); |
| ASSERT_TRUE(st.ok()); |
| st = io::global_local_filesystem()->exists(second_query_dir, &second_exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(first_exists); |
| ASSERT_TRUE(second_exists); |
| |
| const bool previous_enable_debug_points = config::enable_debug_points; |
| constexpr auto debug_point_name = |
| "fault_inject::spill_file_manager::delete_query_spill_directory"; |
| Defer restore_debug_point([&] { |
| DebugPoints::instance()->remove(debug_point_name); |
| config::enable_debug_points = previous_enable_debug_points; |
| }); |
| auto debug_point = std::make_shared<DebugPoint>(); |
| debug_point->execute_limit = 1; |
| config::enable_debug_points = true; |
| DebugPoints::instance()->add(debug_point_name, debug_point); |
| |
| query_ctx.reset(); |
| |
| st = io::global_local_filesystem()->exists(first_query_dir, &first_exists); |
| ASSERT_TRUE(st.ok()); |
| st = io::global_local_filesystem()->exists(second_query_dir, &second_exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_NE(first_exists, second_exists); |
| ASSERT_EQ(debug_point->execute_num.load(), 2); |
| |
| ExecEnv::GetInstance()->spill_file_mgr()->gc(10000); |
| |
| st = io::global_local_filesystem()->exists(first_query_dir, &first_exists); |
| ASSERT_TRUE(st.ok()); |
| st = io::global_local_filesystem()->exists(second_query_dir, &second_exists); |
| ASSERT_TRUE(st.ok()); |
| EXPECT_FALSE(first_exists); |
| EXPECT_FALSE(second_exists); |
| |
| bool live_query_exists = false; |
| st = io::global_local_filesystem()->exists(live_query_file, &live_query_exists); |
| ASSERT_TRUE(st.ok()); |
| EXPECT_TRUE(live_query_exists); |
| |
| EXPECT_EQ(_gc_subdirectories(_data_dir_ptr), first_gc_subdirectories_before); |
| EXPECT_EQ(_gc_subdirectories(_second_data_dir_ptr), second_gc_subdirectories_before); |
| } |
| |
| TEST_F(SpillFileTest, QueryContextRetriesSpillDirectoryDeletionUntilSuccess) { |
| ExecEnv::GetInstance()->spill_file_mgr()->stop(); |
| |
| TUniqueId query_id; |
| query_id.hi = 15; |
| query_id.lo = 16; |
| auto query_id_str = print_id(query_id); |
| auto query_ctx = MockQueryContext::create(query_id); |
| |
| auto query_dir = _data_dir_ptr->get_spill_data_path(query_id_str); |
| const auto gc_subdirectories_before = _gc_subdirectories(_data_dir_ptr); |
| _write_and_release_spill_file(query_id, query_ctx.get(), _data_dir_ptr, "retry_cleanup"); |
| _create_residual_file(query_dir + "/residual/temporary-data"); |
| |
| const bool previous_enable_debug_points = config::enable_debug_points; |
| constexpr auto debug_point_name = |
| "fault_inject::spill_file_manager::delete_query_spill_directory"; |
| Defer restore_debug_point([&] { |
| DebugPoints::instance()->remove(debug_point_name); |
| config::enable_debug_points = previous_enable_debug_points; |
| }); |
| auto debug_point = std::make_shared<DebugPoint>(); |
| debug_point->execute_limit = 5; |
| config::enable_debug_points = true; |
| DebugPoints::instance()->add(debug_point_name, debug_point); |
| |
| query_ctx.reset(); |
| |
| bool exists = false; |
| auto st = io::global_local_filesystem()->exists(query_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(exists); |
| |
| for (int i = 0; i < 4; ++i) { |
| ExecEnv::GetInstance()->spill_file_mgr()->gc(10000); |
| st = io::global_local_filesystem()->exists(query_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(exists); |
| } |
| |
| ExecEnv::GetInstance()->spill_file_mgr()->gc(10000); |
| st = io::global_local_filesystem()->exists(query_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| EXPECT_FALSE(exists); |
| EXPECT_EQ(debug_point->execute_num.load(), 6); |
| EXPECT_EQ(_gc_subdirectories(_data_dir_ptr), gc_subdirectories_before); |
| } |
| |
| TEST_F(SpillFileTest, RetryPreservesDirectoryQueuedAfterPendingDrain) { |
| ExecEnv::GetInstance()->spill_file_mgr()->stop(); |
| |
| TUniqueId first_query_id; |
| first_query_id.hi = 17; |
| first_query_id.lo = 18; |
| auto first_query_ctx = MockQueryContext::create(first_query_id); |
| auto first_query_dir = _data_dir_ptr->get_spill_data_path(print_id(first_query_id)); |
| _write_and_release_spill_file(first_query_id, first_query_ctx.get(), _data_dir_ptr, |
| "first_retry_cleanup"); |
| _create_residual_file(first_query_dir + "/residual/temporary-data"); |
| |
| TUniqueId second_query_id; |
| second_query_id.hi = 19; |
| second_query_id.lo = 20; |
| auto second_query_ctx = MockQueryContext::create(second_query_id); |
| auto second_query_dir = _data_dir_ptr->get_spill_data_path(print_id(second_query_id)); |
| _write_and_release_spill_file(second_query_id, second_query_ctx.get(), _data_dir_ptr, |
| "second_retry_cleanup"); |
| _create_residual_file(second_query_dir + "/residual/temporary-data"); |
| |
| const bool previous_enable_debug_points = config::enable_debug_points; |
| constexpr auto delete_debug_point_name = |
| "fault_inject::spill_file_manager::delete_query_spill_directory"; |
| constexpr auto after_drain_debug_point_name = |
| "fault_inject::spill_file_manager::retry_pending_query_spill_directories_after_drain"; |
| Defer restore_debug_points([&] { |
| DebugPoints::instance()->remove(after_drain_debug_point_name); |
| DebugPoints::instance()->remove(delete_debug_point_name); |
| config::enable_debug_points = previous_enable_debug_points; |
| }); |
| config::enable_debug_points = true; |
| DebugPoints::instance()->add(delete_debug_point_name); |
| |
| first_query_ctx.reset(); |
| |
| auto after_drain_debug_point = std::make_shared<DebugPoint>(); |
| after_drain_debug_point->execute_limit = 1; |
| after_drain_debug_point->callback = std::function<void()>([&]() { second_query_ctx.reset(); }); |
| DebugPoints::instance()->add(after_drain_debug_point_name, after_drain_debug_point); |
| |
| ExecEnv::GetInstance()->spill_file_mgr()->gc(10000); |
| |
| bool first_exists = false; |
| auto st = io::global_local_filesystem()->exists(first_query_dir, &first_exists); |
| ASSERT_TRUE(st.ok()); |
| bool second_exists = false; |
| st = io::global_local_filesystem()->exists(second_query_dir, &second_exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(first_exists); |
| ASSERT_TRUE(second_exists); |
| ASSERT_EQ(after_drain_debug_point->execute_num.load(), 1); |
| |
| DebugPoints::instance()->remove(after_drain_debug_point_name); |
| DebugPoints::instance()->remove(delete_debug_point_name); |
| ExecEnv::GetInstance()->spill_file_mgr()->gc(10000); |
| |
| st = io::global_local_filesystem()->exists(first_query_dir, &first_exists); |
| ASSERT_TRUE(st.ok()); |
| st = io::global_local_filesystem()->exists(second_query_dir, &second_exists); |
| ASSERT_TRUE(st.ok()); |
| EXPECT_FALSE(first_exists); |
| EXPECT_FALSE(second_exists); |
| } |
| |
| TEST_F(SpillFileTest, FinalCloseReleasesRerunnableQueryContextAndDeletesSpillDirectory) { |
| ExecEnv::GetInstance()->spill_file_mgr()->stop(); |
| |
| auto* exec_env = ExecEnv::GetInstance(); |
| auto* previous_fragment_mgr = exec_env->_fragment_mgr; |
| auto* fragment_mgr = new FragmentMgr(exec_env); |
| exec_env->_fragment_mgr = fragment_mgr; |
| |
| TUniqueId query_id; |
| query_id.hi = 13; |
| query_id.lo = 14; |
| constexpr int first_fragment_id = 1; |
| constexpr int second_fragment_id = 2; |
| Defer restore_fragment_mgr([&] { |
| fragment_mgr->remove_query_context(query_id); |
| fragment_mgr->remove_pipeline_context({query_id, first_fragment_id}); |
| fragment_mgr->remove_pipeline_context({query_id, second_fragment_id}); |
| fragment_mgr->stop(); |
| delete fragment_mgr; |
| exec_env->_fragment_mgr = previous_fragment_mgr; |
| }); |
| |
| auto query_ctx = MockQueryContext::create(query_id); |
| std::weak_ptr<QueryContext> weak_query_ctx = query_ctx; |
| const auto query_id_str = print_id(query_id); |
| const auto query_dir = _data_dir_ptr->get_spill_data_path(query_id_str); |
| const auto gc_subdirectories_before = _gc_subdirectories(_data_dir_ptr); |
| _write_and_release_spill_file(query_id, query_ctx.get(), _data_dir_ptr, "recursive_cte"); |
| _create_residual_file(query_dir + "/residual/temporary-data"); |
| |
| auto create_fragment_context = [&](int fragment_id) { |
| TPipelineFragmentParams params; |
| params.__set_query_id(query_id); |
| params.__set_fragment_id(fragment_id); |
| params.__set_need_notify_close(true); |
| auto context = std::make_shared<PipelineFragmentContext>( |
| query_id, params, query_ctx, exec_env, [](RuntimeState*, Status*) {}); |
| fragment_mgr->_pipeline_map.insert({query_id, fragment_id}, context); |
| g_fragment_executing_count << 1; |
| query_ctx->set_pipeline_context(fragment_id, context); |
| { |
| std::lock_guard lock(fragment_mgr->_rerunnable_params_lock); |
| auto& info = fragment_mgr->_rerunnable_params_map[{query_id, fragment_id}]; |
| info.params = params; |
| info.query_ctx = query_ctx; |
| } |
| return context; |
| }; |
| |
| auto first_fragment_ctx = create_fragment_context(first_fragment_id); |
| auto second_fragment_ctx = create_fragment_context(second_fragment_id); |
| query_ctx.reset(); |
| first_fragment_ctx.reset(); |
| second_fragment_ctx.reset(); |
| |
| ASSERT_FALSE(weak_query_ctx.expired()); |
| auto st = fragment_mgr->rerun_fragment({}, query_id, first_fragment_id, |
| PRerunFragmentParams::FINAL_CLOSE); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| EXPECT_EQ(fragment_mgr->_pipeline_map.num_items(), 1); |
| { |
| std::lock_guard lock(fragment_mgr->_rerunnable_params_lock); |
| EXPECT_FALSE(fragment_mgr->_rerunnable_params_map.contains({query_id, first_fragment_id})); |
| EXPECT_TRUE(fragment_mgr->_rerunnable_params_map.contains({query_id, second_fragment_id})); |
| } |
| EXPECT_FALSE(weak_query_ctx.expired()); |
| |
| bool exists = false; |
| st = io::global_local_filesystem()->exists(query_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| EXPECT_TRUE(exists); |
| |
| st = fragment_mgr->rerun_fragment({}, query_id, second_fragment_id, |
| PRerunFragmentParams::FINAL_CLOSE); |
| ASSERT_TRUE(st.ok()) << st.to_string(); |
| EXPECT_EQ(fragment_mgr->_pipeline_map.num_items(), 0); |
| { |
| std::lock_guard lock(fragment_mgr->_rerunnable_params_lock); |
| EXPECT_TRUE(fragment_mgr->_rerunnable_params_map.empty()); |
| } |
| EXPECT_TRUE(weak_query_ctx.expired()); |
| |
| st = io::global_local_filesystem()->exists(query_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| EXPECT_FALSE(exists); |
| |
| EXPECT_EQ(_gc_subdirectories(_data_dir_ptr), gc_subdirectories_before); |
| } |
| |
| TEST_F(SpillFileTest, QueryContextSkipsCleanupWithoutSpill) { |
| TUniqueId query_id; |
| query_id.hi = 7; |
| query_id.lo = 8; |
| auto query_id_str = print_id(query_id); |
| auto query_ctx = MockQueryContext::create(query_id); |
| auto query_dir = _data_dir_ptr->get_spill_data_path(query_id_str); |
| |
| // No spill root was recorded for this query, so teardown must leave this untracked directory. |
| auto st = io::global_local_filesystem()->create_directory(query_dir, false); |
| ASSERT_TRUE(st.ok()); |
| |
| query_ctx.reset(); |
| |
| bool exists = false; |
| st = io::global_local_filesystem()->exists(query_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(exists); |
| } |
| |
| TEST_F(SpillFileTest, DeleteSpillFileThroughManagerSynchronously) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/mgr_delete", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block = _create_int_block({1, 2, 3}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| |
| auto spill_file_dir = _data_dir_ptr->get_spill_data_path("test_query/mgr_delete"); |
| bool exists = false; |
| st = io::global_local_filesystem()->exists(spill_file_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_TRUE(exists); |
| |
| ExecEnv::GetInstance()->spill_file_mgr()->delete_spill_file(spill_file); |
| |
| st = io::global_local_filesystem()->exists(spill_file_dir, &exists); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_FALSE(exists); |
| } |
| |
| // ═══════════════════════════════════════════════════════════════════════ |
| // SpillFileManager tests |
| // ═══════════════════════════════════════════════════════════════════════ |
| |
| TEST_F(SpillFileTest, ManagerNextId) { |
| auto id1 = ExecEnv::GetInstance()->spill_file_mgr()->next_id(); |
| auto id2 = ExecEnv::GetInstance()->spill_file_mgr()->next_id(); |
| auto id3 = ExecEnv::GetInstance()->spill_file_mgr()->next_id(); |
| |
| ASSERT_EQ(id2, id1 + 1); |
| ASSERT_EQ(id3, id2 + 1); |
| } |
| |
| TEST_F(SpillFileTest, ManagerCreateMultipleFiles) { |
| const int num_files = 5; |
| std::vector<SpillFileSPtr> files; |
| |
| for (int i = 0; i < num_files; ++i) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file( |
| fmt::format("test_query/multi_{}", i), spill_file); |
| ASSERT_TRUE(st.ok()) << "create file " << i << " failed: " << st.to_string(); |
| files.push_back(spill_file); |
| } |
| |
| // Write and close each file |
| for (int i = 0; i < num_files; ++i) { |
| SpillFileWriterSPtr writer; |
| auto st = files[i]->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block = _create_int_block({i * 100}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // Read each file and verify |
| for (int i = 0; i < num_files; ++i) { |
| auto reader = files[i]->create_reader(_runtime_state.get(), _profile.get()); |
| auto st = reader->open(); |
| ASSERT_TRUE(st.ok()); |
| |
| Block block; |
| bool eos = false; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_EQ(block.rows(), 1); |
| |
| auto col = block.get_by_position(0).column; |
| ASSERT_EQ(col->get_int(0), i * 100); |
| |
| st = reader->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| } |
| |
| // ═══════════════════════════════════════════════════════════════════════ |
| // Profile counter tests |
| // ═══════════════════════════════════════════════════════════════════════ |
| |
| TEST_F(SpillFileTest, WriteCounters) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/counters", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block = _create_int_block({1, 2, 3, 4, 5}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block2 = _create_int_block({10, 20, 30}); |
| st = writer->write_block(_runtime_state.get(), block2); |
| ASSERT_TRUE(st.ok()); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| |
| auto* write_rows = _custom_profile->get_counter("SpillWriteRows"); |
| ASSERT_TRUE(write_rows != nullptr); |
| ASSERT_EQ(write_rows->value(), 8); |
| |
| auto* write_blocks = _custom_profile->get_counter("SpillWriteBlockCount"); |
| ASSERT_TRUE(write_blocks != nullptr); |
| ASSERT_EQ(write_blocks->value(), 2); |
| |
| auto* write_bytes = _custom_profile->get_counter("SpillWriteFileBytes"); |
| ASSERT_TRUE(write_bytes != nullptr); |
| ASSERT_GT(write_bytes->value(), 0); |
| } |
| |
| TEST_F(SpillFileTest, ReadCounters) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file( |
| "test_query/read_counters", spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| // Write |
| { |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block = _create_int_block({1, 2, 3, 4, 5}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // Read |
| auto reader = spill_file->create_reader(_runtime_state.get(), _profile.get()); |
| st = reader->open(); |
| ASSERT_TRUE(st.ok()); |
| |
| Block block; |
| bool eos = false; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()); |
| |
| st = reader->close(); |
| ASSERT_TRUE(st.ok()); |
| |
| auto* read_blocks = _custom_profile->get_counter("SpillReadBlockCount"); |
| ASSERT_TRUE(read_blocks != nullptr); |
| ASSERT_EQ(read_blocks->value(), 1); |
| |
| auto* read_rows = _custom_profile->get_counter("SpillReadRows"); |
| ASSERT_TRUE(read_rows != nullptr); |
| ASSERT_EQ(read_rows->value(), 5); |
| |
| auto* read_file_size = _custom_profile->get_counter("SpillReadFileBytes"); |
| ASSERT_TRUE(read_file_size != nullptr); |
| ASSERT_GT(read_file_size->value(), 0); |
| } |
| |
| // Regression test: SpillFileReader used to look up the deserialize timer under a |
| // misspelled name ("SpillReadDerializeBlockTime"), so get_counter() returned null and |
| // SCOPED_TIMER silently recorded nothing. The counter stayed at 0 in every profile. |
| TEST_F(SpillFileTest, ReadDeserializeTimerIsRecorded) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file( |
| "test_query/read_deserialize_timer", spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| { |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| auto block = _create_int_block({1, 2, 3, 4, 5}); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| } |
| |
| // The timer is registered by SpillReadCounters::init under the canonical name and |
| // must still be untouched before any read happens. |
| auto* deserialize_timer = |
| _custom_profile->get_counter(profile::SPILL_READ_DESERIALIZE_BLOCK_TIME); |
| ASSERT_TRUE(deserialize_timer != nullptr) |
| << "counter name drifted from " << profile::SPILL_READ_DESERIALIZE_BLOCK_TIME; |
| ASSERT_EQ(deserialize_timer->value(), 0); |
| |
| auto reader = spill_file->create_reader(_runtime_state.get(), _profile.get()); |
| st = reader->open(); |
| ASSERT_TRUE(st.ok()); |
| |
| Block block; |
| bool eos = false; |
| st = reader->read(&block, &eos); |
| ASSERT_TRUE(st.ok()); |
| ASSERT_EQ(block.rows(), 5); |
| |
| st = reader->close(); |
| ASSERT_TRUE(st.ok()); |
| |
| // Deserializing a real block must land on the canonical counter. This is 0 whenever |
| // the reader's lookup name does not match what the operator registered. |
| ASSERT_GT(deserialize_timer->value(), 0); |
| |
| // The misspelled name must not exist: if it reappears, some caller registered it and |
| // the two spellings will drift apart again. |
| ASSERT_TRUE(_custom_profile->get_counter("SpillReadDerializeBlockTime") == nullptr); |
| } |
| |
| // ═══════════════════════════════════════════════════════════════════════ |
| // SpillDataDir tests |
| // ═══════════════════════════════════════════════════════════════════════ |
| |
| TEST_F(SpillFileTest, DataDirCapacityTracking) { |
| SpillFileSPtr spill_file; |
| auto st = ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/capacity", |
| spill_file); |
| ASSERT_TRUE(st.ok()); |
| |
| auto initial_bytes = _data_dir_ptr->get_spill_data_bytes(); |
| |
| SpillFileWriterSPtr writer; |
| st = spill_file->create_writer(_runtime_state.get(), _profile.get(), writer); |
| ASSERT_TRUE(st.ok()); |
| |
| // Write a block to increase usage |
| std::vector<int32_t> data(1000); |
| std::iota(data.begin(), data.end(), 0); |
| auto block = _create_int_block(data); |
| st = writer->write_block(_runtime_state.get(), block); |
| ASSERT_TRUE(st.ok()); |
| |
| st = writer->close(); |
| ASSERT_TRUE(st.ok()); |
| |
| auto after_write_bytes = _data_dir_ptr->get_spill_data_bytes(); |
| ASSERT_GT(after_write_bytes, initial_bytes); |
| } |
| |
| } // namespace doris::vectorized |