blob: 444bd0e740ae6729f52e6e75d98d96001fe4b0f4 [file]
// 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 <gtest/gtest.h>
#include <atomic>
#include <thread>
#include <vector>
#include "io/io_common.h"
#include "storage/compaction/cumulative_compaction_policy.h"
#include "storage/storage_engine.h"
#include "storage/tablet/tablet.h"
#include "storage/tablet/tablet_meta.h"
#include "util/uid_util.h"
namespace doris {
class CompactionSampleInfoTest : public testing::Test {
protected:
void SetUp() override {
_engine = std::make_unique<StorageEngine>(EngineOptions {});
TabletMetaSharedPtr tablet_meta;
tablet_meta.reset(new TabletMeta(1, 2, 15673, 15674, 4, 5, TTabletSchema(), 6, {{7, 8}},
UniqueId(9, 10), TTabletType::TABLET_TYPE_DISK,
TCompressionType::LZ4F));
_tablet = std::make_shared<Tablet>(*_engine, tablet_meta, nullptr,
CUMULATIVE_SIZE_BASED_POLICY);
}
std::unique_ptr<StorageEngine> _engine;
std::shared_ptr<Tablet> _tablet;
};
// Test that get_sample_infos returns the correct reference for each compaction type
TEST_F(CompactionSampleInfoTest, GetSampleInfosReturnsCorrectReference) {
auto& cumu_infos = _tablet->get_sample_infos(ReaderType::READER_CUMULATIVE_COMPACTION);
auto& base_infos = _tablet->get_sample_infos(ReaderType::READER_BASE_COMPACTION);
auto& full_infos = _tablet->get_sample_infos(ReaderType::READER_FULL_COMPACTION);
// Verify they are different references
EXPECT_NE(&cumu_infos, &base_infos);
EXPECT_NE(&base_infos, &full_infos);
EXPECT_NE(&cumu_infos, &full_infos);
// Verify they match the expected member variables
EXPECT_EQ(&cumu_infos, &_tablet->cumu_sample_infos);
EXPECT_EQ(&base_infos, &_tablet->base_sample_infos);
EXPECT_EQ(&full_infos, &_tablet->full_sample_infos);
}
// Test that get_sample_info_lock returns the correct reference for each compaction type
TEST_F(CompactionSampleInfoTest, GetSampleInfoLockReturnsCorrectReference) {
auto& cumu_lock = _tablet->get_sample_info_lock(ReaderType::READER_CUMULATIVE_COMPACTION);
auto& base_lock = _tablet->get_sample_info_lock(ReaderType::READER_BASE_COMPACTION);
auto& full_lock = _tablet->get_sample_info_lock(ReaderType::READER_FULL_COMPACTION);
// Verify they are different references
EXPECT_NE(&cumu_lock, &base_lock);
EXPECT_NE(&base_lock, &full_lock);
EXPECT_NE(&cumu_lock, &full_lock);
// Verify they match the expected member variables
EXPECT_EQ(&cumu_lock, &_tablet->cumu_sample_info_lock);
EXPECT_EQ(&base_lock, &_tablet->base_sample_info_lock);
EXPECT_EQ(&full_lock, &_tablet->full_sample_info_lock);
}
// Test that default reader types fall back to base_sample_infos
TEST_F(CompactionSampleInfoTest, DefaultReaderTypeFallsBackToBase) {
auto& query_infos = _tablet->get_sample_infos(ReaderType::READER_QUERY);
auto& alter_infos = _tablet->get_sample_infos(ReaderType::READER_ALTER_TABLE);
auto& cold_infos = _tablet->get_sample_infos(ReaderType::READER_COLD_DATA_COMPACTION);
// All should fall back to base_sample_infos
EXPECT_EQ(&query_infos, &_tablet->base_sample_infos);
EXPECT_EQ(&alter_infos, &_tablet->base_sample_infos);
EXPECT_EQ(&cold_infos, &_tablet->base_sample_infos);
}
// Test that different compaction types can have different sample_infos sizes
TEST_F(CompactionSampleInfoTest, IndependentSampleInfosSizes) {
auto& cumu_infos = _tablet->get_sample_infos(ReaderType::READER_CUMULATIVE_COMPACTION);
auto& base_infos = _tablet->get_sample_infos(ReaderType::READER_BASE_COMPACTION);
auto& full_infos = _tablet->get_sample_infos(ReaderType::READER_FULL_COMPACTION);
// Resize each to different sizes
cumu_infos.resize(3);
base_infos.resize(5);
full_infos.resize(7);
// Verify sizes are independent
EXPECT_EQ(cumu_infos.size(), 3);
EXPECT_EQ(base_infos.size(), 5);
EXPECT_EQ(full_infos.size(), 7);
// Resize one doesn't affect others
cumu_infos.resize(10);
EXPECT_EQ(cumu_infos.size(), 10);
EXPECT_EQ(base_infos.size(), 5);
EXPECT_EQ(full_infos.size(), 7);
}
// Test concurrent access to different sample_infos doesn't cause issues
TEST_F(CompactionSampleInfoTest, ConcurrentAccessToDifferentTypes) {
std::atomic<bool> has_error {false};
std::atomic<int> completed_threads {0};
constexpr int kIterations = 1000;
// Thread simulating cumulative compaction
auto cumu_thread = [this, &has_error, &completed_threads]() {
try {
for (int i = 0; i < kIterations; ++i) {
auto& lock =
_tablet->get_sample_info_lock(ReaderType::READER_CUMULATIVE_COMPACTION);
auto& infos = _tablet->get_sample_infos(ReaderType::READER_CUMULATIVE_COMPACTION);
std::unique_lock<std::mutex> guard(lock);
infos.resize((i % 5) + 1);
for (size_t j = 0; j < infos.size(); ++j) {
infos[j].group_data_size = i;
infos[j].bytes = i * 100;
infos[j].rows = i * 10;
}
}
} catch (...) {
has_error = true;
}
completed_threads++;
};
// Thread simulating base compaction
auto base_thread = [this, &has_error, &completed_threads]() {
try {
for (int i = 0; i < kIterations; ++i) {
auto& lock = _tablet->get_sample_info_lock(ReaderType::READER_BASE_COMPACTION);
auto& infos = _tablet->get_sample_infos(ReaderType::READER_BASE_COMPACTION);
std::unique_lock<std::mutex> guard(lock);
infos.resize((i % 7) + 1);
for (size_t j = 0; j < infos.size(); ++j) {
infos[j].group_data_size = i * 2;
infos[j].bytes = i * 200;
infos[j].rows = i * 20;
}
}
} catch (...) {
has_error = true;
}
completed_threads++;
};
// Thread simulating full compaction
auto full_thread = [this, &has_error, &completed_threads]() {
try {
for (int i = 0; i < kIterations; ++i) {
auto& lock = _tablet->get_sample_info_lock(ReaderType::READER_FULL_COMPACTION);
auto& infos = _tablet->get_sample_infos(ReaderType::READER_FULL_COMPACTION);
std::unique_lock<std::mutex> guard(lock);
infos.resize((i % 3) + 1);
for (size_t j = 0; j < infos.size(); ++j) {
infos[j].group_data_size = i * 3;
infos[j].bytes = i * 300;
infos[j].rows = i * 30;
}
}
} catch (...) {
has_error = true;
}
completed_threads++;
};
// Run all threads concurrently
std::thread t1(cumu_thread);
std::thread t2(base_thread);
std::thread t3(full_thread);
t1.join();
t2.join();
t3.join();
EXPECT_FALSE(has_error);
EXPECT_EQ(completed_threads, 3);
}
// Test that simulates the race condition scenario that caused the original crash
// This test verifies that concurrent resize and access on different compaction types
// don't interfere with each other
TEST_F(CompactionSampleInfoTest, SimulateOriginalCrashScenario) {
std::atomic<bool> has_crash {false};
std::atomic<int> completed_iterations {0};
constexpr int kIterations = 500;
// This simulates the scenario where:
// - Thread A (base compaction) resizes to 5 groups and starts processing
// - Thread B (cumu compaction) resizes to 3 groups
// - Thread A tries to access group index 4 - with the fix, this should be safe
// because they use different sample_infos
auto base_compaction_thread = [this, &has_crash, &completed_iterations]() {
for (int iter = 0; iter < kIterations && !has_crash; ++iter) {
auto& lock = _tablet->get_sample_info_lock(ReaderType::READER_BASE_COMPACTION);
auto& infos = _tablet->get_sample_infos(ReaderType::READER_BASE_COMPACTION);
// Resize to 5 groups
{
std::unique_lock<std::mutex> guard(lock);
infos.resize(5);
}
// Access all 5 groups (simulating estimate_batch_size calls)
for (int group_idx = 0; group_idx < 5; ++group_idx) {
std::unique_lock<std::mutex> guard(lock);
// This access should be safe even if cumu_compaction resizes its own infos
if (static_cast<size_t>(group_idx) >= infos.size()) {
has_crash = true;
return;
}
infos[group_idx].group_data_size = iter;
}
completed_iterations++;
}
};
auto cumu_compaction_thread = [this, &has_crash, &completed_iterations]() {
for (int iter = 0; iter < kIterations && !has_crash; ++iter) {
auto& lock = _tablet->get_sample_info_lock(ReaderType::READER_CUMULATIVE_COMPACTION);
auto& infos = _tablet->get_sample_infos(ReaderType::READER_CUMULATIVE_COMPACTION);
// Resize to 3 groups (smaller than base compaction's 5)
{
std::unique_lock<std::mutex> guard(lock);
infos.resize(3);
}
// Access all 3 groups
for (int group_idx = 0; group_idx < 3; ++group_idx) {
std::unique_lock<std::mutex> guard(lock);
if (static_cast<size_t>(group_idx) >= infos.size()) {
has_crash = true;
return;
}
infos[group_idx].group_data_size = iter;
}
completed_iterations++;
}
};
auto full_compaction_thread = [this, &has_crash, &completed_iterations]() {
for (int iter = 0; iter < kIterations && !has_crash; ++iter) {
auto& lock = _tablet->get_sample_info_lock(ReaderType::READER_FULL_COMPACTION);
auto& infos = _tablet->get_sample_infos(ReaderType::READER_FULL_COMPACTION);
// Resize to 2 groups (even smaller)
{
std::unique_lock<std::mutex> guard(lock);
infos.resize(2);
}
// Access all 2 groups
for (int group_idx = 0; group_idx < 2; ++group_idx) {
std::unique_lock<std::mutex> guard(lock);
if (static_cast<size_t>(group_idx) >= infos.size()) {
has_crash = true;
return;
}
infos[group_idx].group_data_size = iter;
}
completed_iterations++;
}
};
std::thread t1(base_compaction_thread);
std::thread t2(cumu_compaction_thread);
std::thread t3(full_compaction_thread);
t1.join();
t2.join();
t3.join();
EXPECT_FALSE(has_crash) << "Detected out-of-bounds access during concurrent compaction";
EXPECT_EQ(completed_iterations, kIterations * 3);
}
} // namespace doris