TDengine/source/dnode/vnode/test/tsdbRepairTest.cpp
WANG Xu c52c68aa4f
sync: apply remaining build system changes from monorepo (main)
The following commits could not be applied individually due to context
differences between the monorepo and the public repo's build files.
They have been applied as a cumulative diff to ensure the final state
matches the monorepo exactly:

- chore: sync CI files with 3.0 branch to eliminate merge conflicts (rd-public/tsdb!271)
- revert(refactor): dynamically link taosd taosudf taosmqtt against libtaosnative.so to reduce binary size (revert #183) (rd-public/tsdb!282)
- fix(docs): autofix formatting issues across all doc files (rd-public/tsdb!296)
- feat: support -DBUILD_SANITIZER=true on windows for debug build (rd-public/tsdb!291)
- feat(build): build cache, mirror, and sccache optimizations (rd-public/tsdb!326)
- docs: update image for three replica (rd-public/tsdb!324)
- enh: shared storage on windows (rd-public/tsdb!333)
- fix(cmake): convert ext_libs3 from git clone to URL tarball download (rd-public/tsdb!360)
- feat: dual-source deps and comprehensive docs/packaging (cherry-pick to main) (rd-public/tsdb!352)
- fix(cmake): guard DOWNLOAD_EXTRACT_TIMESTAMP for CMake < 3.24 and fix duplicate Cargo.lock entry (rd-public/tsdb!369)
- fix: test case execution failure in pytest.sh (rd-public/tsdb!338)
- enh: built-in compilation support for Python UDF plugins use abi3 (rd-public/tsdb!325)
2026-05-23 14:11:50 +08:00

515 lines
15 KiB
C++

/*
* Copyright (c) 2019 TAOS Data, Inc. <jhtao@taosdata.com>
*
* This program is free software: you can use, redistribute, and/or modify
* it under the terms of the GNU Affero General Public License, version 3
* or later ("AGPL"), as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <cstring>
extern "C" {
#include "dmRepair.h"
#include "vnodeInt.h"
typedef struct STFileObj STFileObj;
typedef struct SDataFileWriter SDataFileWriter;
typedef struct SSttFileWriter SSttFileWriter;
typedef struct STFileSet STFileSet;
typedef struct SBlockData SBlockData;
typedef struct {
int32_t size;
int32_t capacity;
STFileSet **data;
} TFileSetArray;
typedef struct {
SRowKey key;
int64_t version;
} STsdbRowKey;
typedef struct {
int64_t suid;
int64_t uid;
STsdbRowKey firstKey;
STsdbRowKey lastKey;
int64_t minVer;
int64_t maxVer;
int64_t blockOffset;
int64_t smaOffset;
int32_t blockSize;
int32_t blockKeySize;
int32_t smaSize;
int32_t numRow;
int32_t count;
} SBrinRecord;
struct SBlockData {
int64_t suid;
int64_t uid;
int32_t nRow;
int64_t *aUid;
int64_t *aVersion;
TSKEY *aTSKEY;
};
enum {
TSDB_REPAIR_ACTION_KEEP = 0,
TSDB_REPAIR_ACTION_DROP = 1,
TSDB_REPAIR_ACTION_REBUILD = 2,
};
typedef enum {
TSDB_FEDIT_COMMIT = 1,
TSDB_FEDIT_MERGE,
TSDB_FEDIT_COMPACT,
TSDB_FEDIT_RETENTION,
TSDB_FEDIT_SSMIGRATE,
TSDB_FEDIT_ROLLUP,
TSDB_FEDIT_FORCE_REPAIR,
} EFEditT;
typedef enum {
TSDB_FCURRENT = 1,
TSDB_FCURRENT_C,
TSDB_FCURRENT_M,
} EFCurrentT;
struct STsdb {
char *path;
SVnode *pVnode;
char name[VNODE_TSDB_NAME_LEN];
};
typedef struct STFileSystem {
STsdb *tsdb;
tsem_t canEdit;
int32_t fsstate;
int32_t rollupLevel;
int64_t neid;
EFEditT etype;
TFileSetArray fSetArr[1];
TFileSetArray fSetArrTmp[1];
} STFileSystem;
bool tsdbRepairDataBlockLooksValid(const SBlockData *blockData, const SBrinRecord *record);
bool tsdbRepairSttBlockLooksValid(const SBlockData *blockData);
int32_t tsdbRepairResolveCoreAction(int32_t keptBlocks, int32_t droppedBlocks);
int32_t tsdbRepairResolveSttAction(int32_t keptDataBlocks, int32_t keptTombBlocks, int32_t droppedDataBlocks,
int32_t droppedTombBlocks);
bool tsdbRepairShouldProcessFileSet(int32_t vnodeId, int32_t fid);
EDmRepairStrategy tsdbRepairNormalizeStrategy(EDmRepairStrategy strategy);
const char *tsdbRepairStrategyName(EDmRepairStrategy strategy);
int32_t tsdbRepairResolveMode(EDmRepairStrategy strategy);
void tsdbRepairBuildHeadOnlyBrinRecord(const SBrinRecord *src, bool keepSma, SBrinRecord *dst);
int32_t tsdbRepairDescribeHeadOnlyOps(bool hasHead, bool hasSma, bool dropSma);
bool tsdbRepairFileAffected(const STFileObj *fobj);
const char *tsdbRepairFileIssue(const STFileObj *fobj);
int32_t tsdbDataFileWriterClose(SDataFileWriter **writer, bool abort, void *opArray);
int32_t tsdbSttFileWriterClose(SSttFileWriter **writer, int8_t abort, void *opArray);
void current_fname(STsdb *pTsdb, char *fname, EFCurrentT ftype);
int32_t tsdbFSEditCommit(STFileSystem *fs);
}
namespace {
bool g_hasTsdbRepairTarget = false;
int32_t g_targetVnodeId = 0;
int32_t g_targetFileId = 0;
SRepairTsdbFileOpt g_targetTsdbFileOpt = []{ SRepairTsdbFileOpt o = {}; o.strategy = DM_REPAIR_STRATEGY_TSDB_DROP_INVALID_ONLY; return o; }();
} // namespace
extern "C" {
SDmNotifyHandle dmNotifyHdl = {};
}
extern "C" const SRepairTsdbFileOpt *dmRepairGetTsdbFileOpt(int32_t vnodeId, int32_t fileId) {
if (g_hasTsdbRepairTarget && vnodeId == g_targetVnodeId && fileId == g_targetFileId) {
return &g_targetTsdbFileOpt;
}
return nullptr;
}
struct TestFileMeta {
int32_t type;
SDiskID did;
int32_t fid;
int32_t lcn;
int32_t mid;
int64_t cid;
int64_t size;
int64_t minVer;
int64_t maxVer;
union {
struct {
int32_t level;
} stt[1];
};
};
struct TestFileObj {
TdThreadMutex mutex;
TestFileMeta f[1];
int32_t state;
int32_t ref;
int32_t nlevel;
char fname[TSDB_FILENAME_LEN];
};
int main(int argc, char **argv) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
class TsdbRepairValidationTest : public ::testing::Test {
protected:
void SetUp() override {
std::memset(&block_, 0, sizeof(block_));
std::memset(&record_, 0, sizeof(record_));
block_.suid = 42;
block_.uid = 7;
block_.nRow = 2;
block_.aTSKEY = ts_;
block_.aVersion = versions_;
record_.suid = 42;
record_.uid = 7;
}
SBlockData block_ = {};
SBrinRecord record_ = {};
int64_t versions_[2] = {10, 11};
TSKEY ts_[2] = {100, 101};
int64_t uids_[2] = {7001, 7002};
};
TEST_F(TsdbRepairValidationTest, AcceptsMatchingCoreBlock) {
EXPECT_TRUE(tsdbRepairDataBlockLooksValid(&block_, &record_));
}
TEST_F(TsdbRepairValidationTest, RejectsCoreBlockWithMismatchedUid) {
record_.uid = 9;
EXPECT_FALSE(tsdbRepairDataBlockLooksValid(&block_, &record_));
}
TEST_F(TsdbRepairValidationTest, RejectsCoreBlockWithoutDecodedArrays) {
block_.aTSKEY = nullptr;
EXPECT_FALSE(tsdbRepairDataBlockLooksValid(&block_, &record_));
}
TEST_F(TsdbRepairValidationTest, AcceptsSttBlockWithDecodedRows) {
block_.uid = 0;
block_.aUid = uids_;
EXPECT_TRUE(tsdbRepairSttBlockLooksValid(&block_));
}
TEST_F(TsdbRepairValidationTest, RejectsSttBlockWithoutUidArrayWhenUidIsZero) {
block_.uid = 0;
block_.aUid = nullptr;
EXPECT_FALSE(tsdbRepairSttBlockLooksValid(&block_));
}
TEST(TsdbRepairDecisionTest, KeepsHealthyCore) {
EXPECT_EQ(tsdbRepairResolveCoreAction(3, 0), TSDB_REPAIR_ACTION_KEEP);
}
TEST(TsdbRepairDecisionTest, RebuildsPartiallyDamagedCore) {
EXPECT_EQ(tsdbRepairResolveCoreAction(2, 1), TSDB_REPAIR_ACTION_REBUILD);
}
TEST(TsdbRepairDecisionTest, DropsFullyDamagedCore) {
EXPECT_EQ(tsdbRepairResolveCoreAction(0, 2), TSDB_REPAIR_ACTION_DROP);
}
TEST(TsdbRepairDecisionTest, KeepsHealthySttFile) {
EXPECT_EQ(tsdbRepairResolveSttAction(2, 1, 0, 0), TSDB_REPAIR_ACTION_KEEP);
}
TEST(TsdbRepairDecisionTest, RebuildsPartiallyDamagedSttFile) {
EXPECT_EQ(tsdbRepairResolveSttAction(1, 0, 0, 1), TSDB_REPAIR_ACTION_REBUILD);
}
TEST(TsdbRepairDecisionTest, DropsFullyDamagedSttFile) {
EXPECT_EQ(tsdbRepairResolveSttAction(0, 0, 2, 1), TSDB_REPAIR_ACTION_DROP);
}
enum {
TSDB_REPAIR_MODE_DROP_INVALID_ONLY = 0,
TSDB_REPAIR_MODE_HEAD_ONLY_REBUILD = 1,
TSDB_REPAIR_MODE_FULL_REBUILD = 2,
};
enum {
TSDB_REPAIR_HEAD_OP_REMOVE_HEAD = 1 << 0,
TSDB_REPAIR_HEAD_OP_CREATE_HEAD = 1 << 1,
TSDB_REPAIR_HEAD_OP_REMOVE_DATA = 1 << 2,
TSDB_REPAIR_HEAD_OP_CREATE_DATA = 1 << 3,
TSDB_REPAIR_HEAD_OP_REMOVE_SMA = 1 << 4,
TSDB_REPAIR_HEAD_OP_CREATE_SMA = 1 << 5,
};
TEST(TsdbRepairStrategyTest, UsesPublicTsdbStrategyNames) {
EXPECT_STREQ(tsdbRepairStrategyName(DM_REPAIR_STRATEGY_TSDB_DROP_INVALID_ONLY), "drop_invalid_only");
EXPECT_STREQ(tsdbRepairStrategyName(DM_REPAIR_STRATEGY_TSDB_HEAD_ONLY_REBUILD), "head_only_rebuild");
EXPECT_STREQ(tsdbRepairStrategyName(DM_REPAIR_STRATEGY_TSDB_FULL_REBUILD), "full_rebuild");
}
TEST(TsdbRepairModeTest, MapsDropOnlyStrategyToBadFileOnlyMode) {
EXPECT_EQ(tsdbRepairResolveMode(DM_REPAIR_STRATEGY_TSDB_DROP_INVALID_ONLY), TSDB_REPAIR_MODE_DROP_INVALID_ONLY);
}
TEST(TsdbRepairModeTest, MapsHeadOnlyStrategyToHeadOnlyMode) {
EXPECT_EQ(tsdbRepairResolveMode(DM_REPAIR_STRATEGY_TSDB_HEAD_ONLY_REBUILD), TSDB_REPAIR_MODE_HEAD_ONLY_REBUILD);
}
TEST(TsdbRepairModeTest, MapsFullRebuildStrategyToFullRebuildMode) {
EXPECT_EQ(tsdbRepairResolveMode(DM_REPAIR_STRATEGY_TSDB_FULL_REBUILD), TSDB_REPAIR_MODE_FULL_REBUILD);
}
TEST(TsdbRepairDefaultStrategyTest, NormalizesMissingStrategyToDropInvalidOnly) {
EXPECT_EQ(tsdbRepairNormalizeStrategy(DM_REPAIR_STRATEGY_NONE), DM_REPAIR_STRATEGY_TSDB_DROP_INVALID_ONLY);
}
class TsdbRepairScopeTest : public ::testing::Test {
protected:
void SetUp() override {
g_hasTsdbRepairTarget = false;
g_targetVnodeId = 0;
g_targetFileId = 0;
g_targetTsdbFileOpt.strategy = DM_REPAIR_STRATEGY_TSDB_DROP_INVALID_ONLY;
}
void TearDown() override {
g_hasTsdbRepairTarget = false;
g_targetVnodeId = 0;
g_targetFileId = 0;
}
};
TEST_F(TsdbRepairScopeTest, SkipsFileSetWithoutExplicitTarget) {
EXPECT_FALSE(tsdbRepairShouldProcessFileSet(7, 101));
}
TEST_F(TsdbRepairScopeTest, ProcessesExplicitlyTargetedFileSet) {
g_hasTsdbRepairTarget = true;
g_targetVnodeId = 7;
g_targetFileId = 101;
EXPECT_TRUE(tsdbRepairShouldProcessFileSet(7, 101));
}
TEST_F(TsdbRepairScopeTest, SkipsDifferentFileIdWithinSameVnode) {
g_hasTsdbRepairTarget = true;
g_targetVnodeId = 7;
g_targetFileId = 101;
EXPECT_FALSE(tsdbRepairShouldProcessFileSet(7, 102));
}
TEST(TsdbRepairBrinRecordTest, KeepsSmaOffsetsForHealthyHeadOnlyRewrite) {
SBrinRecord src = {
.suid = 11,
.uid = 12,
.minVer = 13,
.maxVer = 14,
.blockOffset = 15,
.smaOffset = 16,
.blockSize = 17,
.blockKeySize = 18,
.smaSize = 19,
.numRow = 20,
.count = 21,
};
SBrinRecord dst = {};
tsdbRepairBuildHeadOnlyBrinRecord(&src, true, &dst);
EXPECT_EQ(dst.suid, src.suid);
EXPECT_EQ(dst.uid, src.uid);
EXPECT_EQ(dst.minVer, src.minVer);
EXPECT_EQ(dst.maxVer, src.maxVer);
EXPECT_EQ(dst.blockOffset, src.blockOffset);
EXPECT_EQ(dst.blockSize, src.blockSize);
EXPECT_EQ(dst.blockKeySize, src.blockKeySize);
EXPECT_EQ(dst.smaOffset, src.smaOffset);
EXPECT_EQ(dst.smaSize, src.smaSize);
EXPECT_EQ(dst.numRow, src.numRow);
EXPECT_EQ(dst.count, src.count);
}
TEST(TsdbRepairBrinRecordTest, ClearsSmaOffsetsForDamagedHeadOnlyRewrite) {
SBrinRecord src = {
.suid = 21,
.uid = 22,
.minVer = 23,
.maxVer = 24,
.blockOffset = 25,
.smaOffset = 26,
.blockSize = 27,
.blockKeySize = 28,
.smaSize = 29,
.numRow = 30,
.count = 31,
};
SBrinRecord dst = {};
tsdbRepairBuildHeadOnlyBrinRecord(&src, false, &dst);
EXPECT_EQ(dst.suid, src.suid);
EXPECT_EQ(dst.uid, src.uid);
EXPECT_EQ(dst.blockOffset, src.blockOffset);
EXPECT_EQ(dst.blockSize, src.blockSize);
EXPECT_EQ(dst.blockKeySize, src.blockKeySize);
EXPECT_EQ(dst.numRow, src.numRow);
EXPECT_EQ(dst.count, src.count);
EXPECT_EQ(dst.smaOffset, 0);
EXPECT_EQ(dst.smaSize, 0);
}
TEST(TsdbRepairHeadOnlyRebuildTest, ReplacesOnlyHeadWhenSmaStaysHealthy) {
int32_t opMask = tsdbRepairDescribeHeadOnlyOps(true, true, false);
EXPECT_NE(opMask & TSDB_REPAIR_HEAD_OP_REMOVE_HEAD, 0);
EXPECT_NE(opMask & TSDB_REPAIR_HEAD_OP_CREATE_HEAD, 0);
EXPECT_EQ(opMask & TSDB_REPAIR_HEAD_OP_REMOVE_DATA, 0);
EXPECT_EQ(opMask & TSDB_REPAIR_HEAD_OP_CREATE_DATA, 0);
EXPECT_EQ(opMask & TSDB_REPAIR_HEAD_OP_REMOVE_SMA, 0);
EXPECT_EQ(opMask & TSDB_REPAIR_HEAD_OP_CREATE_SMA, 0);
}
TEST(TsdbRepairHeadOnlyRebuildTest, RemovesSmaWhenOriginalSmaIsBad) {
int32_t opMask = tsdbRepairDescribeHeadOnlyOps(true, true, true);
EXPECT_NE(opMask & TSDB_REPAIR_HEAD_OP_REMOVE_HEAD, 0);
EXPECT_NE(opMask & TSDB_REPAIR_HEAD_OP_CREATE_HEAD, 0);
EXPECT_EQ(opMask & TSDB_REPAIR_HEAD_OP_REMOVE_DATA, 0);
EXPECT_EQ(opMask & TSDB_REPAIR_HEAD_OP_CREATE_DATA, 0);
EXPECT_NE(opMask & TSDB_REPAIR_HEAD_OP_REMOVE_SMA, 0);
EXPECT_EQ(opMask & TSDB_REPAIR_HEAD_OP_CREATE_SMA, 0);
}
class TsdbRepairFileIssueTest : public ::testing::Test {
protected:
void SetUp() override {
std::snprintf(path_, sizeof(path_), "/tmp/tsdb-repair-test-XXXXXX");
int fd = mkstemp(path_);
ASSERT_GE(fd, 0);
static const char payload[] = "0123456789abcdef";
ssize_t written = write(fd, payload, sizeof(payload) - 1);
ASSERT_EQ(written, static_cast<ssize_t>(sizeof(payload) - 1));
ASSERT_EQ(syscall(SYS_close, fd), 0);
std::memset(&fobj_, 0, sizeof(fobj_));
std::snprintf(fobj_.fname, sizeof(fobj_.fname), "%s", path_);
fobj_.f->size = 8;
}
void TearDown() override { taosRemoveFile(path_); }
char path_[TSDB_FILENAME_LEN] = {0};
TestFileObj fobj_ = {};
};
TEST_F(TsdbRepairFileIssueTest, SizeMismatchDoesNotMarkFileAsAffected) {
const STFileObj *fobj = reinterpret_cast<const STFileObj *>(&fobj_);
EXPECT_FALSE(tsdbRepairFileAffected(fobj));
EXPECT_EQ(tsdbRepairFileIssue(fobj), nullptr);
}
TEST(TsdbSttFileWriterCloseTest, NullWriterHandleDoesNotCrash) {
ASSERT_EXIT(
{
SSttFileWriter *writer = nullptr;
int32_t code = tsdbSttFileWriterClose(&writer, 1, nullptr);
if (code != 0) {
_exit(1);
}
_exit(0);
},
::testing::ExitedWithCode(0), "");
}
TEST(TsdbDataFileWriterCloseTest, NullWriterPointerDoesNotCrash) {
ASSERT_EXIT(
{
SDataFileWriter **writer = nullptr;
int32_t code = tsdbDataFileWriterClose(writer, true, nullptr);
if (code != 0) {
_exit(1);
}
_exit(0);
},
::testing::ExitedWithCode(0), "");
}
class TsdbForceRepairCommitTest : public ::testing::Test {
protected:
void SetUp() override {
std::snprintf(rootPath_, sizeof(rootPath_), "/tmp/tsdb-force-repair-commit-XXXXXX");
ASSERT_NE(mkdtemp(rootPath_), nullptr);
std::snprintf(tsdbPath_, sizeof(tsdbPath_), "%s/testtsdb", rootPath_);
ASSERT_EQ(taosMkDir(tsdbPath_), 0);
std::memset(&vnode_, 0, sizeof(vnode_));
std::memset(&tsdb_, 0, sizeof(tsdb_));
std::memset(&fs_, 0, sizeof(fs_));
vnode_.path = rootPath_;
vnode_.config.vgId = 7;
std::snprintf(tsdb_.name, sizeof(tsdb_.name), "testtsdb");
tsdb_.pVnode = &vnode_;
fs_.tsdb = &tsdb_;
fs_.etype = TSDB_FEDIT_FORCE_REPAIR;
fs_.fSetArr->size = 0;
fs_.fSetArr->capacity = 0;
fs_.fSetArr->data = nullptr;
fs_.fSetArrTmp->size = 0;
fs_.fSetArrTmp->capacity = 0;
fs_.fSetArrTmp->data = nullptr;
ASSERT_EQ(tsem_init(&fs_.canEdit, 0, 1), 0);
char currentTmp[TSDB_FILENAME_LEN] = {0};
current_fname(&tsdb_, currentTmp, TSDB_FCURRENT_M);
TdFilePtr fp = taosCreateFile(currentTmp, TD_FILE_CREATE | TD_FILE_WRITE | TD_FILE_TRUNC);
ASSERT_NE(fp, nullptr);
ASSERT_EQ(taosWriteFile(fp, "{}", 2), 2);
ASSERT_EQ(taosCloseFile(&fp), 0);
}
void TearDown() override {
ASSERT_EQ(tsem_destroy(&fs_.canEdit), 0);
taosRemoveDir(rootPath_);
}
char rootPath_[TSDB_FILENAME_LEN] = {0};
char tsdbPath_[TSDB_FILENAME_LEN] = {0};
SVnode vnode_ = {};
STsdb tsdb_ = {};
STFileSystem fs_ = {};
};
TEST_F(TsdbForceRepairCommitTest, ForceRepairCommitReleasesCanEditSemaphore) {
ASSERT_EQ(tsem_wait(&fs_.canEdit), 0);
ASSERT_EQ(tsdbFSEditCommit(&fs_), 0);
EXPECT_EQ(tsem_timewait(&fs_.canEdit, 10), 0);
}