blob: 62bdd43963d23583cee56493ef915a6ccea6b323 [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.
// Producer-side coverage for horizontal (non-vertical) row-binlog compaction.
//
// With enable_vertical_compaction=false a row-binlog cumulative compaction that takes the
// real merge path runs Merger::vmerge_rowsets. Two things must hold for downstream
// MIN_DELTA reads to be correct:
// 1. The merged output must be globally (key, TSO)-ordered, not a UNION of the inputs.
// A UNION would write key1@TSO1..keyN@TSO1 then key1@TSO2..keyN@TSO2, so a key's
// change chain is no longer consecutive.
// 2. When a forced segment boundary splits a key range across output segments (the same
// user key lands in more than one segment), the output rowset meta must be OVERLAPPING
// so BetaRowsetReader::is_merge_iterator() picks a merge iterator.
//
// This test drives the real Merger::vmerge_rowsets on a row-binlog tablet with two input
// rowsets that share every key under different TSOs, forces a multi-segment output via
// RowsetWriterContext::max_rows_per_segment, and asserts both properties. A negative case
// on a plain (non-binlog) DUP_KEYS tablet proves the fix is gated on the row-binlog path.
#include <gen_cpp/olap_common.pb.h>
#include <gen_cpp/olap_file.pb.h>
#include <gtest/gtest.h>
#include <unistd.h>
#include <memory>
#include <string>
#include <tuple>
#include <unordered_map>
#include <vector>
#include "common/status.h"
#include "core/block/block.h"
#include "io/fs/local_file_system.h"
#include "runtime/exec_env.h"
#include "storage/merger.h"
#include "storage/olap_common.h"
#include "storage/rowset/beta_rowset.h"
#include "storage/rowset/rowset.h"
#include "storage/rowset/rowset_factory.h"
#include "storage/rowset/rowset_meta.h"
#include "storage/rowset/rowset_reader.h"
#include "storage/rowset/rowset_reader_context.h"
#include "storage/rowset/rowset_writer.h"
#include "storage/rowset/rowset_writer_context.h"
#include "storage/schema.h"
#include "storage/storage_engine.h"
#include "storage/tablet/tablet.h"
#include "storage/tablet/tablet_meta.h"
#include "storage/tablet/tablet_schema.h"
#include "storage/utils.h"
#include "util/uid_util.h"
namespace doris {
using namespace ErrorCode;
namespace {
constexpr uint32_t kMaxPathLen = 1024;
constexpr char kTestDir[] = "/row_binlog_vmerge_test";
} // namespace
class RowBinlogVmergeCompactionTest : public testing::Test {
protected:
void SetUp() override {
char buffer[kMaxPathLen];
EXPECT_NE(getcwd(buffer, kMaxPathLen), nullptr);
_absolute_dir = std::string(buffer) + kTestDir;
auto st = io::global_local_filesystem()->delete_directory(_absolute_dir);
ASSERT_TRUE(st.ok()) << st;
st = io::global_local_filesystem()->create_directory(_absolute_dir);
ASSERT_TRUE(st.ok()) << st;
EXPECT_TRUE(io::global_local_filesystem()
->create_directory(_absolute_dir + "/tablet_path")
.ok());
doris::EngineOptions options;
auto engine = std::make_unique<StorageEngine>(options);
_engine = engine.get();
ExecEnv::GetInstance()->set_storage_engine(std::move(engine));
}
void TearDown() override {
EXPECT_TRUE(io::global_local_filesystem()->delete_directory(_absolute_dir).ok());
_engine = nullptr;
ExecEnv::GetInstance()->set_storage_engine(nullptr);
}
// Row-binlog read schema: single leading key column, a value column, and the binlog TSO
// column (marked via binlog_tso_col_idx so Level1Iterator uses it as the merge sequence
// column). DUP_KEYS matches the FE-generated hidden row-binlog table, which keeps every
// change event instead of deduplicating by user key.
TabletSchemaSPtr create_row_binlog_schema() {
TabletSchemaPB pb;
pb.set_keys_type(DUP_KEYS);
pb.set_num_short_key_columns(1);
pb.set_num_rows_per_row_block(1024);
pb.set_compress_kind(COMPRESS_NONE);
pb.set_next_column_unique_id(4);
ColumnPB* key = pb.add_column();
key->set_unique_id(0);
key->set_name("k");
key->set_type("INT");
key->set_is_key(true);
key->set_length(4);
key->set_index_length(4);
key->set_is_nullable(false);
ColumnPB* val = pb.add_column();
val->set_unique_id(1);
val->set_name("v");
val->set_type("INT");
val->set_is_key(false);
val->set_length(4);
val->set_is_nullable(false);
// The binlog TSO column must be BIGINT; SegmentIterator::_update_tso_col_if_needed
// asserts OLAP_FIELD_TYPE_BIGINT for it.
ColumnPB* tso = pb.add_column();
tso->set_unique_id(2);
tso->set_name(BINLOG_TSO_COL);
tso->set_type("BIGINT");
tso->set_is_key(false);
tso->set_length(8);
tso->set_is_nullable(false);
tso->set_visible(false);
pb.set_binlog_tso_col_idx(2);
auto schema = std::make_shared<TabletSchema>();
schema->init_from_pb(pb);
return schema;
}
TabletSchemaSPtr create_plain_dup_schema() {
TabletSchemaPB pb;
pb.set_keys_type(DUP_KEYS);
pb.set_num_short_key_columns(1);
pb.set_num_rows_per_row_block(1024);
pb.set_compress_kind(COMPRESS_NONE);
pb.set_next_column_unique_id(3);
ColumnPB* key = pb.add_column();
key->set_unique_id(0);
key->set_name("k");
key->set_type("INT");
key->set_is_key(true);
key->set_length(4);
key->set_index_length(4);
key->set_is_nullable(false);
ColumnPB* val = pb.add_column();
val->set_unique_id(1);
val->set_name("v");
val->set_type("INT");
val->set_is_key(false);
val->set_length(4);
val->set_is_nullable(false);
auto schema = std::make_shared<TabletSchema>();
schema->init_from_pb(pb);
return schema;
}
RowsetWriterContext create_rowset_writer_context(const TabletSchemaSPtr& tablet_schema,
const SegmentsOverlapPB& overlap,
uint32_t max_rows_per_segment, Version version,
bool enable_binlog) {
static int64_t inc_id = 0;
RowsetWriterContext context;
RowsetId rowset_id;
rowset_id.init(++inc_id);
context.rowset_id = rowset_id;
context.rowset_type = BETA_ROWSET;
context.rowset_state = VISIBLE;
context.tablet_schema = tablet_schema;
context.tablet_path = _absolute_dir + "/tablet_path";
context.version = version;
context.segments_overlap = overlap;
context.max_rows_per_segment = max_rows_per_segment;
if (enable_binlog) {
context.write_binlog_opt().enable = true;
}
return context;
}
// Build a single-segment input rowset. Each (key, val, tso) tuple becomes one row; every
// input rowset shares the same keys but a distinct tso so the two rowsets overlap on keys.
RowsetSharedPtr create_input_rowset(const TabletSchemaSPtr& tablet_schema, bool with_tso,
const std::vector<std::tuple<int, int, int>>& rows,
int64_t version) {
auto context = create_rowset_writer_context(tablet_schema, NONOVERLAPPING, UINT32_MAX,
{version, version}, /*enable_binlog=*/false);
auto res = RowsetFactory::create_rowset_writer(*_engine, context, false);
EXPECT_TRUE(res.has_value()) << res.error();
auto writer = std::move(res).value();
Block block = tablet_schema->create_storage_block();
auto columns = std::move(block).mutate_columns();
for (const auto& [k, v, tso] : rows) {
columns[0]->insert_data((const char*)&k, sizeof(k));
columns[1]->insert_data((const char*)&v, sizeof(v));
if (with_tso) {
int64_t tso64 = tso;
columns[2]->insert_data((const char*)&tso64, sizeof(tso64));
}
}
block.set_columns(std::move(columns));
EXPECT_TRUE(writer->add_block(&block).ok());
EXPECT_TRUE(writer->flush().ok());
RowsetSharedPtr rowset;
EXPECT_EQ(Status::OK(), writer->build(rowset));
return rowset;
}
TabletSharedPtr create_tablet(const TabletSchema& tablet_schema, bool row_binlog_role) {
std::vector<TColumn> cols;
std::unordered_map<uint32_t, uint32_t> col_ordinal_to_unique_id;
for (auto i = 0; i < tablet_schema.num_columns(); i++) {
const TabletColumn& column = tablet_schema.column(i);
TColumn col;
col.column_type.type = TPrimitiveType::INT;
col.__set_column_name(column.name());
col.__set_is_key(column.is_key());
cols.push_back(col);
col_ordinal_to_unique_id[i] = column.unique_id();
}
TTabletSchema t_tablet_schema;
t_tablet_schema.__set_short_key_column_count(tablet_schema.num_short_key_columns());
t_tablet_schema.__set_schema_hash(3333);
t_tablet_schema.__set_keys_type(TKeysType::DUP_KEYS);
t_tablet_schema.__set_storage_type(TStorageType::COLUMN);
t_tablet_schema.__set_columns(cols);
TabletMetaSharedPtr tablet_meta(new TabletMeta(
1, 1, 1, 1, 1, 1, t_tablet_schema, 1, col_ordinal_to_unique_id, UniqueId(1, 2),
TTabletType::TABLET_TYPE_DISK, TCompressionType::LZ4F, 0, false));
if (row_binlog_role) {
tablet_meta->set_tablet_role(TabletRolePB::TABLET_ROLE_ROW_BINLOG);
}
TabletSharedPtr tablet(new Tablet(*_engine, tablet_meta, nullptr));
static_cast<void>(tablet->init());
return tablet;
}
// Read the output rowset in physical (segment) order. need_ordered_result=false makes the
// reader concatenate segments as written, so the returned sequence reflects the on-disk
// layout produced by the merge.
std::vector<std::tuple<int, int, int>> read_all(const RowsetSharedPtr& rowset,
const TabletSchemaSPtr& tablet_schema,
bool with_tso) {
std::vector<ColumnId> ordinals;
for (uint32_t i = 0; i < tablet_schema->num_columns(); ++i) {
ordinals.push_back(i);
}
auto read_schema = std::make_shared<ReadSchema>(
project_columns_by_ordinal(tablet_schema->columns(), ordinals));
RowsetReaderContext reader_context;
reader_context.tablet_schema = tablet_schema;
reader_context.need_ordered_result = false;
reader_context.read_schema = read_schema;
RowsetReaderSharedPtr reader;
EXPECT_TRUE(rowset->create_reader(&reader).ok());
EXPECT_TRUE(reader->init(&reader_context).ok());
std::vector<std::tuple<int, int, int>> out;
Status s;
do {
Block block = read_schema->create_read_block();
s = reader->next_batch(&block);
auto columns = block.get_columns_with_type_and_name();
for (auto i = 0; i < block.rows(); i++) {
int tso = with_tso ? static_cast<int>(columns[2].column->get_int(i)) : 0;
out.emplace_back(static_cast<int>(columns[0].column->get_int(i)),
static_cast<int>(columns[1].column->get_int(i)), tso);
}
} while (s.ok());
EXPECT_TRUE(s.is<END_OF_FILE>()) << s;
return out;
}
std::string _absolute_dir;
StorageEngine* _engine = nullptr;
};
// A real (key, TSO) merge of two row-binlog rowsets that share every key must interleave the
// events by key then TSO, and a forced segment boundary that splits a key range must mark the
// output OVERLAPPING.
TEST_F(RowBinlogVmergeCompactionTest, HorizontalMergeIsKeyTsoOrderedAndOverlapping) {
TabletSchemaSPtr schema = create_row_binlog_schema();
TabletSharedPtr tablet = create_tablet(*schema, /*row_binlog_role=*/true);
ASSERT_TRUE(tablet->is_row_binlog_tablet());
// rs0: keys 1..3 at tso=10; rs1: keys 1..3 at tso=20. The two rowsets overlap on keys.
auto rs0 = create_input_rowset(schema, /*with_tso=*/true,
{{1, 100, 10}, {2, 200, 10}, {3, 300, 10}}, /*version=*/1);
auto rs1 = create_input_rowset(schema, /*with_tso=*/true,
{{1, 110, 20}, {2, 210, 20}, {3, 310, 20}}, /*version=*/2);
// Output default is NONOVERLAPPING (matches compaction.cpp for the non-quick-merge path).
// With the (key, TSO) merge the physical order is
// (1,100),(1,110),(2,200),(2,210),(3,300),(3,310)
// so max_rows_per_segment=3 splits key 2 across two segments (seg0 max_key=2, seg1
// min_key=2), making the segments genuinely overlap.
auto ctx = create_rowset_writer_context(schema, NONOVERLAPPING, /*max_rows_per_segment=*/3,
{0, rs1->end_version()}, /*enable_binlog=*/true);
auto res = RowsetFactory::create_rowset_writer(*_engine, ctx, /*is_vertical=*/false);
ASSERT_TRUE(res.has_value()) << res.error();
auto writer = std::move(res).value();
std::vector<RowsetReaderSharedPtr> input_rs_readers;
for (auto& rowset : {rs0, rs1}) {
RowsetReaderSharedPtr rs_reader;
ASSERT_TRUE(rowset->create_reader(&rs_reader).ok());
input_rs_readers.push_back(std::move(rs_reader));
}
Merger::Statistics stats;
ASSERT_TRUE(Merger::vmerge_rowsets(tablet, ReaderType::READER_CUMULATIVE_COMPACTION, *schema,
input_rs_readers, writer.get(), &stats)
.ok());
RowsetSharedPtr out_rowset;
ASSERT_EQ(Status::OK(), writer->build(out_rowset));
// Fix (B): a multi-segment output whose segments share a boundary key is OVERLAPPING.
EXPECT_GT(out_rowset->rowset_meta()->num_segments(), 1);
EXPECT_EQ(OVERLAPPING, out_rowset->rowset_meta()->segments_overlap());
EXPECT_TRUE(out_rowset->rowset_meta()->is_segments_overlapping());
// Fix (A): the merged stream is globally (key, TSO)-ordered, so each key's two events are
// consecutive with the earlier-TSO (lower version) row first. A buggy UNION would instead
// produce (1,100),(2,200),(3,300),(1,110),(2,210),(3,310). The binlog TSO column itself is
// rewritten to the rowset commit TSO on read (_update_tso_col_if_needed), so we assert on
// (key, val) which carries the per-event identity.
auto rows = read_all(out_rowset, schema, /*with_tso=*/false);
std::vector<std::pair<int, int>> got;
for (const auto& [k, v, tso] : rows) {
got.emplace_back(k, v);
}
std::vector<std::pair<int, int>> expected = {{1, 100}, {1, 110}, {2, 200},
{2, 210}, {3, 300}, {3, 310}};
EXPECT_EQ(expected, got);
}
// Guard: the same overlapping-input + forced-boundary layout on a plain (non row-binlog)
// DUP_KEYS tablet must NOT be marked OVERLAPPING, proving both fixes are gated on the
// row-binlog path.
TEST_F(RowBinlogVmergeCompactionTest, PlainDupMergeStaysNonOverlapping) {
TabletSchemaSPtr schema = create_plain_dup_schema();
TabletSharedPtr tablet = create_tablet(*schema, /*row_binlog_role=*/false);
ASSERT_FALSE(tablet->is_row_binlog_tablet());
auto rs0 =
create_input_rowset(schema, /*with_tso=*/false, {{1, 100, 0}, {2, 200, 0}, {3, 300, 0}},
/*version=*/1);
auto rs1 =
create_input_rowset(schema, /*with_tso=*/false, {{1, 110, 0}, {2, 210, 0}, {3, 310, 0}},
/*version=*/2);
auto ctx = create_rowset_writer_context(schema, NONOVERLAPPING, /*max_rows_per_segment=*/2,
{0, rs1->end_version()}, /*enable_binlog=*/false);
auto res = RowsetFactory::create_rowset_writer(*_engine, ctx, /*is_vertical=*/false);
ASSERT_TRUE(res.has_value()) << res.error();
auto writer = std::move(res).value();
std::vector<RowsetReaderSharedPtr> input_rs_readers;
for (auto& rowset : {rs0, rs1}) {
RowsetReaderSharedPtr rs_reader;
ASSERT_TRUE(rowset->create_reader(&rs_reader).ok());
input_rs_readers.push_back(std::move(rs_reader));
}
Merger::Statistics stats;
ASSERT_TRUE(Merger::vmerge_rowsets(tablet, ReaderType::READER_CUMULATIVE_COMPACTION, *schema,
input_rs_readers, writer.get(), &stats)
.ok());
RowsetSharedPtr out_rowset;
ASSERT_EQ(Status::OK(), writer->build(out_rowset));
EXPECT_NE(OVERLAPPING, out_rowset->rowset_meta()->segments_overlap());
EXPECT_FALSE(out_rowset->rowset_meta()->is_segments_overlapping());
}
} // namespace doris